Skip to main content

gallery/
gallery.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Generate an HTML gallery showcasing all esoc-chart capabilities.
3
4use std::fmt::Write;
5
6use esoc_chart::express::{
7    area, bar, boxplot, grouped_bar, heatmap, histogram, line, pie_labeled, scatter, stacked_bar,
8};
9use esoc_chart::grammar::annotation::Annotation;
10use esoc_chart::grammar::chart::Chart;
11use esoc_chart::grammar::coord::CoordSystem;
12use esoc_chart::grammar::layer::{Layer, MarkType};
13use esoc_chart::grammar::stat::Stat;
14
15fn main() -> esoc_chart::error::Result<()> {
16    // ── Simple RNG for reproducible data ─────────────────────────────
17    struct Rng(u64);
18    impl Rng {
19        fn uniform(&mut self) -> f64 {
20            self.0 = self
21                .0
22                .wrapping_mul(6_364_136_223_846_793_005)
23                .wrapping_add(1);
24            (self.0 >> 11) as f64 / (1u64 << 53) as f64
25        }
26        fn normal(&mut self) -> f64 {
27            let u1 = self.uniform().max(1e-15);
28            let u2 = self.uniform();
29            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
30        }
31    }
32    let mut sections: Vec<(&str, String)> = Vec::new();
33    let mut rng = Rng(42);
34
35    // ── Scatter ──────────────────────────────────────────────────────
36    {
37        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
38        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
39        let svg = scatter(&x, &y)
40            .title("Scatter Plot")
41            .x_label("X")
42            .y_label("Y")
43            .size(500.0, 350.0)
44            .to_svg()?;
45        sections.push(("Scatter", svg));
46    }
47
48    // ── Scatter with categories ──────────────────────────────────────
49    {
50        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
51        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
52        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
53        let svg = scatter(&x, &y)
54            .color_by(&cats)
55            .title("Colored Scatter")
56            .x_label("X")
57            .y_label("Y")
58            .size(500.0, 350.0)
59            .to_svg()?;
60        sections.push(("Scatter (colored)", svg));
61    }
62
63    // ── Dense scatter (opacity demo) ─────────────────────────────────
64    {
65        let n = 300;
66        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
67        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
68        let svg = scatter(&x, &y)
69            .title("Dense Scatter (auto opacity)")
70            .x_label("Feature A")
71            .y_label("Feature B")
72            .size(500.0, 350.0)
73            .to_svg()?;
74        sections.push(("Dense Scatter", svg));
75    }
76
77    // ── Line ─────────────────────────────────────────────────────────
78    {
79        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
80        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
81        let svg = line(&x, &y)
82            .title("Line Chart")
83            .x_label("Time")
84            .y_label("Value")
85            .size(500.0, 350.0)
86            .to_svg()?;
87        sections.push(("Line", svg));
88    }
89
90    // ── Multi-line (grammar API) ─────────────────────────────────────
91    {
92        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
93        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
94        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
95        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
96
97        let chart = Chart::new()
98            .layer(
99                Layer::new(MarkType::Line)
100                    .with_x(x.clone())
101                    .with_y(y1)
102                    .with_label("sin"),
103            )
104            .layer(
105                Layer::new(MarkType::Line)
106                    .with_x(x.clone())
107                    .with_y(y2)
108                    .with_label("cos"),
109            )
110            .layer(
111                Layer::new(MarkType::Line)
112                    .with_x(x)
113                    .with_y(y3)
114                    .with_label("linear"),
115            )
116            .title("Multi-Line Chart")
117            .x_label("Time")
118            .y_label("Signal")
119            .size(500.0, 350.0);
120        sections.push(("Multi-Line", chart.to_svg()?));
121    }
122
123    // ── Line + Scatter overlay (grammar API) ─────────────────────────
124    {
125        let x: Vec<f64> = (0..10).map(f64::from).collect();
126        let y_data: Vec<f64> = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
127        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
128
129        let chart = Chart::new()
130            .layer(
131                Layer::new(MarkType::Point)
132                    .with_x(x.clone())
133                    .with_y(y_data)
134                    .with_label("Data"),
135            )
136            .layer(
137                Layer::new(MarkType::Line)
138                    .with_x(x)
139                    .with_y(y_trend)
140                    .with_label("Trend"),
141            )
142            .title("Scatter + Trend Line")
143            .x_label("X")
144            .y_label("Y")
145            .size(500.0, 350.0);
146        sections.push(("Scatter + Line Overlay", chart.to_svg()?));
147    }
148
149    // ── LOESS Smooth ─────────────────────────────────────────────────
150    {
151        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
152        let y: Vec<f64> = x
153            .iter()
154            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
155            .collect();
156
157        let chart = Chart::new()
158            .layer(
159                Layer::new(MarkType::Point)
160                    .with_x(x.clone())
161                    .with_y(y.clone())
162                    .with_label("Raw"),
163            )
164            .layer(
165                Layer::new(MarkType::Line)
166                    .with_x(x)
167                    .with_y(y)
168                    .stat(Stat::Smooth { bandwidth: 0.3 })
169                    .with_label("LOESS"),
170            )
171            .title("LOESS Smoothing")
172            .x_label("X")
173            .y_label("Y")
174            .size(500.0, 350.0);
175        sections.push(("LOESS Smooth", chart.to_svg()?));
176    }
177
178    // ── Bar ──────────────────────────────────────────────────────────
179    {
180        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
181        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
182        let svg = bar(&cats, &vals)
183            .title("Language Popularity")
184            .x_label("Language")
185            .y_label("Score")
186            .size(500.0, 350.0)
187            .to_svg()?;
188        sections.push(("Bar", svg));
189    }
190
191    // ── Horizontal Bar (flipped coords) ──────────────────────────────
192    {
193        let chart = Chart::new()
194            .layer(
195                Layer::new(MarkType::Bar)
196                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
197                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
198                    .with_categories(vec![
199                        "Rust".into(),
200                        "Python".into(),
201                        "Go".into(),
202                        "Java".into(),
203                        "C++".into(),
204                    ]),
205            )
206            .coord(CoordSystem::Flipped)
207            .title("Horizontal Bars")
208            .x_label("Score")
209            .y_label("Language")
210            .size(500.0, 350.0);
211        sections.push(("Horizontal Bar", chart.to_svg()?));
212    }
213
214    // ── Histogram ────────────────────────────────────────────────────
215    {
216        let data: Vec<f64> = (0..300).map(|_| rng.normal() * 1.5 + 10.0).collect();
217        let svg = histogram(&data)
218            .bins(20)
219            .title("Histogram")
220            .x_label("Value")
221            .y_label("Count")
222            .size(500.0, 350.0)
223            .to_svg()?;
224        sections.push(("Histogram", svg));
225    }
226
227    // ── Area ─────────────────────────────────────────────────────────
228    {
229        let x: Vec<f64> = (0..30).map(f64::from).collect();
230        let y: Vec<f64> = x
231            .iter()
232            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
233            .collect();
234        let svg = area(&x, &y)
235            .title("Area Chart")
236            .x_label("Day")
237            .y_label("Traffic")
238            .size(500.0, 350.0)
239            .to_svg()?;
240        sections.push(("Area", svg));
241    }
242
243    // ── Pie ──────────────────────────────────────────────────────────
244    {
245        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
246        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
247        let svg = pie_labeled(&labels, &vals)
248            .title("Browser Share")
249            .size(400.0, 400.0)
250            .to_svg()?;
251        sections.push(("Pie", svg));
252    }
253
254    // ── Donut ────────────────────────────────────────────────────────
255    {
256        let vals = vec![60.0, 25.0, 15.0];
257        let labels = vec!["Pass", "Warn", "Fail"];
258        let svg = pie_labeled(&labels, &vals)
259            .donut(0.5)
260            .title("Test Results")
261            .size(400.0, 400.0)
262            .to_svg()?;
263        sections.push(("Donut", svg));
264    }
265
266    // ── Grouped Bar ──────────────────────────────────────────────────
267    {
268        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
269        let groups = vec![
270            "2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025",
271        ];
272        let vals = vec![12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
273        let svg = grouped_bar(&cats, &groups, &vals)
274            .title("Quarterly Revenue")
275            .x_label("Quarter")
276            .y_label("Revenue ($M)")
277            .size(500.0, 350.0)
278            .to_svg()?;
279        sections.push(("Grouped Bar", svg));
280    }
281
282    // ── Stacked Bar ──────────────────────────────────────────────────
283    {
284        let cats = vec!["Q1", "Q2", "Q3", "Q1", "Q2", "Q3"];
285        let groups = vec![
286            "Product", "Product", "Product", "Service", "Service", "Service",
287        ];
288        let vals = vec![10.0, 15.0, 20.0, 5.0, 8.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Revenue by Segment")
291            .x_label("Quarter")
292            .y_label("Revenue ($M)")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Stacked Bar", svg));
296    }
297
298    // ── Box Plot ─────────────────────────────────────────────────────
299    {
300        let mut cats = Vec::new();
301        let mut vals = Vec::new();
302        for label in &["Control", "Treatment A", "Treatment B"] {
303            let base = match *label {
304                "Control" => 50.0,
305                "Treatment A" => 65.0,
306                _ => 70.0,
307            };
308            for _ in 0..30 {
309                vals.push(base + (rng.uniform() - 0.5) * 30.0);
310                cats.push(*label);
311            }
312        }
313        let svg = boxplot(&cats, &vals)
314            .title("Treatment Comparison")
315            .x_label("Group")
316            .y_label("Response")
317            .size(500.0, 350.0)
318            .to_svg()?;
319        sections.push(("Box Plot", svg));
320    }
321
322    // ── Annotations (hline, vline, band, text) ───────────────────────
323    {
324        let x: Vec<f64> = (0..20).map(f64::from).collect();
325        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
326        let chart = scatter(&x, &y)
327            .title("Annotations Demo")
328            .x_label("X")
329            .y_label("Y")
330            .size(500.0, 350.0)
331            .build()
332            .annotate(Annotation::hline(15.0).with_label("Target"))
333            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
334            .annotate(Annotation::band(10.0, 20.0))
335            .annotate(Annotation::text(15.0, 25.0, "Peak zone"));
336        sections.push(("Annotations", chart.to_svg()?));
337    }
338
339    // ── Subtitle + Caption ───────────────────────────────────────────
340    {
341        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
342        let y = vec![10.0, 25.0, 18.0, 32.0, 28.0];
343        let chart = Chart::new()
344            .layer(Layer::new(MarkType::Line).with_x(x).with_y(y))
345            .title("Monthly Sales")
346            .subtitle("Jan–May 2026")
347            .caption("Source: internal data")
348            .x_label("Month")
349            .y_label("Revenue ($K)")
350            .size(500.0, 350.0);
351        sections.push(("Subtitle + Caption", chart.to_svg()?));
352    }
353
354    // ── Faceted Scatter (small multiples) ────────────────────────────
355    {
356        let mut x = Vec::new();
357        let mut y = Vec::new();
358        let mut facets = Vec::new();
359        for panel in &["East", "West", "North", "South"] {
360            for _ in 0..20 {
361                x.push(rng.uniform() * 10.0);
362                y.push(rng.uniform() * 10.0);
363                facets.push(*panel);
364            }
365        }
366        let svg = scatter(&x, &y)
367            .facet_wrap(&facets, 2)
368            .title("Regional Data")
369            .x_label("X")
370            .y_label("Y")
371            .size(500.0, 400.0)
372            .to_svg()?;
373        sections.push(("Faceted Scatter", svg));
374    }
375
376    // ── Heatmap ──────────────────────────────────────────────────────
377    {
378        let data = vec![
379            vec![1.0, 2.0, 3.0, 4.0, 5.0],
380            vec![5.0, 4.0, 3.0, 2.0, 1.0],
381            vec![2.0, 8.0, 6.0, 4.0, 2.0],
382            vec![3.0, 3.0, 9.0, 3.0, 3.0],
383        ];
384        let svg = heatmap(data)
385            .annotate()
386            .with_row_labels(&["A", "B", "C", "D"])
387            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
388            .title("Heatmap")
389            .x_label("Variable")
390            .y_label("Group")
391            .size(450.0, 380.0)
392            .to_svg()?;
393        sections.push(("Heatmap", svg));
394    }
395
396    // ── Confusion Matrix ─────────────────────────────────────────────
397    {
398        let data = vec![
399            vec![45.0, 3.0, 2.0],
400            vec![1.0, 40.0, 5.0],
401            vec![0.0, 4.0, 50.0],
402        ];
403        let svg = heatmap(data)
404            .annotate()
405            .with_row_labels(&["Cat", "Dog", "Bird"])
406            .with_col_labels(&["Cat", "Dog", "Bird"])
407            .title("Confusion Matrix")
408            .x_label("Predicted")
409            .y_label("Actual")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Confusion Matrix", svg));
413    }
414
415    // ── Build HTML ───────────────────────────────────────────────────
416    let mut html = String::from(
417        r#"<!DOCTYPE html>
418<html lang="en">
419<head>
420<meta charset="UTF-8">
421<meta name="viewport" content="width=device-width, initial-scale=1.0">
422<title>esoc-chart Gallery</title>
423<style>
424  * { margin: 0; padding: 0; box-sizing: border-box; }
425  body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; color: #333; }
426  header { background: #1a1a2e; color: white; padding: 2rem; text-align: center; }
427  header h1 { font-size: 2rem; font-weight: 300; }
428  header p { margin-top: 0.5rem; opacity: 0.7; }
429  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1400px; margin: 0 auto; }
430  .card { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); overflow: hidden; }
431  .card h2 { font-size: 0.9rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #666; padding: 1rem 1.5rem 0; }
432  .card svg { display: block; width: 100%; height: auto; padding: 0.5rem 1rem 1rem; }
433  .feedback { padding: 0 1rem 1rem; }
434  .feedback textarea { width: 100%; min-height: 60px; border: 1px solid #ddd; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.85rem; resize: vertical; }
435  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
436  .feedback .status { font-size: 0.75rem; color: #999; margin-top: 0.25rem; }
437  .actions { padding: 1.5rem 2rem; text-align: center; }
438  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; }
439  .actions button:hover { background: #2a2a4e; }
440</style>
441<script>
442  const feedback = {};
443  function loadFeedback() {
444    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_feedback') || '{}')); } catch {}
445    document.querySelectorAll('.feedback textarea').forEach(ta => {
446      const key = ta.dataset.chart;
447      if (feedback[key]) ta.value = feedback[key];
448    });
449  }
450  function saveFeedback(key, value) {
451    feedback[key] = value;
452    localStorage.setItem('chart_feedback', JSON.stringify(feedback));
453  }
454  function exportFeedback() {
455    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
456    const a = document.createElement('a');
457    a.href = URL.createObjectURL(blob);
458    a.download = 'chart_feedback.json';
459    a.click();
460  }
461  window.addEventListener('DOMContentLoaded', loadFeedback);
462</script>
463</head>
464<body>
465<header>
466  <h1>esoc-chart Gallery</h1>
467  <p>All charts generated with the express &amp; grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471    );
472
473    for (title, svg) in &sections {
474        let key = title.to_lowercase().replace(' ', "_");
475        write!(
476            html,
477            concat!(
478                "<div class=\"card\">\n",
479                "  <h2>{title}</h2>\n",
480                "  {svg}\n",
481                "  <div class=\"feedback\">\n",
482                "    <textarea data-chart=\"{key}\" placeholder=\"Feedback on {title}…\" ",
483                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
484                "    <div class=\"status\">Auto-saved to browser</div>\n",
485                "  </div>\n",
486                "</div>\n",
487            ),
488            title = title,
489            svg = svg,
490            key = key,
491        )
492        .unwrap();
493    }
494
495    html.push_str(concat!(
496        "</div>\n",
497        "<div class=\"actions\">\n",
498        "  <button onclick=\"exportFeedback()\">Export Feedback as JSON</button>\n",
499        "</div>\n",
500        "</body>\n</html>\n",
501    ));
502
503    std::fs::write("chart_gallery.html", &html).expect("failed to write HTML");
504    println!("Saved chart_gallery.html ({} charts)", sections.len());
505
506    Ok(())
507}