Initial public release

This commit is contained in:
thakares committed 2026-06-11 20:19:03 +05:30
commit c1107147b4
92 files changed
+10562

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
use super::svg::{wrap_in_svg, DEFAULT_TEXT_COLOR};
pub fn generate_bar_chart(data: &[(String, i64)]) -> String {
if data.is_empty() {
return r##"<svg viewBox="0 0 600 200" class="w-full h-auto bg-slate-900/50 rounded-xl border border-slate-800/80 p-4" xmlns="http://www.w3.org/2000/svg">
<text x="300" y="100" fill="#94a3b8" text-anchor="middle" font-family="system-ui, sans-serif">No data available</text>
</svg>"##.to_string();
}
let bar_height = 24.0;
let gap = 14.0;
let pad_top = 10.0;
let pad_bottom = 10.0;
let pad_left = 130.0;
let pad_right = 70.0;
let width = 600.0;
let height = pad_top + pad_bottom + (data.len() as f64 * (bar_height + gap)) - gap;
let chart_w = width - pad_left - pad_right;
let max_val = data.iter().map(|(_, v)| *v).max().unwrap_or(0);
let max_x = if max_val == 0 { 1.0 } else { max_val as f64 };
let mut bars = String::new();
let total_count: i64 = data.iter().map(|(_, v)| *v).sum();
for (i, (label, val)) in data.iter().enumerate() {
let y = pad_top + (i as f64 * (bar_height + gap));
let bar_w = (*val as f64 / max_x) * chart_w;
let pct = if total_count > 0 {
(*val as f64 / total_count as f64) * 100.0
} else {
0.0
};
// Truncate long labels
let display_label = if label.len() > 18 {
format!("{}...", &label[0..15])
} else {
label.to_string()
};
bars.push_str(&format!(
r##"<!-- Row {i} -->
<text x="{pad_left_label}" y="{text_y}" fill="#e2e8f0" font-size="12" font-family="system-ui, sans-serif" font-weight="500" text-anchor="end" alignment-baseline="middle">{label}</text>
<rect x="{pad_left}" y="{y}" width="{bar_w:.1}" height="{bar_height}" rx="4" fill="url(#barGrad)"/>
<text x="{val_x:.1}" y="{text_y}" fill="{text_color}" font-size="11" font-family="system-ui, sans-serif" alignment-baseline="middle">{val} ({pct:.1}%)</text>
"##,
i = i,
pad_left_label = pad_left - 10.0,
text_y = y + (bar_height / 2.0) + 1.0,
label = display_label,
pad_left = pad_left,
y = y,
bar_w = bar_w.max(2.0),
bar_height = bar_height,
val_x = pad_left + bar_w.max(2.0) + 8.0,
val = val,
pct = pct,
text_color = DEFAULT_TEXT_COLOR
));
}
wrap_in_svg(width, height, &bars)
}
+106
View File
@@ -0,0 +1,106 @@
use super::svg::{wrap_in_svg, DEFAULT_GRID_COLOR, DEFAULT_TEXT_COLOR};
pub fn generate_line_chart(data: &[(String, i64)]) -> String {
if data.is_empty() {
return r##"<svg viewBox="0 0 800 300" class="w-full h-auto bg-slate-900/50 rounded-xl border border-slate-800/80 p-4" xmlns="http://www.w3.org/2000/svg">
<text x="400" y="150" fill="#94a3b8" text-anchor="middle" font-family="system-ui, sans-serif">No traffic data available</text>
</svg>"##.to_string();
}
let width = 800.0;
let height = 300.0;
let pad_left = 60.0;
let pad_right = 30.0;
let pad_top = 30.0;
let pad_bottom = 40.0;
let chart_w = width - pad_left - pad_right;
let chart_h = height - pad_top - pad_bottom;
// Find max value for scaling
let max_val = data.iter().map(|(_, v)| *v).max().unwrap_or(0);
let max_y = if max_val == 0 { 10.0 } else { max_val as f64 };
// Y-axis grid ticks
let mut inner_content = String::new();
let ticks = 4;
for i in 0..=ticks {
let pct = i as f64 / ticks as f64;
let y = pad_top + chart_h - (pct * chart_h);
let val = (pct * max_y).round() as i64;
inner_content.push_str(&format!(
r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-dasharray="4,4" stroke-width="1"/>
<text x="{}" y="{}" fill="{}" font-size="11" font-family="system-ui, sans-serif" text-anchor="end" alignment-baseline="middle">{}</text>"##,
pad_left, y, width - pad_right, y, DEFAULT_GRID_COLOR, pad_left - 10.0, y, DEFAULT_TEXT_COLOR, val
));
}
// Coordinates calculations
let count = data.len();
let step_x = if count > 1 { chart_w / (count - 1) as f64 } else { chart_w };
let mut points = Vec::new();
for (i, &(_, val)) in data.iter().enumerate() {
let x = pad_left + (i as f64 * step_x);
let y = pad_top + chart_h - ((val as f64 / max_y) * chart_h);
points.push((x, y));
}
// Path strings
let mut line_path = String::new();
let mut area_path = String::new();
if !points.is_empty() {
line_path.push_str(&format!("M {:.1} {:.1}", points[0].0, points[0].1));
area_path.push_str(&format!("M {:.1} {:.1}", points[0].0, pad_top + chart_h));
area_path.push_str(&format!("L {:.1} {:.1}", points[0].0, points[0].1));
for &(x, y) in points.iter().skip(1) {
line_path.push_str(&format!(" L {:.1} {:.1}", x, y));
area_path.push_str(&format!(" L {:.1} {:.1}", x, y));
}
let last_x = points[points.len() - 1].0;
area_path.push_str(&format!(" L {:.1} {:.1} Z", last_x, pad_top + chart_h));
}
// X-axis labels
let mut x_labels = String::new();
let label_step = (count / 7).max(1);
for (i, (label, _)) in data.iter().enumerate() {
if i % label_step == 0 || i == count - 1 {
let x = points[i].0;
let short_label = if label.len() == 10 { &label[5..] } else { label };
x_labels.push_str(&format!(
r##"<text x="{}" y="{}" fill="{}" font-size="11" font-family="system-ui, sans-serif" text-anchor="middle">{}</text>"##,
x, height - 15.0, DEFAULT_TEXT_COLOR, short_label
));
x_labels.push_str(&format!(
r##"<line x1="{}" y1="{}" x2="{}" y2="{}" stroke="{}" stroke-width="1"/>"##,
x, pad_top + chart_h, x, pad_top + chart_h + 5.0, DEFAULT_GRID_COLOR
));
}
}
// Draw little circles on points
let mut dots = String::new();
if count < 40 {
for &(x, y) in &points {
dots.push_str(&format!(
r##"<circle cx="{:.1}" cy="{:.1}" r="4" fill="#6366f1" stroke="#1e293b" stroke-width="2"/>"##,
x, y
));
}
}
inner_content.push_str(&format!(
r##"<path d="{}" fill="url(#areaGrad)"/>
<path d="{}" fill="none" stroke="#6366f1" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
{}
{}"##,
area_path, line_path, dots, x_labels
));
wrap_in_svg(width, height, &inner_content)
}
+10
View File
@@ -0,0 +1,10 @@
pub mod svg;
pub mod line;
pub mod bar;
pub mod pie;
pub mod timeseries;
pub use line::generate_line_chart;
pub use bar::generate_bar_chart;
pub use pie::generate_pie_chart;
pub use timeseries::generate_timeseries_chart;
+8
View File
@@ -0,0 +1,8 @@
// Donut / Pie SVG Chart Renderer (Placeholder / Stub)
pub fn generate_pie_chart(_data: &[(String, i64)]) -> String {
r##"<svg viewBox="0 0 400 400" class="w-full h-auto bg-slate-900/50 rounded-xl border border-slate-800/80 p-4" xmlns="http://www.w3.org/2000/svg">
<circle cx="200" cy="200" r="100" fill="none" stroke="#6366f1" stroke-width="40"/>
<text x="200" y="200" fill="#94a3b8" text-anchor="middle" font-family="system-ui, sans-serif" alignment-baseline="middle">Donut Chart Placeholder</text>
</svg>"##.to_string()
}
+25
View File
@@ -0,0 +1,25 @@
pub const DEFAULT_GRID_COLOR: &str = "#334155";
pub const DEFAULT_TEXT_COLOR: &str = "#94a3b8";
pub fn wrap_in_svg(width: f64, height: f64, content: &str) -> String {
// Note: raw string literals with # inside double-quotes can break standard parsing,
// so we use r##"..."## formatting to prevent compiler errors.
format!(
r##"<svg viewBox="0 0 {width} {height}" class="w-full h-auto" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="areaGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6366f1" stop-opacity="0.35"/>
<stop offset="100%" stop-color="#6366f1" stop-opacity="0.01"/>
</linearGradient>
<linearGradient id="barGrad" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#4f46e5"/>
<stop offset="100%" stop-color="#818cf8"/>
</linearGradient>
</defs>
{content}
</svg>"##,
width = width,
height = height,
content = content
)
}
+9
View File
@@ -0,0 +1,9 @@
use super::line::generate_line_chart;
/// Generates a timeseries traffic trend SVG chart.
///
/// This provides a specialized wrapper around line charts, specifically
/// configured for timeseries data streams (daily/monthly traffic logs).
pub fn generate_timeseries_chart(data: &[(String, i64)]) -> String {
generate_line_chart(data)
}