Skip to main content

HeatmapBuilder

Struct HeatmapBuilder 

Source
pub struct HeatmapBuilder { /* private fields */ }
Expand description

Builder for heatmap charts.

Implementations§

Source§

impl HeatmapBuilder

Source

pub fn title(self, title: impl Into<String>) -> Self

Set title.

Examples found in repository?
examples/audit_edge_cases.rs (line 258)
22fn main() {
23    let mut sections: Vec<(String, String)> = Vec::new();
24
25    // 1. Single data point
26    if let Some(s) = try_chart("Single Point", || {
27        scatter(&[5.0], &[10.0])
28            .title("Single Point")
29            .size(400.0, 300.0)
30            .to_svg()
31    }) {
32        sections.push(s);
33    }
34
35    // 2. Two points
36    if let Some(s) = try_chart("Two Points", || {
37        scatter(&[0.0, 100.0], &[0.0, 100.0])
38            .title("Two Points")
39            .size(400.0, 300.0)
40            .to_svg()
41    }) {
42        sections.push(s);
43    }
44
45    // 3. Very long category labels
46    if let Some(s) = try_chart("Long Labels", || {
47        let cats = vec![
48            "This is an extremely long category label",
49            "Another very very long label here",
50            "Short",
51            "Medium length label",
52            "Yet another verbose category name",
53        ];
54        bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55            .title("Long Category Labels")
56            .x_label("Categories")
57            .y_label("Value")
58            .size(600.0, 400.0)
59            .to_svg()
60    }) {
61        sections.push(s);
62    }
63
64    // 4. Large numeric values (millions)
65    if let Some(s) = try_chart("Large Numbers", || {
66        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67        let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68        scatter(&x, &y)
69            .title("Large Numbers (Millions)")
70            .x_label("Revenue ($)")
71            .y_label("Profit ($)")
72            .size(600.0, 350.0)
73            .to_svg()
74    }) {
75        sections.push(s);
76    }
77
78    // 5. Very small numbers
79    if let Some(s) = try_chart("Small Numbers", || {
80        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81        let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82        scatter(&x, &y)
83            .title("Small Numbers")
84            .size(600.0, 350.0)
85            .to_svg()
86    }) {
87        sections.push(s);
88    }
89
90    // 6. All-negative bars
91    if let Some(s) = try_chart("Negative Bars", || {
92        bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93            .title("All-Negative Bars")
94            .y_label("Loss")
95            .size(500.0, 350.0)
96            .to_svg()
97    }) {
98        sections.push(s);
99    }
100
101    // 7. Mixed +/- bars
102    if let Some(s) = try_chart("Mixed Bars", || {
103        bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104            .title("Mixed +/- Bars")
105            .y_label("P&L")
106            .size(500.0, 350.0)
107            .to_svg()
108    }) {
109        sections.push(s);
110    }
111
112    // 8. Dense scatter (1000 points)
113    if let Some(s) = try_chart("Dense Scatter", || {
114        struct Rng(u64);
115        impl Rng {
116            fn next(&mut self) -> f64 {
117                self.0 = self
118                    .0
119                    .wrapping_mul(6_364_136_223_846_793_005)
120                    .wrapping_add(1);
121                (self.0 >> 11) as f64 / (1u64 << 53) as f64
122            }
123        }
124        let mut rng = Rng(123);
125        let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126        let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127        scatter(&x, &y)
128            .title("Dense Scatter (n=1000)")
129            .size(500.0, 400.0)
130            .to_svg()
131    }) {
132        sections.push(s);
133    }
134
135    // 9. Single bar
136    if let Some(s) = try_chart("Single Bar", || {
137        bar(&["Only One"], &[42.0])
138            .title("Single Bar")
139            .size(500.0, 350.0)
140            .to_svg()
141    }) {
142        sections.push(s);
143    }
144
145    // 10. Many categories (20 bars)
146    if let Some(s) = try_chart("Many Categories", || {
147        let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148        let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149        let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150        bar(&cats_ref, &vals)
151            .title("Many Categories (20)")
152            .size(700.0, 400.0)
153            .to_svg()
154    }) {
155        sections.push(s);
156    }
157
158    // 11. Narrow chart
159    if let Some(s) = try_chart("Narrow Chart", || {
160        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162        line(&x, &y)
163            .title("Narrow Chart")
164            .x_label("X")
165            .y_label("Y")
166            .size(300.0, 400.0)
167            .to_svg()
168    }) {
169        sections.push(s);
170    }
171
172    // 12. Wide chart
173    if let Some(s) = try_chart("Wide Chart", || {
174        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176        line(&x, &y)
177            .title("Wide Chart")
178            .x_label("X")
179            .y_label("Y")
180            .size(1000.0, 250.0)
181            .to_svg()
182    }) {
183        sections.push(s);
184    }
185
186    // 13. Histogram (few bins)
187    if let Some(s) = try_chart("Few Bins", || {
188        histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189            .bins(3)
190            .title("Histogram (3 bins)")
191            .size(400.0, 300.0)
192            .to_svg()
193    }) {
194        sections.push(s);
195    }
196
197    // 14. Dark theme scatter
198    if let Some(s) = try_chart("Dark Scatter", || {
199        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202        scatter(&x, &y)
203            .color_by(&cats)
204            .title("Dark Theme Scatter")
205            .x_label("X")
206            .y_label("Y")
207            .theme(NewTheme::dark())
208            .size(500.0, 350.0)
209            .to_svg()
210    }) {
211        sections.push(s);
212    }
213
214    // 15. Dark theme bars
215    if let Some(s) = try_chart("Dark Bars", || {
216        bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217            .title("Dark Theme Bars")
218            .theme(NewTheme::dark())
219            .size(500.0, 350.0)
220            .to_svg()
221    }) {
222        sections.push(s);
223    }
224
225    // 16. Flat line (constant y)
226    if let Some(s) = try_chart("Flat Line", || {
227        let x: Vec<f64> = (0..10).map(f64::from).collect();
228        line(&x, &[5.0; 10])
229            .title("Constant Y")
230            .size(400.0, 300.0)
231            .to_svg()
232    }) {
233        sections.push(s);
234    }
235
236    // 17. Pie with many small slices
237    if let Some(s) = try_chart("Many Pie Slices", || {
238        let mut vals = vec![50.0, 30.0, 15.0];
239        vals.extend(std::iter::repeat_n(0.5, 10));
240        let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241        let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242        pie_labeled(&labels_ref, &vals)
243            .title("Pie: Many Slices")
244            .size(500.0, 400.0)
245            .to_svg()
246    }) {
247        sections.push(s);
248    }
249
250    // 18. Heatmap
251    if let Some(s) = try_chart("Heatmap", || {
252        let data = vec![
253            vec![1.0, 2.0, 3.0, 4.0],
254            vec![5.0, 6.0, 7.0, 8.0],
255            vec![9.0, 10.0, 11.0, 12.0],
256        ];
257        heatmap(data)
258            .title("Basic Heatmap")
259            .with_row_labels(&["R1", "R2", "R3"])
260            .with_col_labels(&["C1", "C2", "C3", "C4"])
261            .size(500.0, 350.0)
262            .to_svg()
263    }) {
264        sections.push(s);
265    }
266
267    // ── Write HTML ─────────────────────────────────────────────────────
268    let mut html = String::from(
269        r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274  box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279    );
280    for (title, svg) in &sections {
281        writeln!(
282            html,
283            "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284        )
285        .unwrap();
286    }
287    html.push_str("</body></html>");
288    std::fs::write("audit_edge_cases.html", &html).unwrap();
289    println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}
More examples
Hide additional examples
examples/readme_charts.rs (line 237)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
examples/gallery.rs (line 388)
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}
examples/chart_review.rs (line 460)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 909)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn x_label(self, label: impl Into<String>) -> Self

Set X-axis label.

Examples found in repository?
examples/readme_charts.rs (line 238)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
More examples
Hide additional examples
examples/gallery.rs (line 389)
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}
examples/chart_review.rs (line 461)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 910)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn y_label(self, label: impl Into<String>) -> Self

Set Y-axis label.

Examples found in repository?
examples/readme_charts.rs (line 239)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
More examples
Hide additional examples
examples/gallery.rs (line 390)
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}
examples/chart_review.rs (line 462)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 911)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn theme(self, theme: NewTheme) -> Self

Set theme.

Examples found in repository?
examples/chart_review.rs (line 499)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
More examples
Hide additional examples
examples/stress_test.rs (line 975)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn size(self, width: f32, height: f32) -> Self

Set dimensions.

Examples found in repository?
examples/audit_edge_cases.rs (line 261)
22fn main() {
23    let mut sections: Vec<(String, String)> = Vec::new();
24
25    // 1. Single data point
26    if let Some(s) = try_chart("Single Point", || {
27        scatter(&[5.0], &[10.0])
28            .title("Single Point")
29            .size(400.0, 300.0)
30            .to_svg()
31    }) {
32        sections.push(s);
33    }
34
35    // 2. Two points
36    if let Some(s) = try_chart("Two Points", || {
37        scatter(&[0.0, 100.0], &[0.0, 100.0])
38            .title("Two Points")
39            .size(400.0, 300.0)
40            .to_svg()
41    }) {
42        sections.push(s);
43    }
44
45    // 3. Very long category labels
46    if let Some(s) = try_chart("Long Labels", || {
47        let cats = vec![
48            "This is an extremely long category label",
49            "Another very very long label here",
50            "Short",
51            "Medium length label",
52            "Yet another verbose category name",
53        ];
54        bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55            .title("Long Category Labels")
56            .x_label("Categories")
57            .y_label("Value")
58            .size(600.0, 400.0)
59            .to_svg()
60    }) {
61        sections.push(s);
62    }
63
64    // 4. Large numeric values (millions)
65    if let Some(s) = try_chart("Large Numbers", || {
66        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67        let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68        scatter(&x, &y)
69            .title("Large Numbers (Millions)")
70            .x_label("Revenue ($)")
71            .y_label("Profit ($)")
72            .size(600.0, 350.0)
73            .to_svg()
74    }) {
75        sections.push(s);
76    }
77
78    // 5. Very small numbers
79    if let Some(s) = try_chart("Small Numbers", || {
80        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81        let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82        scatter(&x, &y)
83            .title("Small Numbers")
84            .size(600.0, 350.0)
85            .to_svg()
86    }) {
87        sections.push(s);
88    }
89
90    // 6. All-negative bars
91    if let Some(s) = try_chart("Negative Bars", || {
92        bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93            .title("All-Negative Bars")
94            .y_label("Loss")
95            .size(500.0, 350.0)
96            .to_svg()
97    }) {
98        sections.push(s);
99    }
100
101    // 7. Mixed +/- bars
102    if let Some(s) = try_chart("Mixed Bars", || {
103        bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104            .title("Mixed +/- Bars")
105            .y_label("P&L")
106            .size(500.0, 350.0)
107            .to_svg()
108    }) {
109        sections.push(s);
110    }
111
112    // 8. Dense scatter (1000 points)
113    if let Some(s) = try_chart("Dense Scatter", || {
114        struct Rng(u64);
115        impl Rng {
116            fn next(&mut self) -> f64 {
117                self.0 = self
118                    .0
119                    .wrapping_mul(6_364_136_223_846_793_005)
120                    .wrapping_add(1);
121                (self.0 >> 11) as f64 / (1u64 << 53) as f64
122            }
123        }
124        let mut rng = Rng(123);
125        let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126        let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127        scatter(&x, &y)
128            .title("Dense Scatter (n=1000)")
129            .size(500.0, 400.0)
130            .to_svg()
131    }) {
132        sections.push(s);
133    }
134
135    // 9. Single bar
136    if let Some(s) = try_chart("Single Bar", || {
137        bar(&["Only One"], &[42.0])
138            .title("Single Bar")
139            .size(500.0, 350.0)
140            .to_svg()
141    }) {
142        sections.push(s);
143    }
144
145    // 10. Many categories (20 bars)
146    if let Some(s) = try_chart("Many Categories", || {
147        let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148        let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149        let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150        bar(&cats_ref, &vals)
151            .title("Many Categories (20)")
152            .size(700.0, 400.0)
153            .to_svg()
154    }) {
155        sections.push(s);
156    }
157
158    // 11. Narrow chart
159    if let Some(s) = try_chart("Narrow Chart", || {
160        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162        line(&x, &y)
163            .title("Narrow Chart")
164            .x_label("X")
165            .y_label("Y")
166            .size(300.0, 400.0)
167            .to_svg()
168    }) {
169        sections.push(s);
170    }
171
172    // 12. Wide chart
173    if let Some(s) = try_chart("Wide Chart", || {
174        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176        line(&x, &y)
177            .title("Wide Chart")
178            .x_label("X")
179            .y_label("Y")
180            .size(1000.0, 250.0)
181            .to_svg()
182    }) {
183        sections.push(s);
184    }
185
186    // 13. Histogram (few bins)
187    if let Some(s) = try_chart("Few Bins", || {
188        histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189            .bins(3)
190            .title("Histogram (3 bins)")
191            .size(400.0, 300.0)
192            .to_svg()
193    }) {
194        sections.push(s);
195    }
196
197    // 14. Dark theme scatter
198    if let Some(s) = try_chart("Dark Scatter", || {
199        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202        scatter(&x, &y)
203            .color_by(&cats)
204            .title("Dark Theme Scatter")
205            .x_label("X")
206            .y_label("Y")
207            .theme(NewTheme::dark())
208            .size(500.0, 350.0)
209            .to_svg()
210    }) {
211        sections.push(s);
212    }
213
214    // 15. Dark theme bars
215    if let Some(s) = try_chart("Dark Bars", || {
216        bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217            .title("Dark Theme Bars")
218            .theme(NewTheme::dark())
219            .size(500.0, 350.0)
220            .to_svg()
221    }) {
222        sections.push(s);
223    }
224
225    // 16. Flat line (constant y)
226    if let Some(s) = try_chart("Flat Line", || {
227        let x: Vec<f64> = (0..10).map(f64::from).collect();
228        line(&x, &[5.0; 10])
229            .title("Constant Y")
230            .size(400.0, 300.0)
231            .to_svg()
232    }) {
233        sections.push(s);
234    }
235
236    // 17. Pie with many small slices
237    if let Some(s) = try_chart("Many Pie Slices", || {
238        let mut vals = vec![50.0, 30.0, 15.0];
239        vals.extend(std::iter::repeat_n(0.5, 10));
240        let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241        let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242        pie_labeled(&labels_ref, &vals)
243            .title("Pie: Many Slices")
244            .size(500.0, 400.0)
245            .to_svg()
246    }) {
247        sections.push(s);
248    }
249
250    // 18. Heatmap
251    if let Some(s) = try_chart("Heatmap", || {
252        let data = vec![
253            vec![1.0, 2.0, 3.0, 4.0],
254            vec![5.0, 6.0, 7.0, 8.0],
255            vec![9.0, 10.0, 11.0, 12.0],
256        ];
257        heatmap(data)
258            .title("Basic Heatmap")
259            .with_row_labels(&["R1", "R2", "R3"])
260            .with_col_labels(&["C1", "C2", "C3", "C4"])
261            .size(500.0, 350.0)
262            .to_svg()
263    }) {
264        sections.push(s);
265    }
266
267    // ── Write HTML ─────────────────────────────────────────────────────
268    let mut html = String::from(
269        r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274  box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279    );
280    for (title, svg) in &sections {
281        writeln!(
282            html,
283            "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284        )
285        .unwrap();
286    }
287    html.push_str("</body></html>");
288    std::fs::write("audit_edge_cases.html", &html).unwrap();
289    println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}
More examples
Hide additional examples
examples/readme_charts.rs (line 240)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
examples/gallery.rs (line 391)
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}
examples/chart_review.rs (line 463)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 912)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn to_svg(self) -> Result<String>

Build and render to SVG.

Examples found in repository?
examples/audit_edge_cases.rs (line 262)
22fn main() {
23    let mut sections: Vec<(String, String)> = Vec::new();
24
25    // 1. Single data point
26    if let Some(s) = try_chart("Single Point", || {
27        scatter(&[5.0], &[10.0])
28            .title("Single Point")
29            .size(400.0, 300.0)
30            .to_svg()
31    }) {
32        sections.push(s);
33    }
34
35    // 2. Two points
36    if let Some(s) = try_chart("Two Points", || {
37        scatter(&[0.0, 100.0], &[0.0, 100.0])
38            .title("Two Points")
39            .size(400.0, 300.0)
40            .to_svg()
41    }) {
42        sections.push(s);
43    }
44
45    // 3. Very long category labels
46    if let Some(s) = try_chart("Long Labels", || {
47        let cats = vec![
48            "This is an extremely long category label",
49            "Another very very long label here",
50            "Short",
51            "Medium length label",
52            "Yet another verbose category name",
53        ];
54        bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55            .title("Long Category Labels")
56            .x_label("Categories")
57            .y_label("Value")
58            .size(600.0, 400.0)
59            .to_svg()
60    }) {
61        sections.push(s);
62    }
63
64    // 4. Large numeric values (millions)
65    if let Some(s) = try_chart("Large Numbers", || {
66        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67        let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68        scatter(&x, &y)
69            .title("Large Numbers (Millions)")
70            .x_label("Revenue ($)")
71            .y_label("Profit ($)")
72            .size(600.0, 350.0)
73            .to_svg()
74    }) {
75        sections.push(s);
76    }
77
78    // 5. Very small numbers
79    if let Some(s) = try_chart("Small Numbers", || {
80        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81        let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82        scatter(&x, &y)
83            .title("Small Numbers")
84            .size(600.0, 350.0)
85            .to_svg()
86    }) {
87        sections.push(s);
88    }
89
90    // 6. All-negative bars
91    if let Some(s) = try_chart("Negative Bars", || {
92        bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93            .title("All-Negative Bars")
94            .y_label("Loss")
95            .size(500.0, 350.0)
96            .to_svg()
97    }) {
98        sections.push(s);
99    }
100
101    // 7. Mixed +/- bars
102    if let Some(s) = try_chart("Mixed Bars", || {
103        bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104            .title("Mixed +/- Bars")
105            .y_label("P&L")
106            .size(500.0, 350.0)
107            .to_svg()
108    }) {
109        sections.push(s);
110    }
111
112    // 8. Dense scatter (1000 points)
113    if let Some(s) = try_chart("Dense Scatter", || {
114        struct Rng(u64);
115        impl Rng {
116            fn next(&mut self) -> f64 {
117                self.0 = self
118                    .0
119                    .wrapping_mul(6_364_136_223_846_793_005)
120                    .wrapping_add(1);
121                (self.0 >> 11) as f64 / (1u64 << 53) as f64
122            }
123        }
124        let mut rng = Rng(123);
125        let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126        let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127        scatter(&x, &y)
128            .title("Dense Scatter (n=1000)")
129            .size(500.0, 400.0)
130            .to_svg()
131    }) {
132        sections.push(s);
133    }
134
135    // 9. Single bar
136    if let Some(s) = try_chart("Single Bar", || {
137        bar(&["Only One"], &[42.0])
138            .title("Single Bar")
139            .size(500.0, 350.0)
140            .to_svg()
141    }) {
142        sections.push(s);
143    }
144
145    // 10. Many categories (20 bars)
146    if let Some(s) = try_chart("Many Categories", || {
147        let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148        let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149        let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150        bar(&cats_ref, &vals)
151            .title("Many Categories (20)")
152            .size(700.0, 400.0)
153            .to_svg()
154    }) {
155        sections.push(s);
156    }
157
158    // 11. Narrow chart
159    if let Some(s) = try_chart("Narrow Chart", || {
160        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162        line(&x, &y)
163            .title("Narrow Chart")
164            .x_label("X")
165            .y_label("Y")
166            .size(300.0, 400.0)
167            .to_svg()
168    }) {
169        sections.push(s);
170    }
171
172    // 12. Wide chart
173    if let Some(s) = try_chart("Wide Chart", || {
174        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176        line(&x, &y)
177            .title("Wide Chart")
178            .x_label("X")
179            .y_label("Y")
180            .size(1000.0, 250.0)
181            .to_svg()
182    }) {
183        sections.push(s);
184    }
185
186    // 13. Histogram (few bins)
187    if let Some(s) = try_chart("Few Bins", || {
188        histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189            .bins(3)
190            .title("Histogram (3 bins)")
191            .size(400.0, 300.0)
192            .to_svg()
193    }) {
194        sections.push(s);
195    }
196
197    // 14. Dark theme scatter
198    if let Some(s) = try_chart("Dark Scatter", || {
199        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202        scatter(&x, &y)
203            .color_by(&cats)
204            .title("Dark Theme Scatter")
205            .x_label("X")
206            .y_label("Y")
207            .theme(NewTheme::dark())
208            .size(500.0, 350.0)
209            .to_svg()
210    }) {
211        sections.push(s);
212    }
213
214    // 15. Dark theme bars
215    if let Some(s) = try_chart("Dark Bars", || {
216        bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217            .title("Dark Theme Bars")
218            .theme(NewTheme::dark())
219            .size(500.0, 350.0)
220            .to_svg()
221    }) {
222        sections.push(s);
223    }
224
225    // 16. Flat line (constant y)
226    if let Some(s) = try_chart("Flat Line", || {
227        let x: Vec<f64> = (0..10).map(f64::from).collect();
228        line(&x, &[5.0; 10])
229            .title("Constant Y")
230            .size(400.0, 300.0)
231            .to_svg()
232    }) {
233        sections.push(s);
234    }
235
236    // 17. Pie with many small slices
237    if let Some(s) = try_chart("Many Pie Slices", || {
238        let mut vals = vec![50.0, 30.0, 15.0];
239        vals.extend(std::iter::repeat_n(0.5, 10));
240        let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241        let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242        pie_labeled(&labels_ref, &vals)
243            .title("Pie: Many Slices")
244            .size(500.0, 400.0)
245            .to_svg()
246    }) {
247        sections.push(s);
248    }
249
250    // 18. Heatmap
251    if let Some(s) = try_chart("Heatmap", || {
252        let data = vec![
253            vec![1.0, 2.0, 3.0, 4.0],
254            vec![5.0, 6.0, 7.0, 8.0],
255            vec![9.0, 10.0, 11.0, 12.0],
256        ];
257        heatmap(data)
258            .title("Basic Heatmap")
259            .with_row_labels(&["R1", "R2", "R3"])
260            .with_col_labels(&["C1", "C2", "C3", "C4"])
261            .size(500.0, 350.0)
262            .to_svg()
263    }) {
264        sections.push(s);
265    }
266
267    // ── Write HTML ─────────────────────────────────────────────────────
268    let mut html = String::from(
269        r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274  box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279    );
280    for (title, svg) in &sections {
281        writeln!(
282            html,
283            "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284        )
285        .unwrap();
286    }
287    html.push_str("</body></html>");
288    std::fs::write("audit_edge_cases.html", &html).unwrap();
289    println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}
More examples
Hide additional examples
examples/gallery.rs (line 392)
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}
examples/chart_review.rs (line 464)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 913)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn save_svg(self, path: impl AsRef<Path>) -> Result<()>

Build and save as SVG file.

Examples found in repository?
examples/readme_charts.rs (line 241)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
Source

pub fn x_domain(self, min: f64, max: f64) -> Self

Set explicit X-axis domain (overrides auto-computed bounds).

Source

pub fn y_domain(self, min: f64, max: f64) -> Self

Set explicit Y-axis domain (overrides auto-computed bounds).

Source

pub fn opacity(self, alpha: f32) -> Self

Set point/fill opacity (0.0–1.0).

Source

pub fn hline(self, y: f64) -> Self

Add a horizontal reference line at the given y value.

Source

pub fn vline(self, x: f64) -> Self

Add a vertical reference line at the given x value.

Source

pub fn annotate(self) -> Self

Enable cell value annotations.

Examples found in repository?
examples/readme_charts.rs (line 234)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
More examples
Hide additional examples
examples/gallery.rs (line 385)
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}
examples/chart_review.rs (line 457)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 906)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn row_labels(self, labels: Vec<String>) -> Self

👎Deprecated:

Use with_row_labels(&[impl ToString]) instead

Set row labels (owned).

Source

pub fn col_labels(self, labels: Vec<String>) -> Self

👎Deprecated:

Use with_col_labels(&[impl ToString]) instead

Set column labels (owned).

Source

pub fn with_row_labels(self, labels: &[impl ToString]) -> Self

Set row labels from any string-like slice.

Examples found in repository?
examples/audit_edge_cases.rs (line 259)
22fn main() {
23    let mut sections: Vec<(String, String)> = Vec::new();
24
25    // 1. Single data point
26    if let Some(s) = try_chart("Single Point", || {
27        scatter(&[5.0], &[10.0])
28            .title("Single Point")
29            .size(400.0, 300.0)
30            .to_svg()
31    }) {
32        sections.push(s);
33    }
34
35    // 2. Two points
36    if let Some(s) = try_chart("Two Points", || {
37        scatter(&[0.0, 100.0], &[0.0, 100.0])
38            .title("Two Points")
39            .size(400.0, 300.0)
40            .to_svg()
41    }) {
42        sections.push(s);
43    }
44
45    // 3. Very long category labels
46    if let Some(s) = try_chart("Long Labels", || {
47        let cats = vec![
48            "This is an extremely long category label",
49            "Another very very long label here",
50            "Short",
51            "Medium length label",
52            "Yet another verbose category name",
53        ];
54        bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55            .title("Long Category Labels")
56            .x_label("Categories")
57            .y_label("Value")
58            .size(600.0, 400.0)
59            .to_svg()
60    }) {
61        sections.push(s);
62    }
63
64    // 4. Large numeric values (millions)
65    if let Some(s) = try_chart("Large Numbers", || {
66        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67        let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68        scatter(&x, &y)
69            .title("Large Numbers (Millions)")
70            .x_label("Revenue ($)")
71            .y_label("Profit ($)")
72            .size(600.0, 350.0)
73            .to_svg()
74    }) {
75        sections.push(s);
76    }
77
78    // 5. Very small numbers
79    if let Some(s) = try_chart("Small Numbers", || {
80        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81        let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82        scatter(&x, &y)
83            .title("Small Numbers")
84            .size(600.0, 350.0)
85            .to_svg()
86    }) {
87        sections.push(s);
88    }
89
90    // 6. All-negative bars
91    if let Some(s) = try_chart("Negative Bars", || {
92        bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93            .title("All-Negative Bars")
94            .y_label("Loss")
95            .size(500.0, 350.0)
96            .to_svg()
97    }) {
98        sections.push(s);
99    }
100
101    // 7. Mixed +/- bars
102    if let Some(s) = try_chart("Mixed Bars", || {
103        bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104            .title("Mixed +/- Bars")
105            .y_label("P&L")
106            .size(500.0, 350.0)
107            .to_svg()
108    }) {
109        sections.push(s);
110    }
111
112    // 8. Dense scatter (1000 points)
113    if let Some(s) = try_chart("Dense Scatter", || {
114        struct Rng(u64);
115        impl Rng {
116            fn next(&mut self) -> f64 {
117                self.0 = self
118                    .0
119                    .wrapping_mul(6_364_136_223_846_793_005)
120                    .wrapping_add(1);
121                (self.0 >> 11) as f64 / (1u64 << 53) as f64
122            }
123        }
124        let mut rng = Rng(123);
125        let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126        let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127        scatter(&x, &y)
128            .title("Dense Scatter (n=1000)")
129            .size(500.0, 400.0)
130            .to_svg()
131    }) {
132        sections.push(s);
133    }
134
135    // 9. Single bar
136    if let Some(s) = try_chart("Single Bar", || {
137        bar(&["Only One"], &[42.0])
138            .title("Single Bar")
139            .size(500.0, 350.0)
140            .to_svg()
141    }) {
142        sections.push(s);
143    }
144
145    // 10. Many categories (20 bars)
146    if let Some(s) = try_chart("Many Categories", || {
147        let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148        let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149        let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150        bar(&cats_ref, &vals)
151            .title("Many Categories (20)")
152            .size(700.0, 400.0)
153            .to_svg()
154    }) {
155        sections.push(s);
156    }
157
158    // 11. Narrow chart
159    if let Some(s) = try_chart("Narrow Chart", || {
160        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162        line(&x, &y)
163            .title("Narrow Chart")
164            .x_label("X")
165            .y_label("Y")
166            .size(300.0, 400.0)
167            .to_svg()
168    }) {
169        sections.push(s);
170    }
171
172    // 12. Wide chart
173    if let Some(s) = try_chart("Wide Chart", || {
174        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176        line(&x, &y)
177            .title("Wide Chart")
178            .x_label("X")
179            .y_label("Y")
180            .size(1000.0, 250.0)
181            .to_svg()
182    }) {
183        sections.push(s);
184    }
185
186    // 13. Histogram (few bins)
187    if let Some(s) = try_chart("Few Bins", || {
188        histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189            .bins(3)
190            .title("Histogram (3 bins)")
191            .size(400.0, 300.0)
192            .to_svg()
193    }) {
194        sections.push(s);
195    }
196
197    // 14. Dark theme scatter
198    if let Some(s) = try_chart("Dark Scatter", || {
199        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202        scatter(&x, &y)
203            .color_by(&cats)
204            .title("Dark Theme Scatter")
205            .x_label("X")
206            .y_label("Y")
207            .theme(NewTheme::dark())
208            .size(500.0, 350.0)
209            .to_svg()
210    }) {
211        sections.push(s);
212    }
213
214    // 15. Dark theme bars
215    if let Some(s) = try_chart("Dark Bars", || {
216        bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217            .title("Dark Theme Bars")
218            .theme(NewTheme::dark())
219            .size(500.0, 350.0)
220            .to_svg()
221    }) {
222        sections.push(s);
223    }
224
225    // 16. Flat line (constant y)
226    if let Some(s) = try_chart("Flat Line", || {
227        let x: Vec<f64> = (0..10).map(f64::from).collect();
228        line(&x, &[5.0; 10])
229            .title("Constant Y")
230            .size(400.0, 300.0)
231            .to_svg()
232    }) {
233        sections.push(s);
234    }
235
236    // 17. Pie with many small slices
237    if let Some(s) = try_chart("Many Pie Slices", || {
238        let mut vals = vec![50.0, 30.0, 15.0];
239        vals.extend(std::iter::repeat_n(0.5, 10));
240        let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241        let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242        pie_labeled(&labels_ref, &vals)
243            .title("Pie: Many Slices")
244            .size(500.0, 400.0)
245            .to_svg()
246    }) {
247        sections.push(s);
248    }
249
250    // 18. Heatmap
251    if let Some(s) = try_chart("Heatmap", || {
252        let data = vec![
253            vec![1.0, 2.0, 3.0, 4.0],
254            vec![5.0, 6.0, 7.0, 8.0],
255            vec![9.0, 10.0, 11.0, 12.0],
256        ];
257        heatmap(data)
258            .title("Basic Heatmap")
259            .with_row_labels(&["R1", "R2", "R3"])
260            .with_col_labels(&["C1", "C2", "C3", "C4"])
261            .size(500.0, 350.0)
262            .to_svg()
263    }) {
264        sections.push(s);
265    }
266
267    // ── Write HTML ─────────────────────────────────────────────────────
268    let mut html = String::from(
269        r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274  box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279    );
280    for (title, svg) in &sections {
281        writeln!(
282            html,
283            "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284        )
285        .unwrap();
286    }
287    html.push_str("</body></html>");
288    std::fs::write("audit_edge_cases.html", &html).unwrap();
289    println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}
More examples
Hide additional examples
examples/readme_charts.rs (line 235)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
examples/gallery.rs (line 386)
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}
examples/chart_review.rs (line 458)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 907)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn with_col_labels(self, labels: &[impl ToString]) -> Self

Set column labels from any string-like slice.

Examples found in repository?
examples/audit_edge_cases.rs (line 260)
22fn main() {
23    let mut sections: Vec<(String, String)> = Vec::new();
24
25    // 1. Single data point
26    if let Some(s) = try_chart("Single Point", || {
27        scatter(&[5.0], &[10.0])
28            .title("Single Point")
29            .size(400.0, 300.0)
30            .to_svg()
31    }) {
32        sections.push(s);
33    }
34
35    // 2. Two points
36    if let Some(s) = try_chart("Two Points", || {
37        scatter(&[0.0, 100.0], &[0.0, 100.0])
38            .title("Two Points")
39            .size(400.0, 300.0)
40            .to_svg()
41    }) {
42        sections.push(s);
43    }
44
45    // 3. Very long category labels
46    if let Some(s) = try_chart("Long Labels", || {
47        let cats = vec![
48            "This is an extremely long category label",
49            "Another very very long label here",
50            "Short",
51            "Medium length label",
52            "Yet another verbose category name",
53        ];
54        bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55            .title("Long Category Labels")
56            .x_label("Categories")
57            .y_label("Value")
58            .size(600.0, 400.0)
59            .to_svg()
60    }) {
61        sections.push(s);
62    }
63
64    // 4. Large numeric values (millions)
65    if let Some(s) = try_chart("Large Numbers", || {
66        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67        let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68        scatter(&x, &y)
69            .title("Large Numbers (Millions)")
70            .x_label("Revenue ($)")
71            .y_label("Profit ($)")
72            .size(600.0, 350.0)
73            .to_svg()
74    }) {
75        sections.push(s);
76    }
77
78    // 5. Very small numbers
79    if let Some(s) = try_chart("Small Numbers", || {
80        let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81        let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82        scatter(&x, &y)
83            .title("Small Numbers")
84            .size(600.0, 350.0)
85            .to_svg()
86    }) {
87        sections.push(s);
88    }
89
90    // 6. All-negative bars
91    if let Some(s) = try_chart("Negative Bars", || {
92        bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93            .title("All-Negative Bars")
94            .y_label("Loss")
95            .size(500.0, 350.0)
96            .to_svg()
97    }) {
98        sections.push(s);
99    }
100
101    // 7. Mixed +/- bars
102    if let Some(s) = try_chart("Mixed Bars", || {
103        bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104            .title("Mixed +/- Bars")
105            .y_label("P&L")
106            .size(500.0, 350.0)
107            .to_svg()
108    }) {
109        sections.push(s);
110    }
111
112    // 8. Dense scatter (1000 points)
113    if let Some(s) = try_chart("Dense Scatter", || {
114        struct Rng(u64);
115        impl Rng {
116            fn next(&mut self) -> f64 {
117                self.0 = self
118                    .0
119                    .wrapping_mul(6_364_136_223_846_793_005)
120                    .wrapping_add(1);
121                (self.0 >> 11) as f64 / (1u64 << 53) as f64
122            }
123        }
124        let mut rng = Rng(123);
125        let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126        let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127        scatter(&x, &y)
128            .title("Dense Scatter (n=1000)")
129            .size(500.0, 400.0)
130            .to_svg()
131    }) {
132        sections.push(s);
133    }
134
135    // 9. Single bar
136    if let Some(s) = try_chart("Single Bar", || {
137        bar(&["Only One"], &[42.0])
138            .title("Single Bar")
139            .size(500.0, 350.0)
140            .to_svg()
141    }) {
142        sections.push(s);
143    }
144
145    // 10. Many categories (20 bars)
146    if let Some(s) = try_chart("Many Categories", || {
147        let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148        let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149        let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150        bar(&cats_ref, &vals)
151            .title("Many Categories (20)")
152            .size(700.0, 400.0)
153            .to_svg()
154    }) {
155        sections.push(s);
156    }
157
158    // 11. Narrow chart
159    if let Some(s) = try_chart("Narrow Chart", || {
160        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162        line(&x, &y)
163            .title("Narrow Chart")
164            .x_label("X")
165            .y_label("Y")
166            .size(300.0, 400.0)
167            .to_svg()
168    }) {
169        sections.push(s);
170    }
171
172    // 12. Wide chart
173    if let Some(s) = try_chart("Wide Chart", || {
174        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175        let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176        line(&x, &y)
177            .title("Wide Chart")
178            .x_label("X")
179            .y_label("Y")
180            .size(1000.0, 250.0)
181            .to_svg()
182    }) {
183        sections.push(s);
184    }
185
186    // 13. Histogram (few bins)
187    if let Some(s) = try_chart("Few Bins", || {
188        histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189            .bins(3)
190            .title("Histogram (3 bins)")
191            .size(400.0, 300.0)
192            .to_svg()
193    }) {
194        sections.push(s);
195    }
196
197    // 14. Dark theme scatter
198    if let Some(s) = try_chart("Dark Scatter", || {
199        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201        let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202        scatter(&x, &y)
203            .color_by(&cats)
204            .title("Dark Theme Scatter")
205            .x_label("X")
206            .y_label("Y")
207            .theme(NewTheme::dark())
208            .size(500.0, 350.0)
209            .to_svg()
210    }) {
211        sections.push(s);
212    }
213
214    // 15. Dark theme bars
215    if let Some(s) = try_chart("Dark Bars", || {
216        bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217            .title("Dark Theme Bars")
218            .theme(NewTheme::dark())
219            .size(500.0, 350.0)
220            .to_svg()
221    }) {
222        sections.push(s);
223    }
224
225    // 16. Flat line (constant y)
226    if let Some(s) = try_chart("Flat Line", || {
227        let x: Vec<f64> = (0..10).map(f64::from).collect();
228        line(&x, &[5.0; 10])
229            .title("Constant Y")
230            .size(400.0, 300.0)
231            .to_svg()
232    }) {
233        sections.push(s);
234    }
235
236    // 17. Pie with many small slices
237    if let Some(s) = try_chart("Many Pie Slices", || {
238        let mut vals = vec![50.0, 30.0, 15.0];
239        vals.extend(std::iter::repeat_n(0.5, 10));
240        let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241        let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242        pie_labeled(&labels_ref, &vals)
243            .title("Pie: Many Slices")
244            .size(500.0, 400.0)
245            .to_svg()
246    }) {
247        sections.push(s);
248    }
249
250    // 18. Heatmap
251    if let Some(s) = try_chart("Heatmap", || {
252        let data = vec![
253            vec![1.0, 2.0, 3.0, 4.0],
254            vec![5.0, 6.0, 7.0, 8.0],
255            vec![9.0, 10.0, 11.0, 12.0],
256        ];
257        heatmap(data)
258            .title("Basic Heatmap")
259            .with_row_labels(&["R1", "R2", "R3"])
260            .with_col_labels(&["C1", "C2", "C3", "C4"])
261            .size(500.0, 350.0)
262            .to_svg()
263    }) {
264        sections.push(s);
265    }
266
267    // ── Write HTML ─────────────────────────────────────────────────────
268    let mut html = String::from(
269        r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274  box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279    );
280    for (title, svg) in &sections {
281        writeln!(
282            html,
283            "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284        )
285        .unwrap();
286    }
287    html.push_str("</body></html>");
288    std::fs::write("audit_edge_cases.html", &html).unwrap();
289    println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}
More examples
Hide additional examples
examples/readme_charts.rs (line 236)
15fn main() -> esoc_chart::error::Result<()> {
16    // Simple deterministic RNG
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 rng = Rng(42);
33
34    let dir = "crates/esoc-chart/images";
35    std::fs::create_dir_all(dir).unwrap();
36
37    // ── 1. Scatter ──────────────────────────────────────────────────
38    {
39        let n = 80;
40        let x: Vec<f64> = (0..n).map(|_| rng.uniform() * 10.0).collect();
41        let y: Vec<f64> = x
42            .iter()
43            .map(|&xi| 0.4 * xi * xi - 2.0 * xi + 3.0 + rng.normal() * 2.0)
44            .collect();
45        scatter(&x, &y)
46            .title("Quadratic Trend")
47            .x_label("x")
48            .y_label("y")
49            .size(560.0, 380.0)
50            .save_svg(format!("{dir}/scatter.svg"))?;
51    }
52
53    // ── 2. Scatter with categories ──────────────────────────────────
54    {
55        let mut x = Vec::new();
56        let mut y = Vec::new();
57        let mut cats = Vec::new();
58        for (label, cx, cy) in [("Setosa", 5.0, 3.4), ("Versicolor", 5.9, 2.8), ("Virginica", 6.6, 3.0)] {
59            for _ in 0..40 {
60                x.push(cx + rng.normal() * 0.4);
61                y.push(cy + rng.normal() * 0.3);
62                cats.push(label);
63            }
64        }
65        scatter(&x, &y)
66            .color_by(&cats)
67            .title("Iris Clusters")
68            .x_label("Sepal Length")
69            .y_label("Sepal Width")
70            .size(560.0, 380.0)
71            .save_svg(format!("{dir}/scatter_categories.svg"))?;
72    }
73
74    // ── 3. Line chart ───────────────────────────────────────────────
75    {
76        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
77        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v * 0.5).collect();
78        line(&x, &y)
79            .title("Signal + Trend")
80            .x_label("Time (s)")
81            .y_label("Amplitude")
82            .size(560.0, 380.0)
83            .save_svg(format!("{dir}/line.svg"))?;
84    }
85
86    // ── 4. Multi-line (grammar API) ─────────────────────────────────
87    {
88        let epochs: Vec<f64> = (1..=30).map(f64::from).collect();
89        let train_loss: Vec<f64> = epochs.iter().map(|&e| 2.5 * (-e / 8.0).exp() + 0.1).collect();
90        let val_loss: Vec<f64> = epochs
91            .iter()
92            .map(|&e| 2.5 * (-e / 10.0).exp() + 0.25 + rng.normal() * 0.05)
93            .collect();
94
95        let chart = Chart::new()
96            .layer(
97                Layer::new(MarkType::Line)
98                    .with_x(epochs.clone())
99                    .with_y(train_loss)
100                    .with_label("Train"),
101            )
102            .layer(
103                Layer::new(MarkType::Line)
104                    .with_x(epochs)
105                    .with_y(val_loss)
106                    .with_label("Validation"),
107            )
108            .title("Training Curves")
109            .x_label("Epoch")
110            .y_label("Loss")
111            .size(560.0, 380.0);
112        chart.save_svg_to(format!("{dir}/multi_line.svg"))?;
113    }
114
115    // ── 5. Bar chart ────────────────────────────────────────────────
116    {
117        let cats = ["Rust", "Python", "Go", "TypeScript", "Java"];
118        let vals = [92.0, 87.0, 79.0, 73.0, 68.0];
119        bar(&cats, &vals)
120            .title("Developer Satisfaction")
121            .x_label("Language")
122            .y_label("Score (%)")
123            .size(560.0, 380.0)
124            .save_svg(format!("{dir}/bar.svg"))?;
125    }
126
127    // ── 6. Grouped bar ──────────────────────────────────────────────
128    {
129        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
130        let groups = ["2024", "2024", "2024", "2024", "2025", "2025", "2025", "2025"];
131        let vals = [12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0];
132        grouped_bar(&cats, &groups, &vals)
133            .title("Quarterly Revenue")
134            .x_label("Quarter")
135            .y_label("Revenue ($M)")
136            .size(560.0, 380.0)
137            .save_svg(format!("{dir}/grouped_bar.svg"))?;
138    }
139
140    // ── 7. Stacked bar ──────────────────────────────────────────────
141    {
142        let cats = ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
143        let groups = [
144            "Product", "Product", "Product", "Product",
145            "Service", "Service", "Service", "Service",
146        ];
147        let vals = [10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
148        stacked_bar(&cats, &groups, &vals)
149            .title("Revenue by Segment")
150            .x_label("Quarter")
151            .y_label("Revenue ($M)")
152            .size(560.0, 380.0)
153            .save_svg(format!("{dir}/stacked_bar.svg"))?;
154    }
155
156    // ── 8. Histogram ────────────────────────────────────────────────
157    {
158        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 1.5 + 10.0).collect();
159        histogram(&data)
160            .bins(25)
161            .title("Feature Distribution")
162            .x_label("Value")
163            .y_label("Count")
164            .size(560.0, 380.0)
165            .save_svg(format!("{dir}/histogram.svg"))?;
166    }
167
168    // ── 9. Area chart ───────────────────────────────────────────────
169    {
170        let x: Vec<f64> = (0..40).map(f64::from).collect();
171        let y: Vec<f64> = x
172            .iter()
173            .map(|&v| (v * 0.2).sin().abs() * 25.0 + 8.0 + rng.normal() * 1.5)
174            .collect();
175        area(&x, &y)
176            .title("Daily Active Users")
177            .x_label("Day")
178            .y_label("Users (k)")
179            .size(560.0, 380.0)
180            .save_svg(format!("{dir}/area.svg"))?;
181    }
182
183    // ── 10. Pie chart ───────────────────────────────────────────────
184    {
185        let labels = ["Chrome", "Firefox", "Safari", "Edge", "Other"];
186        let vals = [64.0, 12.0, 10.0, 8.0, 6.0];
187        pie_labeled(&labels, &vals)
188            .title("Browser Market Share")
189            .size(420.0, 420.0)
190            .save_svg(format!("{dir}/pie.svg"))?;
191    }
192
193    // ── 11. Donut chart ─────────────────────────────────────────────
194    {
195        let labels = ["Pass", "Warn", "Fail"];
196        let vals = [72.0, 18.0, 10.0];
197        pie_labeled(&labels, &vals)
198            .donut(0.5)
199            .title("Test Suite Results")
200            .size(420.0, 420.0)
201            .save_svg(format!("{dir}/donut.svg"))?;
202    }
203
204    // ── 12. Box plot ────────────────────────────────────────────────
205    {
206        let mut cats = Vec::new();
207        let mut vals = Vec::new();
208        for (label, center, spread) in [
209            ("Control", 50.0, 12.0),
210            ("Treatment A", 62.0, 10.0),
211            ("Treatment B", 71.0, 8.0),
212        ] {
213            for _ in 0..40 {
214                vals.push(center + rng.normal() * spread);
215                cats.push(label);
216            }
217        }
218        boxplot(&cats, &vals)
219            .title("Treatment Comparison")
220            .x_label("Group")
221            .y_label("Response")
222            .size(560.0, 380.0)
223            .save_svg(format!("{dir}/boxplot.svg"))?;
224    }
225
226    // ── 13. Heatmap ─────────────────────────────────────────────────
227    {
228        let data = vec![
229            vec![0.92, 0.05, 0.03],
230            vec![0.04, 0.88, 0.08],
231            vec![0.02, 0.06, 0.92],
232        ];
233        heatmap(data)
234            .annotate()
235            .with_row_labels(&["Cat", "Dog", "Bird"])
236            .with_col_labels(&["Cat", "Dog", "Bird"])
237            .title("Confusion Matrix")
238            .x_label("Predicted")
239            .y_label("Actual")
240            .size(420.0, 420.0)
241            .save_svg(format!("{dir}/heatmap.svg"))?;
242    }
243
244    // ── 14. Treemap ─────────────────────────────────────────────────
245    {
246        let labels = ["AWS", "Azure", "GCP", "Alibaba", "Oracle", "IBM"];
247        let vals = [32.0, 23.0, 11.0, 5.0, 3.0, 2.0];
248        treemap(&labels, &vals)
249            .title("Cloud Market Share (%)")
250            .size(560.0, 380.0)
251            .save_svg(format!("{dir}/treemap.svg"))?;
252    }
253
254    // ── 15. LOESS smooth ────────────────────────────────────────────
255    {
256        let x: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.15).collect();
257        let y: Vec<f64> = x
258            .iter()
259            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
260            .collect();
261        let chart = Chart::new()
262            .layer(
263                Layer::new(MarkType::Point)
264                    .with_x(x.clone())
265                    .with_y(y.clone())
266                    .with_label("Raw"),
267            )
268            .layer(
269                Layer::new(MarkType::Line)
270                    .with_x(x)
271                    .with_y(y)
272                    .stat(Stat::Smooth { bandwidth: 0.3 })
273                    .with_label("LOESS"),
274            )
275            .title("LOESS Smoothing")
276            .x_label("x")
277            .y_label("y")
278            .size(560.0, 380.0);
279        chart.save_svg_to(format!("{dir}/loess.svg"))?;
280    }
281
282    // ── 16. Annotations ─────────────────────────────────────────────
283    {
284        let x: Vec<f64> = (0..30).map(f64::from).collect();
285        let y: Vec<f64> = x
286            .iter()
287            .map(|&v| v * 1.2 + rng.normal() * 3.0 + 5.0)
288            .collect();
289        let chart = scatter(&x, &y)
290            .title("Annotated Scatter")
291            .x_label("Day")
292            .y_label("Metric")
293            .size(560.0, 380.0)
294            .build()
295            .annotate(Annotation::hline(25.0).with_label("Target"))
296            .annotate(Annotation::band(15.0, 25.0));
297        chart.save_svg_to(format!("{dir}/annotations.svg"))?;
298    }
299
300    // ── 17. Dark theme ──────────────────────────────────────────────
301    {
302        let epochs: Vec<f64> = (1..=25).map(f64::from).collect();
303        let loss: Vec<f64> = epochs.iter().map(|&e| 3.0 * (-e / 6.0).exp() + 0.15).collect();
304        let acc: Vec<f64> = epochs
305            .iter()
306            .map(|&e| 0.95 * (1.0 - (-e / 5.0).exp()))
307            .collect();
308
309        let chart = Chart::new()
310            .layer(
311                Layer::new(MarkType::Line)
312                    .with_x(epochs.clone())
313                    .with_y(loss)
314                    .with_label("Loss"),
315            )
316            .layer(
317                Layer::new(MarkType::Line)
318                    .with_x(epochs)
319                    .with_y(acc)
320                    .with_label("Accuracy"),
321            )
322            .title("Model Training")
323            .x_label("Epoch")
324            .y_label("Value")
325            .theme(NewTheme::dark())
326            .size(560.0, 380.0);
327        chart.save_svg_to(format!("{dir}/dark_theme.svg"))?;
328    }
329
330    // ── 18. Horizontal bar ──────────────────────────────────────────
331    {
332        let chart = Chart::new()
333            .layer(
334                Layer::new(MarkType::Bar)
335                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
336                    .with_y(vec![92.0, 87.0, 79.0, 73.0, 68.0])
337                    .with_categories(vec![
338                        "Rust".into(),
339                        "Python".into(),
340                        "Go".into(),
341                        "TypeScript".into(),
342                        "Java".into(),
343                    ]),
344            )
345            .coord(CoordSystem::Flipped)
346            .title("Satisfaction Scores")
347            .x_label("Score (%)")
348            .y_label("Language")
349            .size(560.0, 380.0);
350        chart.save_svg_to(format!("{dir}/horizontal_bar.svg"))?;
351    }
352
353    println!("Generated 18 SVGs in {dir}/");
354    Ok(())
355}
examples/gallery.rs (line 387)
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}
examples/chart_review.rs (line 459)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple RNG for reproducible data ─────────────────────────────
20    struct Rng(u64);
21    impl Rng {
22        fn uniform(&mut self) -> f64 {
23            self.0 = self
24                .0
25                .wrapping_mul(6_364_136_223_846_793_005)
26                .wrapping_add(1);
27            (self.0 >> 11) as f64 / (1u64 << 53) as f64
28        }
29        fn normal(&mut self) -> f64 {
30            let u1 = self.uniform().max(1e-15);
31            let u2 = self.uniform();
32            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
33        }
34    }
35    let mut sections: Vec<(&str, String)> = Vec::new();
36    let mut rng = Rng(42);
37
38    // ═══════════════════════════════════════════════════════════════════
39    // SCATTER PLOTS
40    // ═══════════════════════════════════════════════════════════════════
41
42    // Basic scatter
43    {
44        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
45        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
46        let svg = scatter(&x, &y)
47            .title("Basic Scatter")
48            .x_label("X")
49            .y_label("Y")
50            .size(500.0, 350.0)
51            .to_svg()?;
52        sections.push(("Scatter – Basic", svg));
53    }
54
55    // Scatter with categories + legend
56    {
57        let x = vec![
58            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
59        ];
60        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1, 3.5, 5.2, 6.8, 7.9];
61        let cats = vec!["A", "B", "C", "A", "B", "C", "A", "B", "C", "A", "B", "C"];
62        let svg = scatter(&x, &y)
63            .color_by(&cats)
64            .title("Scatter – 3 Categories")
65            .x_label("Feature 1")
66            .y_label("Feature 2")
67            .size(500.0, 350.0)
68            .to_svg()?;
69        sections.push(("Scatter – Categories", svg));
70    }
71
72    // Dense scatter (auto opacity)
73    {
74        let n = 400;
75        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 3.0 + 5.0).collect();
76        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 2.0).collect();
77        let svg = scatter(&x, &y)
78            .title("Dense Scatter (n=400, auto-opacity)")
79            .x_label("Feature A")
80            .y_label("Feature B")
81            .size(500.0, 350.0)
82            .to_svg()?;
83        sections.push(("Scatter – Dense", svg));
84    }
85
86    // Single point scatter (edge case)
87    {
88        let svg = scatter(&[5.0], &[10.0])
89            .title("Single Point")
90            .size(400.0, 300.0)
91            .to_svg()?;
92        sections.push(("Scatter – Single Point", svg));
93    }
94
95    // Scatter with description (accessibility)
96    {
97        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
98        let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
99        let chart = Chart::new()
100            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
101            .title("Accessible Chart")
102            .description("A scatter plot showing 5 data points with an upward trend")
103            .size(500.0, 350.0);
104        let svg = chart.to_svg()?;
105        // Verify SVG has role="img", <title>, <desc>
106        assert!(svg.contains(r#"role="img""#));
107        assert!(svg.contains("<title>"));
108        assert!(svg.contains("<desc>"));
109        sections.push(("Scatter – Accessibility", svg));
110    }
111
112    // ═══════════════════════════════════════════════════════════════════
113    // LINE CHARTS
114    // ═══════════════════════════════════════════════════════════════════
115
116    // Basic line
117    {
118        let x: Vec<f64> = (0..20).map(|i| f64::from(i) * 0.5).collect();
119        let y: Vec<f64> = x.iter().map(|&v| (v * 0.8).sin() * 3.0 + v).collect();
120        let svg = line(&x, &y)
121            .title("Line Chart")
122            .x_label("Time")
123            .y_label("Value")
124            .size(500.0, 350.0)
125            .to_svg()?;
126        sections.push(("Line – Basic", svg));
127    }
128
129    // Multi-line with legend
130    {
131        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
132        let y1: Vec<f64> = x.iter().map(|&v| (v * 0.4).sin() * 5.0 + 10.0).collect();
133        let y2: Vec<f64> = x.iter().map(|&v| (v * 0.4).cos() * 4.0 + 12.0).collect();
134        let y3: Vec<f64> = x.iter().map(|&v| v * 0.5 + 5.0).collect();
135
136        let chart = Chart::new()
137            .layer(
138                Layer::new(MarkType::Line)
139                    .with_x(x.clone())
140                    .with_y(y1)
141                    .with_label("sin"),
142            )
143            .layer(
144                Layer::new(MarkType::Line)
145                    .with_x(x.clone())
146                    .with_y(y2)
147                    .with_label("cos"),
148            )
149            .layer(
150                Layer::new(MarkType::Line)
151                    .with_x(x)
152                    .with_y(y3)
153                    .with_label("linear"),
154            )
155            .title("Multi-Line with Legend")
156            .x_label("Time")
157            .y_label("Signal")
158            .size(500.0, 350.0);
159        sections.push(("Line – Multi-series", chart.to_svg()?));
160    }
161
162    // LOESS smooth overlay
163    {
164        let x: Vec<f64> = (0..40).map(|i| f64::from(i) * 0.25).collect();
165        let y: Vec<f64> = x
166            .iter()
167            .map(|&v| (v * 0.5).sin() * 3.0 + rng.normal() * 0.8)
168            .collect();
169        let chart = Chart::new()
170            .layer(
171                Layer::new(MarkType::Point)
172                    .with_x(x.clone())
173                    .with_y(y.clone())
174                    .with_label("Raw"),
175            )
176            .layer(
177                Layer::new(MarkType::Line)
178                    .with_x(x)
179                    .with_y(y)
180                    .stat(Stat::Smooth { bandwidth: 0.3 })
181                    .with_label("LOESS"),
182            )
183            .title("LOESS Smoothing")
184            .x_label("X")
185            .y_label("Y")
186            .size(500.0, 350.0);
187        sections.push(("Line – LOESS Overlay", chart.to_svg()?));
188    }
189
190    // ═══════════════════════════════════════════════════════════════════
191    // BAR CHARTS
192    // ═══════════════════════════════════════════════════════════════════
193
194    // Basic bar (no legend expected)
195    {
196        let cats = vec!["Rust", "Python", "Go", "Java", "C++"];
197        let vals = vec![42.0, 35.0, 28.0, 22.0, 18.0];
198        let svg = bar(&cats, &vals)
199            .title("Bar Chart (no legend)")
200            .x_label("Language")
201            .y_label("Score")
202            .size(500.0, 350.0)
203            .to_svg()?;
204        sections.push(("Bar – Basic", svg));
205    }
206
207    // Bar with many categories (label rotation)
208    {
209        let cats: Vec<String> = (0..15).map(|i| format!("Category {}", i + 1)).collect();
210        let vals: Vec<f64> = (0..15)
211            .map(|i| (f64::from(i) * 3.7 + 5.0) % 30.0 + 5.0)
212            .collect();
213        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
214        let svg = bar(&cat_refs, &vals)
215            .title("Bar – Label Rotation")
216            .x_label("Category")
217            .y_label("Value")
218            .size(600.0, 350.0)
219            .to_svg()?;
220        sections.push(("Bar – Rotated Labels", svg));
221    }
222
223    // Horizontal bar (flipped)
224    {
225        let chart = Chart::new()
226            .layer(
227                Layer::new(MarkType::Bar)
228                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0])
229                    .with_y(vec![42.0, 35.0, 28.0, 22.0, 18.0])
230                    .with_categories(vec![
231                        "Rust".into(),
232                        "Python".into(),
233                        "Go".into(),
234                        "Java".into(),
235                        "C++".into(),
236                    ]),
237            )
238            .coord(CoordSystem::Flipped)
239            .title("Horizontal Bars")
240            .x_label("Score")
241            .y_label("Language")
242            .size(500.0, 350.0);
243        sections.push(("Bar – Horizontal", chart.to_svg()?));
244    }
245
246    // Grouped bar
247    {
248        let cats = vec![
249            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
250        ];
251        let groups = vec![
252            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
253            "2025",
254        ];
255        let vals = vec![
256            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
257        ];
258        let svg = grouped_bar(&cats, &groups, &vals)
259            .title("Grouped Bar – 3 Series")
260            .x_label("Quarter")
261            .y_label("Revenue ($M)")
262            .size(550.0, 350.0)
263            .to_svg()?;
264        sections.push(("Bar – Grouped", svg));
265    }
266
267    // Stacked bar
268    {
269        let cats = vec!["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"];
270        let groups = vec![
271            "Product", "Product", "Product", "Product", "Service", "Service", "Service", "Service",
272        ];
273        let vals = vec![10.0, 15.0, 20.0, 18.0, 5.0, 8.0, 12.0, 10.0];
274        let svg = stacked_bar(&cats, &groups, &vals)
275            .title("Stacked Bar")
276            .x_label("Quarter")
277            .y_label("Revenue ($M)")
278            .size(500.0, 350.0)
279            .to_svg()?;
280        sections.push(("Bar – Stacked", svg));
281    }
282
283    // Stacked bar with sparse groups (tests key-based stacking fix)
284    {
285        // Group A only has Q1,Q2; Group B has Q2,Q3,Q4 — sparse overlap
286        let cats = vec!["Q1", "Q2", "Q2", "Q3", "Q4"];
287        let groups = vec!["Alpha", "Alpha", "Beta", "Beta", "Beta"];
288        let vals = vec![10.0, 20.0, 15.0, 25.0, 12.0];
289        let svg = stacked_bar(&cats, &groups, &vals)
290            .title("Stacked – Sparse Groups")
291            .x_label("Quarter")
292            .y_label("Value")
293            .size(500.0, 350.0)
294            .to_svg()?;
295        sections.push(("Bar – Sparse Stacked", svg));
296    }
297
298    // Stacked bar with mixed positive/negative (diverging stack)
299    {
300        let chart = Chart::new()
301            .layer(
302                Layer::new(MarkType::Bar)
303                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
304                    .with_y(vec![10.0, 15.0, 12.0, 18.0])
305                    .with_label("Revenue")
306                    .position(Position::Stack),
307            )
308            .layer(
309                Layer::new(MarkType::Bar)
310                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
311                    .with_y(vec![-4.0, -8.0, -5.0, -6.0])
312                    .with_label("Costs")
313                    .position(Position::Stack),
314            )
315            .title("Diverging Stack (+/-)")
316            .x_label("Period")
317            .y_label("Net Change")
318            .size(500.0, 350.0);
319        sections.push(("Bar – Diverging Stack", chart.to_svg()?));
320    }
321
322    // ═══════════════════════════════════════════════════════════════════
323    // HISTOGRAM
324    // ═══════════════════════════════════════════════════════════════════
325
326    {
327        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0 + 10.0).collect();
328        let svg = histogram(&data)
329            .bins(25)
330            .title("Histogram (n=500, 25 bins)")
331            .x_label("Value")
332            .y_label("Count")
333            .size(500.0, 350.0)
334            .to_svg()?;
335        sections.push(("Histogram", svg));
336    }
337
338    // ═══════════════════════════════════════════════════════════════════
339    // AREA CHARTS
340    // ═══════════════════════════════════════════════════════════════════
341
342    {
343        let x: Vec<f64> = (0..30).map(f64::from).collect();
344        let y: Vec<f64> = x
345            .iter()
346            .map(|&v| (v * 0.3).sin().abs() * 20.0 + 5.0)
347            .collect();
348        let svg = area(&x, &y)
349            .title("Area Chart")
350            .x_label("Day")
351            .y_label("Traffic")
352            .size(500.0, 350.0)
353            .to_svg()?;
354        sections.push(("Area – Basic", svg));
355    }
356
357    // Stacked area
358    {
359        let x: Vec<f64> = (0..20).map(f64::from).collect();
360        let y1: Vec<f64> = x
361            .iter()
362            .map(|&v| (v * 0.3).sin().abs() * 10.0 + 5.0)
363            .collect();
364        let y2: Vec<f64> = x
365            .iter()
366            .map(|&v| (v * 0.2).cos().abs() * 8.0 + 3.0)
367            .collect();
368        let chart = Chart::new()
369            .layer(
370                Layer::new(MarkType::Area)
371                    .with_x(x.clone())
372                    .with_y(y1)
373                    .with_label("Direct")
374                    .position(Position::Stack),
375            )
376            .layer(
377                Layer::new(MarkType::Area)
378                    .with_x(x)
379                    .with_y(y2)
380                    .with_label("Referral")
381                    .position(Position::Stack),
382            )
383            .title("Stacked Area")
384            .x_label("Week")
385            .y_label("Visits")
386            .size(500.0, 350.0);
387        sections.push(("Area – Stacked", chart.to_svg()?));
388    }
389
390    // ═══════════════════════════════════════════════════════════════════
391    // PIE / DONUT
392    // ═══════════════════════════════════════════════════════════════════
393
394    {
395        let vals = vec![35.0, 25.0, 20.0, 15.0, 5.0];
396        let labels = vec!["Chrome", "Firefox", "Safari", "Edge", "Other"];
397        let svg = pie_labeled(&labels, &vals)
398            .title("Pie Chart")
399            .size(400.0, 400.0)
400            .to_svg()?;
401        sections.push(("Pie", svg));
402    }
403
404    {
405        let vals = vec![60.0, 25.0, 15.0];
406        let labels = vec!["Pass", "Warn", "Fail"];
407        let svg = pie_labeled(&labels, &vals)
408            .donut(0.55)
409            .title("Donut Chart")
410            .size(400.0, 400.0)
411            .to_svg()?;
412        sections.push(("Donut", svg));
413    }
414
415    // ═══════════════════════════════════════════════════════════════════
416    // BOX PLOT
417    // ═══════════════════════════════════════════════════════════════════
418
419    {
420        let mut cats = Vec::new();
421        let mut vals = Vec::new();
422        for (label, base, spread) in &[
423            ("Control", 50.0, 15.0),
424            ("Drug A", 65.0, 10.0),
425            ("Drug B", 70.0, 20.0),
426        ] {
427            for _ in 0..40 {
428                vals.push(base + (rng.uniform() - 0.5) * spread * 2.0);
429                cats.push(*label);
430            }
431            // Add outlier
432            vals.push(base + spread * 4.0);
433            cats.push(*label);
434        }
435        let svg = boxplot(&cats, &vals)
436            .title("Box Plot with Outliers")
437            .x_label("Treatment")
438            .y_label("Response")
439            .size(500.0, 350.0)
440            .to_svg()?;
441        sections.push(("Box Plot", svg));
442    }
443
444    // ═══════════════════════════════════════════════════════════════════
445    // HEATMAPS
446    // ═══════════════════════════════════════════════════════════════════
447
448    // Basic heatmap with annotations + gradient legend
449    {
450        let data = vec![
451            vec![1.0, 2.0, 3.0, 4.0, 5.0],
452            vec![5.0, 4.0, 3.0, 2.0, 1.0],
453            vec![2.0, 8.0, 6.0, 4.0, 2.0],
454            vec![3.0, 3.0, 9.0, 3.0, 3.0],
455        ];
456        let svg = heatmap(data)
457            .annotate()
458            .with_row_labels(&["A", "B", "C", "D"])
459            .with_col_labels(&["v1", "v2", "v3", "v4", "v5"])
460            .title("Heatmap (annotated + gradient legend)")
461            .x_label("Variable")
462            .y_label("Group")
463            .size(500.0, 400.0)
464            .to_svg()?;
465        sections.push(("Heatmap – Annotated", svg));
466    }
467
468    // Confusion matrix
469    {
470        let data = vec![
471            vec![45.0, 3.0, 2.0],
472            vec![1.0, 40.0, 5.0],
473            vec![0.0, 4.0, 50.0],
474        ];
475        let svg = heatmap(data)
476            .annotate()
477            .with_row_labels(&["Cat", "Dog", "Bird"])
478            .with_col_labels(&["Cat", "Dog", "Bird"])
479            .title("Confusion Matrix")
480            .x_label("Predicted")
481            .y_label("Actual")
482            .size(400.0, 400.0)
483            .to_svg()?;
484        sections.push(("Heatmap – Confusion Matrix", svg));
485    }
486
487    // Heatmap with custom color scale
488    {
489        let data = vec![
490            vec![0.0, 0.3, 0.7, 1.0],
491            vec![0.2, 0.5, 0.8, 0.9],
492            vec![0.1, 0.4, 0.6, 0.95],
493        ];
494        let mut theme = NewTheme::light();
495        theme.color_scale = Some(esoc_color::ColorScale::rdbu());
496        let svg = heatmap(data)
497            .annotate()
498            .title("Heatmap – RdBu Color Scale")
499            .theme(theme)
500            .size(400.0, 350.0)
501            .to_svg()?;
502        sections.push(("Heatmap – Custom Color Scale", svg));
503    }
504
505    // ═══════════════════════════════════════════════════════════════════
506    // FACETED CHARTS (small multiples)
507    // ═══════════════════════════════════════════════════════════════════
508
509    // Faceted scatter
510    {
511        let mut x = Vec::new();
512        let mut y = Vec::new();
513        let mut facets = Vec::new();
514        for panel in &["East", "West", "North", "South"] {
515            for _ in 0..25 {
516                x.push(rng.uniform() * 10.0);
517                y.push(rng.uniform() * 10.0);
518                facets.push(*panel);
519            }
520        }
521        let svg = scatter(&x, &y)
522            .facet_wrap(&facets, 2)
523            .title("Faceted Scatter (2 cols)")
524            .x_label("X")
525            .y_label("Y")
526            .size(550.0, 450.0)
527            .to_svg()?;
528        sections.push(("Facet – Scatter", svg));
529    }
530
531    // Faceted scatter with categories + legend (tests faceted legend fix)
532    {
533        let mut x = Vec::new();
534        let mut y = Vec::new();
535        let mut cats = Vec::new();
536        let mut facets = Vec::new();
537        for panel in &["Male", "Female"] {
538            for cat in &["Young", "Old"] {
539                for _ in 0..12 {
540                    x.push(rng.uniform() * 10.0);
541                    y.push(rng.uniform() * 10.0);
542                    cats.push(*cat);
543                    facets.push(*panel);
544                }
545            }
546        }
547        let chart = Chart::new()
548            .layer(
549                Layer::new(MarkType::Point)
550                    .with_x(x)
551                    .with_y(y)
552                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
553                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
554            )
555            .facet(Facet::Wrap { ncol: 2 })
556            .title("Faceted + Categories + Legend")
557            .x_label("X")
558            .y_label("Y")
559            .size(550.0, 350.0);
560        sections.push(("Facet – With Legend", chart.to_svg()?));
561    }
562
563    // Faceted with FreeY scales (tests FreeY fix: shared X, free Y)
564    {
565        let mut x = Vec::new();
566        let mut y = Vec::new();
567        let mut facets = Vec::new();
568        // Panel A: small values; Panel B: large values
569        for _ in 0..20 {
570            x.push(rng.uniform() * 10.0);
571            y.push(rng.uniform() * 5.0);
572            facets.push("Small Range");
573        }
574        for _ in 0..20 {
575            x.push(rng.uniform() * 10.0);
576            y.push(rng.uniform() * 500.0);
577            facets.push("Large Range");
578        }
579        let chart = Chart::new()
580            .layer(
581                Layer::new(MarkType::Point)
582                    .with_x(x)
583                    .with_y(y)
584                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
585            )
586            .facet(Facet::Wrap { ncol: 2 })
587            .facet_scales(FacetScales::FreeY)
588            .title("FreeY Scales (shared X, free Y)")
589            .x_label("X")
590            .y_label("Y")
591            .size(550.0, 350.0);
592        sections.push(("Facet – FreeY", chart.to_svg()?));
593    }
594
595    // ═══════════════════════════════════════════════════════════════════
596    // ANNOTATIONS
597    // ═══════════════════════════════════════════════════════════════════
598
599    {
600        let x: Vec<f64> = (0..20).map(f64::from).collect();
601        let y: Vec<f64> = x.iter().map(|&v| v * 1.5 + rng.normal() * 3.0).collect();
602        let chart = scatter(&x, &y)
603            .title("Annotations Demo")
604            .x_label("X")
605            .y_label("Y")
606            .size(500.0, 350.0)
607            .build()
608            .annotate(Annotation::hline(15.0).with_label("Target"))
609            .annotate(Annotation::vline(10.0).with_label("Midpoint"))
610            .annotate(Annotation::band(10.0, 20.0).with_label("Peak zone"));
611        sections.push(("Annotations", chart.to_svg()?));
612    }
613
614    // ═══════════════════════════════════════════════════════════════════
615    // SUBTITLE, CAPTION, LINE+SCATTER OVERLAY
616    // ═══════════════════════════════════════════════════════════════════
617
618    {
619        let x: Vec<f64> = (0..10).map(f64::from).collect();
620        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];
621        let y_trend: Vec<f64> = x.iter().map(|&v| v * 0.8 + 2.0).collect();
622        let chart = Chart::new()
623            .layer(
624                Layer::new(MarkType::Point)
625                    .with_x(x.clone())
626                    .with_y(y_data)
627                    .with_label("Data"),
628            )
629            .layer(
630                Layer::new(MarkType::Line)
631                    .with_x(x)
632                    .with_y(y_trend)
633                    .with_label("Trend"),
634            )
635            .title("Revenue Trend")
636            .subtitle("H1 2026 with linear fit")
637            .caption("Source: internal CRM data")
638            .x_label("Month")
639            .y_label("Revenue ($K)")
640            .size(500.0, 380.0);
641        sections.push(("Line + Scatter + Subtitle/Caption", chart.to_svg()?));
642    }
643
644    // ═══════════════════════════════════════════════════════════════════
645    // DARK THEME
646    // ═══════════════════════════════════════════════════════════════════
647
648    {
649        let x: Vec<f64> = (0..30).map(|i| f64::from(i) * 0.5).collect();
650        let y: Vec<f64> = x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 8.0).collect();
651        let svg = line(&x, &y)
652            .title("Dark Theme")
653            .x_label("Time")
654            .y_label("Value")
655            .theme(NewTheme::dark())
656            .size(500.0, 350.0)
657            .to_svg()?;
658        sections.push(("Theme – Dark", svg));
659    }
660
661    // ═══════════════════════════════════════════════════════════════════
662    // PUBLICATION THEME
663    // ═══════════════════════════════════════════════════════════════════
664
665    {
666        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
667        let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
668        let svg = scatter(&x, &y)
669            .title("Publication Theme (no grid, serif)")
670            .x_label("X")
671            .y_label("Y")
672            .theme(NewTheme::publication())
673            .size(500.0, 350.0)
674            .to_svg()?;
675        sections.push(("Theme – Publication", svg));
676    }
677
678    // ═══════════════════════════════════════════════════════════════════
679    // BUILD HTML
680    // ═══════════════════════════════════════════════════════════════════
681
682    let mut html = String::from(
683        r#"<!DOCTYPE html>
684<html lang="en">
685<head>
686<meta charset="UTF-8">
687<meta name="viewport" content="width=device-width, initial-scale=1.0">
688<title>esoc-chart Review — All Chart Types</title>
689<style>
690  * { margin: 0; padding: 0; box-sizing: border-box; }
691  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
692  header { background: linear-gradient(135deg, #1a1a2e, #16213e); color: white; padding: 2.5rem 2rem; text-align: center; }
693  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
694  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
695  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; }
696  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
697  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 2rem; max-width: 1600px; margin: 0 auto; }
698  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
699  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
700  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
701  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
702  .card svg { display: block; width: 100%; height: auto; }
703  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
704  .feedback { padding: 0 1rem 1rem; }
705  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
706  .feedback textarea:focus { outline: none; border-color: #1a1a2e; }
707  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
708  .actions { padding: 1.5rem 2rem; text-align: center; }
709  .actions button { background: #1a1a2e; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
710  .actions button:hover { background: #2a2a4e; }
711</style>
712<script>
713  const feedback = {};
714  function loadFeedback() {
715    try { Object.assign(feedback, JSON.parse(localStorage.getItem('chart_review_feedback') || '{}')); } catch {}
716    document.querySelectorAll('.feedback textarea').forEach(ta => {
717      const key = ta.dataset.chart;
718      if (feedback[key]) ta.value = feedback[key];
719    });
720  }
721  function saveFeedback(key, value) {
722    feedback[key] = value;
723    localStorage.setItem('chart_review_feedback', JSON.stringify(feedback));
724  }
725  function exportFeedback() {
726    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
727    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
728    a.download = 'chart_review_feedback.json'; a.click();
729  }
730  window.addEventListener('DOMContentLoaded', loadFeedback);
731</script>
732</head>
733<body>
734<header>
735  <h1>esoc-chart Review</h1>
736  <p>Comprehensive sample of all chart types &amp; variations after audit fixes</p>
737  <div class="stats">
738"#,
739    );
740
741    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
742    html.push_str("    <span>6 phases of fixes</span>\n");
743    html.push_str("    <span>23 new tests</span>\n");
744    html.push_str("  </div>\n</header>\n<div class=\"grid\">\n");
745
746    for (title, svg) in &sections {
747        let key = title
748            .to_lowercase()
749            .replace([' ', '–', '+', '/', '(', ')'], "_")
750            .replace("__", "_");
751        let dark_class = if title.contains("Dark") {
752            " dark-bg"
753        } else {
754            ""
755        };
756        write!(
757            html,
758            concat!(
759                "<div class=\"card{dark_class}\">\n",
760                "  <h2>{title}</h2>\n",
761                "  <div class=\"chart-wrap\">{svg}</div>\n",
762                "  <div class=\"feedback\">\n",
763                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
764                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
765                "    <div class=\"status\">Auto-saved</div>\n",
766                "  </div>\n",
767                "</div>\n",
768            ),
769            title = title,
770            svg = svg,
771            key = key,
772            dark_class = dark_class,
773        )
774        .unwrap();
775    }
776
777    html.push_str(concat!(
778        "</div>\n",
779        "<div class=\"actions\">\n",
780        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
781        "</div>\n",
782        "</body>\n</html>\n",
783    ));
784
785    let out_path = "chart_review.html";
786    std::fs::write(out_path, &html).expect("failed to write HTML");
787    println!("Saved {} ({} charts)", out_path, sections.len());
788
789    Ok(())
790}
examples/stress_test.rs (line 908)
20fn main() -> esoc_chart::error::Result<()> {
21    // ── Simple RNG ───────────────────────────────────────────────────
22    struct Rng(u64);
23    impl Rng {
24        fn uniform(&mut self) -> f64 {
25            self.0 = self
26                .0
27                .wrapping_mul(6_364_136_223_846_793_005)
28                .wrapping_add(1);
29            (self.0 >> 11) as f64 / (1u64 << 53) as f64
30        }
31        fn normal(&mut self) -> f64 {
32            let u1 = self.uniform().max(1e-15);
33            let u2 = self.uniform();
34            (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
35        }
36        fn range(&mut self, lo: f64, hi: f64) -> f64 {
37            lo + self.uniform() * (hi - lo)
38        }
39    }
40    let mut sections: Vec<(&str, String)> = Vec::new();
41    let mut rng = Rng(12345);
42
43    // ═════════════════════════════════════════════════════════════════
44    //  SECTION 1: SCATTER VARIATIONS
45    // ═════════════════════════════════════════════════════════════════
46
47    // 1a. Tiny scatter (2 points)
48    {
49        let svg = scatter(&[1.0, 2.0], &[3.0, 4.0])
50            .title("2-Point Scatter")
51            .x_label("X")
52            .y_label("Y")
53            .size(400.0, 300.0)
54            .to_svg()?;
55        sections.push(("Scatter — 2 Points", svg));
56    }
57
58    // 1b. Large scatter with auto-opacity
59    {
60        let n = 1000;
61        let x: Vec<f64> = (0..n).map(|_| rng.normal() * 5.0 + 50.0).collect();
62        let y: Vec<f64> = x
63            .iter()
64            .map(|&xi| xi * 0.6 + rng.normal() * 8.0 + 10.0)
65            .collect();
66        let svg = scatter(&x, &y)
67            .title("Dense Scatter (n=1000)")
68            .x_label("Income ($K)")
69            .y_label("Spending ($K)")
70            .size(500.0, 400.0)
71            .to_svg()?;
72        sections.push(("Scatter — Dense 1K", svg));
73    }
74
75    // 1c. Scatter with 6 categories
76    {
77        let mut x = Vec::new();
78        let mut y = Vec::new();
79        let mut cats = Vec::new();
80        let names = [
81            "Setosa",
82            "Versicolor",
83            "Virginica",
84            "Hybrid-A",
85            "Hybrid-B",
86            "Unknown",
87        ];
88        for (i, name) in names.iter().enumerate() {
89            let cx = 2.0 + i as f64 * 1.5;
90            let cy = 3.0 + (i as f64 * 0.7).sin() * 2.0;
91            for _ in 0..20 {
92                x.push(cx + rng.normal() * 0.5);
93                y.push(cy + rng.normal() * 0.5);
94                cats.push(*name);
95            }
96        }
97        let svg = scatter(&x, &y)
98            .color_by(&cats)
99            .title("Scatter — 6 Categories (Iris-like)")
100            .x_label("Sepal Length")
101            .y_label("Petal Width")
102            .size(550.0, 400.0)
103            .to_svg()?;
104        sections.push(("Scatter — 6 Categories", svg));
105    }
106
107    // 1d. Scatter with jitter position
108    {
109        let x: Vec<f64> = (0..60).map(|i| f64::from(i % 3)).collect();
110        let y: Vec<f64> = x
111            .iter()
112            .map(|&xi| xi * 2.0 + rng.normal() * 0.5 + 5.0)
113            .collect();
114        let cats: Vec<String> = (0..60)
115            .map(|i| ["Low", "Med", "High"][i % 3].into())
116            .collect();
117        let chart = Chart::new()
118            .layer(
119                Layer::new(MarkType::Point)
120                    .with_x(x)
121                    .with_y(y)
122                    .with_categories(cats)
123                    .position(Position::Jitter {
124                        x_amount: 0.2,
125                        y_amount: 0.0,
126                    }),
127            )
128            .title("Scatter — Jittered (strip plot)")
129            .x_label("Group")
130            .y_label("Value")
131            .size(450.0, 350.0);
132        sections.push(("Scatter — Jitter", chart.to_svg()?));
133    }
134
135    // 1e. Scatter with all annotations
136    {
137        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 100.0)).collect();
138        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + rng.normal() * 10.0).collect();
139        let chart = scatter(&x, &y)
140            .title("Scatter — All Annotation Types")
141            .x_label("X")
142            .y_label("Y")
143            .size(550.0, 400.0)
144            .build()
145            .annotate(
146                Annotation::hline(40.0)
147                    .with_label("Mean")
148                    .with_color(Color::from_hex("#e74c3c").unwrap()),
149            )
150            .annotate(
151                Annotation::vline(50.0)
152                    .with_label("Midpoint")
153                    .with_color(Color::from_hex("#3498db").unwrap()),
154            )
155            .annotate(
156                Annotation::band(20.0, 60.0)
157                    .with_label("Normal range")
158                    .with_color(Color::from_hex("#2ecc71").unwrap().with_alpha(0.12)),
159            )
160            .annotate(Annotation::text(70.0, 70.0, "Outlier zone"));
161        sections.push(("Scatter — All Annotations", chart.to_svg()?));
162    }
163
164    // 1f. Scatter with LOESS smooth overlay
165    {
166        let x: Vec<f64> = (0..50).map(|i| f64::from(i) * 0.2).collect();
167        let y: Vec<f64> = x
168            .iter()
169            .map(|&v| (v * 0.5).sin() * 4.0 + v * 0.3 + rng.normal() * 1.0)
170            .collect();
171        let chart = Chart::new()
172            .layer(
173                Layer::new(MarkType::Point)
174                    .with_x(x.clone())
175                    .with_y(y.clone())
176                    .with_label("Raw"),
177            )
178            .layer(
179                Layer::new(MarkType::Line)
180                    .with_x(x)
181                    .with_y(y)
182                    .stat(Stat::Smooth { bandwidth: 0.25 })
183                    .with_label("LOESS (bw=0.25)"),
184            )
185            .title("Scatter + LOESS Smooth")
186            .x_label("Time")
187            .y_label("Signal")
188            .size(500.0, 380.0);
189        sections.push(("Scatter — LOESS Overlay", chart.to_svg()?));
190    }
191
192    // 1g. Scatter with subtitle, caption, description
193    {
194        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
195        let y = vec![2.1, 3.8, 3.2, 5.5, 4.8, 7.1, 6.3, 8.0, 7.5, 9.2];
196        let chart = Chart::new()
197            .layer(Layer::new(MarkType::Point).with_x(x).with_y(y))
198            .title("Monthly Revenue")
199            .subtitle("Jan–Oct 2026, all regions")
200            .caption("Source: internal CRM")
201            .description("Scatter plot of monthly revenue showing upward trend")
202            .x_label("Month")
203            .y_label("Revenue ($M)")
204            .size(500.0, 400.0);
205        sections.push(("Scatter — Subtitle+Caption", chart.to_svg()?));
206    }
207
208    // ═════════════════════════════════════════════════════════════════
209    //  SECTION 2: LINE VARIATIONS
210    // ═════════════════════════════════════════════════════════════════
211
212    // 2a. Single line
213    {
214        let x: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
215        let y: Vec<f64> = x.iter().map(|&v| (v * 0.5).sin() * 3.0 + v * 0.2).collect();
216        let svg = line(&x, &y)
217            .title("Smooth Sine Wave")
218            .x_label("Time (s)")
219            .y_label("Amplitude")
220            .size(600.0, 300.0)
221            .to_svg()?;
222        sections.push(("Line — Single", svg));
223    }
224
225    // 2b. Multi-line (5 series)
226    {
227        let x: Vec<f64> = (0..40).map(f64::from).collect();
228        let chart = Chart::new()
229            .layer(
230                Layer::new(MarkType::Line)
231                    .with_x(x.clone())
232                    .with_y(x.iter().map(|&v| (v * 0.2).sin() * 10.0 + 20.0).collect())
233                    .with_label("Server A"),
234            )
235            .layer(
236                Layer::new(MarkType::Line)
237                    .with_x(x.clone())
238                    .with_y(x.iter().map(|&v| (v * 0.15).cos() * 8.0 + 25.0).collect())
239                    .with_label("Server B"),
240            )
241            .layer(
242                Layer::new(MarkType::Line)
243                    .with_x(x.clone())
244                    .with_y(x.iter().map(|&v| v * 0.5 + 10.0).collect())
245                    .with_label("Server C"),
246            )
247            .layer(
248                Layer::new(MarkType::Line)
249                    .with_x(x.clone())
250                    .with_y(x.iter().map(|&v| 30.0 - v * 0.3).collect())
251                    .with_label("Server D"),
252            )
253            .layer(
254                Layer::new(MarkType::Line)
255                    .with_x(x.clone())
256                    .with_y(
257                        x.iter()
258                            .map(|&v| ((v * 0.3).sin() * 5.0 + 18.0).max(5.0))
259                            .collect(),
260                    )
261                    .with_label("Server E"),
262            )
263            .title("5-Server Latency Dashboard")
264            .x_label("Minute")
265            .y_label("Latency (ms)")
266            .size(650.0, 400.0);
267        sections.push(("Line — 5 Series", chart.to_svg()?));
268    }
269
270    // 2c. Line + scatter overlay (actual vs predicted)
271    {
272        let x: Vec<f64> = (0..15).map(f64::from).collect();
273        let actual: Vec<f64> = vec![
274            3.0, 5.2, 4.1, 7.8, 6.5, 9.1, 8.3, 10.2, 9.8, 12.1, 11.5, 13.7, 12.9, 15.0, 14.2,
275        ];
276        let predicted: Vec<f64> = x.iter().map(|&v| v * 0.9 + 2.5).collect();
277        let chart = Chart::new()
278            .layer(
279                Layer::new(MarkType::Point)
280                    .with_x(x.clone())
281                    .with_y(actual)
282                    .with_label("Actual"),
283            )
284            .layer(
285                Layer::new(MarkType::Line)
286                    .with_x(x)
287                    .with_y(predicted)
288                    .with_label("Predicted"),
289            )
290            .title("Actual vs Predicted")
291            .x_label("Week")
292            .y_label("Sales ($K)")
293            .size(500.0, 380.0);
294        sections.push(("Line — Actual vs Predicted", chart.to_svg()?));
295    }
296
297    // 2d. Noisy line with wide LOESS
298    {
299        let x: Vec<f64> = (0..80).map(|i| f64::from(i) * 0.125).collect();
300        let y: Vec<f64> = x
301            .iter()
302            .map(|&v| (v * 0.8).sin() * 5.0 + rng.normal() * 2.5)
303            .collect();
304        let chart = Chart::new()
305            .layer(
306                Layer::new(MarkType::Line)
307                    .with_x(x.clone())
308                    .with_y(y.clone())
309                    .with_label("Raw"),
310            )
311            .layer(
312                Layer::new(MarkType::Line)
313                    .with_x(x.clone())
314                    .with_y(y.clone())
315                    .stat(Stat::Smooth { bandwidth: 0.15 })
316                    .with_label("bw=0.15"),
317            )
318            .layer(
319                Layer::new(MarkType::Line)
320                    .with_x(x)
321                    .with_y(y)
322                    .stat(Stat::Smooth { bandwidth: 0.5 })
323                    .with_label("bw=0.5"),
324            )
325            .title("LOESS Bandwidth Comparison")
326            .x_label("X")
327            .y_label("Y")
328            .size(550.0, 380.0);
329        sections.push(("Line — LOESS Bandwidths", chart.to_svg()?));
330    }
331
332    // ═════════════════════════════════════════════════════════════════
333    //  SECTION 3: BAR VARIATIONS
334    // ═════════════════════════════════════════════════════════════════
335
336    // 3a. 3 bars — minimal
337    {
338        let svg = bar(&["A", "B", "C"], &[10.0, 20.0, 15.0])
339            .title("3-Bar Minimal")
340            .size(350.0, 280.0)
341            .to_svg()?;
342        sections.push(("Bar — 3 Bars", svg));
343    }
344
345    // 3b. Single bar
346    {
347        let svg = bar(&["Total"], &[42.0])
348            .title("Single Bar")
349            .y_label("Count")
350            .size(300.0, 280.0)
351            .to_svg()?;
352        sections.push(("Bar — Single", svg));
353    }
354
355    // 3c. Many bars (20 categories, label rotation stress)
356    {
357        let cats: Vec<String> = (0..20).map(|i| format!("Department {}", i + 1)).collect();
358        let vals: Vec<f64> = (0..20)
359            .map(|i| 10.0 + (f64::from(i) * 2.7).sin().abs() * 40.0)
360            .collect();
361        let cat_refs: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
362        let svg = bar(&cat_refs, &vals)
363            .title("20 Departments — Label Rotation Stress")
364            .x_label("Department")
365            .y_label("Budget ($K)")
366            .size(700.0, 400.0)
367            .to_svg()?;
368        sections.push(("Bar — 20 Categories", svg));
369    }
370
371    // 3d. Very long category names
372    {
373        let cats = [
374            "Engineering & Product Development",
375            "Marketing & Communications",
376            "Human Resources Management",
377            "Finance & Accounting Dept.",
378        ];
379        let vals = vec![85.0, 62.0, 45.0, 71.0];
380        let svg = bar(&cats, &vals)
381            .title("Long Category Names")
382            .x_label("Department")
383            .y_label("Headcount")
384            .size(550.0, 400.0)
385            .to_svg()?;
386        sections.push(("Bar — Long Names", svg));
387    }
388
389    // 3e. Horizontal bar
390    {
391        let chart = Chart::new()
392            .layer(
393                Layer::new(MarkType::Bar)
394                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
395                    .with_y(vec![95.0, 87.0, 76.0, 68.0, 55.0, 42.0])
396                    .with_categories(vec![
397                        "Rust".into(),
398                        "Go".into(),
399                        "Python".into(),
400                        "Java".into(),
401                        "C++".into(),
402                        "JavaScript".into(),
403                    ]),
404            )
405            .coord(CoordSystem::Flipped)
406            .title("Horizontal Bars — Performance Score")
407            .x_label("Score")
408            .y_label("Language")
409            .size(500.0, 380.0);
410        sections.push(("Bar — Horizontal", chart.to_svg()?));
411    }
412
413    // 3f. Horizontal bar with very long labels
414    {
415        let chart = Chart::new()
416            .layer(
417                Layer::new(MarkType::Bar)
418                    .with_x(vec![0.0, 1.0, 2.0, 3.0])
419                    .with_y(vec![88.0, 72.0, 65.0, 91.0])
420                    .with_categories(vec![
421                        "Customer Satisfaction Index".into(),
422                        "Net Promoter Score".into(),
423                        "Employee Engagement Rate".into(),
424                        "Year-over-Year Revenue Growth".into(),
425                    ]),
426            )
427            .coord(CoordSystem::Flipped)
428            .title("Horizontal — Long Y Labels")
429            .x_label("Percentage")
430            .y_label("KPI")
431            .size(550.0, 350.0);
432        sections.push(("Bar — Horiz Long Labels", chart.to_svg()?));
433    }
434
435    // 3g. Grouped bar (3 groups × 4 quarters)
436    {
437        let cats = vec![
438            "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4",
439        ];
440        let groups = vec![
441            "2023", "2023", "2023", "2023", "2024", "2024", "2024", "2024", "2025", "2025", "2025",
442            "2025",
443        ];
444        let vals = vec![
445            10.0, 14.0, 18.0, 12.0, 12.0, 18.0, 22.0, 15.0, 14.0, 20.0, 28.0, 19.0,
446        ];
447        let svg = grouped_bar(&cats, &groups, &vals)
448            .title("Grouped Bar — 3 Years × 4 Quarters")
449            .x_label("Quarter")
450            .y_label("Revenue ($M)")
451            .size(550.0, 380.0)
452            .to_svg()?;
453        sections.push(("Bar — Grouped 3×4", svg));
454    }
455
456    // 3h. Grouped bar — many groups (5 groups)
457    {
458        let mut cats = Vec::new();
459        let mut groups = Vec::new();
460        let mut vals = Vec::new();
461        let regions = ["North", "South", "East", "West", "Central"];
462        let quarters = ["Q1", "Q2", "Q3"];
463        for q in quarters {
464            for (i, r) in regions.iter().enumerate() {
465                cats.push(q);
466                groups.push(*r);
467                vals.push(10.0 + i as f64 * 5.0 + rng.range(0.0, 15.0));
468            }
469        }
470        let svg = grouped_bar(&cats, &groups, &vals)
471            .title("Grouped Bar — 5 Groups (crowded)")
472            .x_label("Quarter")
473            .y_label("Sales ($K)")
474            .size(600.0, 380.0)
475            .to_svg()?;
476        sections.push(("Bar — Grouped 5 Groups", svg));
477    }
478
479    // 3i. Stacked bar
480    {
481        let cats = vec![
482            "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
483        ];
484        let groups = vec![
485            "Online", "Online", "Online", "Online", "Online", "Online", "Retail", "Retail",
486            "Retail", "Retail", "Retail", "Retail",
487        ];
488        let vals = vec![
489            20.0, 25.0, 30.0, 28.0, 35.0, 40.0, 15.0, 12.0, 18.0, 20.0, 16.0, 14.0,
490        ];
491        let svg = stacked_bar(&cats, &groups, &vals)
492            .title("Stacked Bar — Online vs Retail")
493            .x_label("Month")
494            .y_label("Revenue ($K)")
495            .size(550.0, 380.0)
496            .to_svg()?;
497        sections.push(("Bar — Stacked", svg));
498    }
499
500    // 3j. Stacked bar — 4 groups (tall stack)
501    {
502        let mut cats = Vec::new();
503        let mut groups = Vec::new();
504        let mut vals = Vec::new();
505        let products = ["Widget", "Gadget", "Doohickey", "Thingamajig"];
506        let months = ["Jan", "Feb", "Mar", "Apr"];
507        for m in months {
508            for p in products {
509                cats.push(m);
510                groups.push(p);
511                vals.push(rng.range(5.0, 30.0));
512            }
513        }
514        let svg = stacked_bar(&cats, &groups, &vals)
515            .title("Stacked — 4 Product Lines")
516            .x_label("Month")
517            .y_label("Units Sold")
518            .size(500.0, 380.0)
519            .to_svg()?;
520        sections.push(("Bar — Stacked 4 Groups", svg));
521    }
522
523    // 3k. Diverging stack (positive + negative)
524    {
525        let chart = Chart::new()
526            .layer(
527                Layer::new(MarkType::Bar)
528                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
529                    .with_y(vec![15.0, 22.0, 18.0, 25.0, 20.0, 28.0])
530                    .with_label("Revenue")
531                    .position(Position::Stack),
532            )
533            .layer(
534                Layer::new(MarkType::Bar)
535                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
536                    .with_y(vec![-8.0, -12.0, -6.0, -10.0, -14.0, -9.0])
537                    .with_label("Costs")
538                    .position(Position::Stack),
539            )
540            .layer(
541                Layer::new(MarkType::Bar)
542                    .with_x(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
543                    .with_y(vec![-3.0, -4.0, -5.0, -3.0, -2.0, -4.0])
544                    .with_label("Tax")
545                    .position(Position::Stack),
546            )
547            .title("Diverging Stack — Revenue/Costs/Tax")
548            .x_label("Month")
549            .y_label("$M")
550            .size(550.0, 380.0);
551        sections.push(("Bar — Diverging 3 Layers", chart.to_svg()?));
552    }
553
554    // 3l. Bars with values near zero
555    {
556        let svg = bar(&["A", "B", "C", "D", "E"], &[0.1, 0.05, 0.2, 0.01, 0.15])
557            .title("Bars — Very Small Values")
558            .y_label("Rate")
559            .size(450.0, 320.0)
560            .to_svg()?;
561        sections.push(("Bar — Small Values", svg));
562    }
563
564    // 3m. Bars with huge variance
565    {
566        let svg = bar(&["Tiny", "Medium", "Huge"], &[2.0, 50.0, 980.0])
567            .title("Bars — Huge Variance")
568            .y_label("Count")
569            .size(400.0, 320.0)
570            .to_svg()?;
571        sections.push(("Bar — Huge Variance", svg));
572    }
573
574    // ═════════════════════════════════════════════════════════════════
575    //  SECTION 4: HISTOGRAM VARIATIONS
576    // ═════════════════════════════════════════════════════════════════
577
578    // 4a. Normal distribution
579    {
580        let data: Vec<f64> = (0..800).map(|_| rng.normal() * 3.0 + 50.0).collect();
581        let svg = histogram(&data)
582            .bins(30)
583            .title("Histogram — Normal (n=800, 30 bins)")
584            .x_label("Value")
585            .y_label("Frequency")
586            .size(500.0, 350.0)
587            .to_svg()?;
588        sections.push(("Histogram — Normal", svg));
589    }
590
591    // 4b. Bimodal distribution
592    {
593        let mut data: Vec<f64> = (0..400).map(|_| rng.normal() * 2.0 + 30.0).collect();
594        data.extend((0..400).map(|_| rng.normal() * 2.0 + 45.0));
595        let svg = histogram(&data)
596            .bins(35)
597            .title("Histogram — Bimodal")
598            .x_label("Measurement")
599            .y_label("Count")
600            .size(500.0, 350.0)
601            .to_svg()?;
602        sections.push(("Histogram — Bimodal", svg));
603    }
604
605    // 4c. Skewed (exponential-like)
606    {
607        let data: Vec<f64> = (0..600)
608            .map(|_| (-rng.uniform().max(1e-10).ln()) * 5.0)
609            .collect();
610        let svg = histogram(&data)
611            .bins(40)
612            .title("Histogram — Skewed (Exponential)")
613            .x_label("Wait Time (s)")
614            .y_label("Count")
615            .size(500.0, 350.0)
616            .to_svg()?;
617        sections.push(("Histogram — Skewed", svg));
618    }
619
620    // 4d. Few bins
621    {
622        let data: Vec<f64> = (0..200).map(|_| rng.normal() * 5.0 + 100.0).collect();
623        let svg = histogram(&data)
624            .bins(5)
625            .title("Histogram — 5 Bins Only")
626            .x_label("Score")
627            .y_label("Count")
628            .size(450.0, 320.0)
629            .to_svg()?;
630        sections.push(("Histogram — 5 Bins", svg));
631    }
632
633    // 4e. Many bins (tiny bars)
634    {
635        let data: Vec<f64> = (0..500).map(|_| rng.normal() * 2.0).collect();
636        let svg = histogram(&data)
637            .bins(80)
638            .title("Histogram — 80 Bins (sparse)")
639            .x_label("X")
640            .y_label("Count")
641            .size(600.0, 320.0)
642            .to_svg()?;
643        sections.push(("Histogram — 80 Bins", svg));
644    }
645
646    // ═════════════════════════════════════════════════════════════════
647    //  SECTION 5: AREA VARIATIONS
648    // ═════════════════════════════════════════════════════════════════
649
650    // 5a. Basic area
651    {
652        let x: Vec<f64> = (0..50).map(f64::from).collect();
653        let y: Vec<f64> = x
654            .iter()
655            .map(|&v| (v * 0.2).sin().abs() * 30.0 + 5.0 + rng.normal() * 2.0)
656            .collect();
657        let svg = area(&x, &y)
658            .title("Area — Website Traffic")
659            .x_label("Day")
660            .y_label("Visitors (K)")
661            .size(550.0, 350.0)
662            .to_svg()?;
663        sections.push(("Area — Basic", svg));
664    }
665
666    // 5b. Stacked area (2 series)
667    {
668        let x: Vec<f64> = (0..30).map(f64::from).collect();
669        let y1: Vec<f64> = x
670            .iter()
671            .map(|&v| (v * 0.15).sin().abs() * 10.0 + 8.0)
672            .collect();
673        let y2: Vec<f64> = x
674            .iter()
675            .map(|&v| (v * 0.2).cos().abs() * 7.0 + 4.0)
676            .collect();
677        let chart = Chart::new()
678            .layer(
679                Layer::new(MarkType::Area)
680                    .with_x(x.clone())
681                    .with_y(y1)
682                    .with_label("Organic")
683                    .position(Position::Stack),
684            )
685            .layer(
686                Layer::new(MarkType::Area)
687                    .with_x(x)
688                    .with_y(y2)
689                    .with_label("Paid")
690                    .position(Position::Stack),
691            )
692            .title("Stacked Area — Traffic Sources")
693            .x_label("Day")
694            .y_label("Sessions")
695            .size(550.0, 380.0);
696        sections.push(("Area — Stacked 2", chart.to_svg()?));
697    }
698
699    // 5c. Stacked area (4 series)
700    {
701        let x: Vec<f64> = (0..25).map(f64::from).collect();
702        let chart = Chart::new()
703            .layer(
704                Layer::new(MarkType::Area)
705                    .with_x(x.clone())
706                    .with_y(
707                        x.iter()
708                            .map(|&v| (v * 0.3).sin().abs() * 8.0 + 3.0)
709                            .collect(),
710                    )
711                    .with_label("Direct")
712                    .position(Position::Stack),
713            )
714            .layer(
715                Layer::new(MarkType::Area)
716                    .with_x(x.clone())
717                    .with_y(
718                        x.iter()
719                            .map(|&v| (v * 0.2).cos().abs() * 6.0 + 2.0)
720                            .collect(),
721                    )
722                    .with_label("Social")
723                    .position(Position::Stack),
724            )
725            .layer(
726                Layer::new(MarkType::Area)
727                    .with_x(x.clone())
728                    .with_y(
729                        x.iter()
730                            .map(|&v| (v * 0.15).sin().abs() * 4.0 + 1.5)
731                            .collect(),
732                    )
733                    .with_label("Email")
734                    .position(Position::Stack),
735            )
736            .layer(
737                Layer::new(MarkType::Area)
738                    .with_x(x.clone())
739                    .with_y(
740                        x.iter()
741                            .map(|&v| (v * 0.25).cos().abs() * 5.0 + 2.0)
742                            .collect(),
743                    )
744                    .with_label("Referral")
745                    .position(Position::Stack),
746            )
747            .title("Stacked Area — 4 Channels")
748            .x_label("Week")
749            .y_label("Visits (K)")
750            .size(600.0, 400.0);
751        sections.push(("Area — Stacked 4", chart.to_svg()?));
752    }
753
754    // ═════════════════════════════════════════════════════════════════
755    //  SECTION 6: PIE / DONUT VARIATIONS
756    // ═════════════════════════════════════════════════════════════════
757
758    // 6a. Basic pie (5 slices)
759    {
760        let svg = pie_labeled(
761            &["Chrome", "Firefox", "Safari", "Edge", "Other"],
762            &[35.0, 25.0, 20.0, 12.0, 8.0],
763        )
764        .title("Browser Market Share")
765        .size(400.0, 400.0)
766        .to_svg()?;
767        sections.push(("Pie — 5 Slices", svg));
768    }
769
770    // 6b. Pie with many slices (10)
771    {
772        let vals: Vec<f64> = (0..10).map(|i| 20.0 - f64::from(i) * 1.5).collect();
773        let labels: Vec<String> = (0..10).map(|i| format!("Slice {}", i + 1)).collect();
774        let label_refs: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
775        let svg = pie_labeled(&label_refs, &vals)
776            .title("Pie — 10 Slices")
777            .size(450.0, 450.0)
778            .to_svg()?;
779        sections.push(("Pie — 10 Slices", svg));
780    }
781
782    // 6c. Pie with one dominant slice
783    {
784        let svg = pie_labeled(
785            &["Dominant", "Small-A", "Small-B", "Tiny"],
786            &[90.0, 5.0, 3.0, 2.0],
787        )
788        .title("Pie — One Dominant (90%)")
789        .size(400.0, 400.0)
790        .to_svg()?;
791        sections.push(("Pie — Dominant Slice", svg));
792    }
793
794    // 6d. Equal slices
795    {
796        let svg = pie_labeled(
797            &["North", "South", "East", "West"],
798            &[25.0, 25.0, 25.0, 25.0],
799        )
800        .title("Pie — Equal Slices")
801        .size(400.0, 400.0)
802        .to_svg()?;
803        sections.push(("Pie — Equal", svg));
804    }
805
806    // 6e. Donut variants
807    {
808        let svg = pie_labeled(&["Pass", "Warn", "Fail"], &[60.0, 25.0, 15.0])
809            .donut(0.55)
810            .title("Donut — 55% hole")
811            .size(380.0, 380.0)
812            .to_svg()?;
813        sections.push(("Donut — 55%", svg));
814    }
815    {
816        let svg = pie_labeled(&["A", "B", "C", "D"], &[40.0, 30.0, 20.0, 10.0])
817            .donut(0.8)
818            .title("Donut — 80% hole (thin ring)")
819            .size(380.0, 380.0)
820            .to_svg()?;
821        sections.push(("Donut — 80% (thin)", svg));
822    }
823
824    // ═════════════════════════════════════════════════════════════════
825    //  SECTION 7: BOX PLOT VARIATIONS
826    // ═════════════════════════════════════════════════════════════════
827
828    // 7a. 3 groups
829    {
830        let mut cats = Vec::new();
831        let mut vals = Vec::new();
832        for (label, center, spread) in &[
833            ("Control", 50.0, 10.0),
834            ("Drug A", 65.0, 8.0),
835            ("Drug B", 70.0, 15.0),
836        ] {
837            for _ in 0..50 {
838                vals.push(center + rng.normal() * spread);
839                cats.push(*label);
840            }
841            vals.push(center + spread * 4.0); // outlier
842            cats.push(*label);
843        }
844        let svg = boxplot(&cats, &vals)
845            .title("Box Plot — Clinical Trial")
846            .x_label("Treatment")
847            .y_label("Response")
848            .size(500.0, 380.0)
849            .to_svg()?;
850        sections.push(("Box Plot — 3 Groups", svg));
851    }
852
853    // 7b. Many groups (7)
854    {
855        let mut cats = Vec::new();
856        let mut vals = Vec::new();
857        let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
858        for (i, day) in days.iter().enumerate() {
859            let center = 40.0 + (i as f64 * 0.9).sin() * 15.0;
860            let spread = 5.0 + i as f64 * 1.5;
861            for _ in 0..30 {
862                vals.push(center + rng.normal() * spread);
863                cats.push(*day);
864            }
865        }
866        let svg = boxplot(&cats, &vals)
867            .title("Box Plot — Daily Response Times")
868            .x_label("Day of Week")
869            .y_label("Time (ms)")
870            .size(600.0, 380.0)
871            .to_svg()?;
872        sections.push(("Box Plot — 7 Groups", svg));
873    }
874
875    // 7c. Box plot with tight distributions (minimal spread)
876    {
877        let mut cats = Vec::new();
878        let mut vals = Vec::new();
879        for (label, center) in &[("Batch 1", 100.0), ("Batch 2", 100.5), ("Batch 3", 99.8)] {
880            for _ in 0..40 {
881                vals.push(center + rng.normal() * 0.3);
882                cats.push(*label);
883            }
884        }
885        let svg = boxplot(&cats, &vals)
886            .title("Box Plot — Tight Distributions")
887            .x_label("Batch")
888            .y_label("Weight (g)")
889            .size(450.0, 350.0)
890            .to_svg()?;
891        sections.push(("Box Plot — Tight", svg));
892    }
893
894    // ═════════════════════════════════════════════════════════════════
895    //  SECTION 8: HEATMAP VARIATIONS
896    // ═════════════════════════════════════════════════════════════════
897
898    // 8a. Small heatmap with annotations
899    {
900        let data = vec![
901            vec![1.0, 5.0, 9.0],
902            vec![2.0, 6.0, 7.0],
903            vec![3.0, 4.0, 8.0],
904        ];
905        let svg = heatmap(data)
906            .annotate()
907            .with_row_labels(&["X", "Y", "Z"])
908            .with_col_labels(&["A", "B", "C"])
909            .title("Heatmap — 3×3 Annotated")
910            .x_label("Feature")
911            .y_label("Sample")
912            .size(350.0, 350.0)
913            .to_svg()?;
914        sections.push(("Heatmap — 3×3", svg));
915    }
916
917    // 8b. Large heatmap (8×8)
918    {
919        let data: Vec<Vec<f64>> = (0..8)
920            .map(|r| {
921                (0..8)
922                    .map(|c| {
923                        ((f64::from(r) * 0.5 + f64::from(c) * 0.3).sin() * 50.0 + 50.0).round()
924                    })
925                    .collect()
926            })
927            .collect();
928        let rows: Vec<String> = (0..8).map(|i| format!("Gene {}", i + 1)).collect();
929        let cols: Vec<String> = (0..8).map(|i| format!("Sample {}", i + 1)).collect();
930        let svg = heatmap(data)
931            .annotate()
932            .with_row_labels(&rows)
933            .with_col_labels(&cols)
934            .title("Heatmap — 8×8 Gene Expression")
935            .x_label("Sample")
936            .y_label("Gene")
937            .size(600.0, 500.0)
938            .to_svg()?;
939        sections.push(("Heatmap — 8×8", svg));
940    }
941
942    // 8c. Confusion matrix (4×4)
943    {
944        let data = vec![
945            vec![85.0, 3.0, 1.0, 2.0],
946            vec![2.0, 78.0, 5.0, 1.0],
947            vec![0.0, 4.0, 90.0, 3.0],
948            vec![1.0, 2.0, 3.0, 82.0],
949        ];
950        let labels: Vec<String> = vec!["Cat".into(), "Dog".into(), "Bird".into(), "Fish".into()];
951        let svg = heatmap(data)
952            .annotate()
953            .with_row_labels(&labels)
954            .with_col_labels(&labels)
955            .title("Confusion Matrix — 4 Classes")
956            .x_label("Predicted")
957            .y_label("Actual")
958            .size(450.0, 450.0)
959            .to_svg()?;
960        sections.push(("Heatmap — Confusion 4×4", svg));
961    }
962
963    // 8d. Heatmap with RdBu color scale
964    {
965        let data = vec![
966            vec![-1.0, -0.5, 0.0, 0.5, 1.0],
967            vec![0.5, -0.3, 0.8, -0.7, 0.2],
968            vec![0.1, 0.9, -0.8, 0.3, -0.4],
969        ];
970        let mut theme = NewTheme::light();
971        theme.color_scale = Some(ColorScale::rdbu());
972        let svg = heatmap(data)
973            .annotate()
974            .title("Heatmap — RdBu Diverging Scale")
975            .theme(theme)
976            .size(450.0, 350.0)
977            .to_svg()?;
978        sections.push(("Heatmap — RdBu", svg));
979    }
980
981    // 8e. Heatmap no annotations (just gradient)
982    {
983        let data: Vec<Vec<f64>> = (0..6)
984            .map(|r| (0..10).map(|c| f64::from(r * 10 + c)).collect())
985            .collect();
986        let svg = heatmap(data)
987            .title("Heatmap — 6×10 No Annotations")
988            .x_label("Time")
989            .y_label("Sensor")
990            .size(600.0, 400.0)
991            .to_svg()?;
992        sections.push(("Heatmap — Plain 6×10", svg));
993    }
994
995    // ═════════════════════════════════════════════════════════════════
996    //  SECTION 9: FACETED CHARTS
997    // ═════════════════════════════════════════════════════════════════
998
999    // 9a. Faceted scatter — 4 panels
1000    {
1001        let mut x = Vec::new();
1002        let mut y = Vec::new();
1003        let mut facets = Vec::new();
1004        for panel in &["Q1 2025", "Q2 2025", "Q3 2025", "Q4 2025"] {
1005            for _ in 0..30 {
1006                x.push(rng.range(0.0, 100.0));
1007                y.push(rng.range(0.0, 100.0));
1008                facets.push(*panel);
1009            }
1010        }
1011        let svg = scatter(&x, &y)
1012            .facet_wrap(&facets, 2)
1013            .title("Faceted Scatter — Quarterly")
1014            .x_label("Impressions")
1015            .y_label("Clicks")
1016            .size(600.0, 500.0)
1017            .to_svg()?;
1018        sections.push(("Facet — Scatter 2×2", svg));
1019    }
1020
1021    // 9b. Faceted scatter with categories + legend
1022    {
1023        let mut x = Vec::new();
1024        let mut y = Vec::new();
1025        let mut cats = Vec::new();
1026        let mut facets = Vec::new();
1027        for region in &["US", "EU", "APAC"] {
1028            for segment in &["Enterprise", "SMB", "Consumer"] {
1029                for _ in 0..10 {
1030                    x.push(rng.range(10.0, 90.0));
1031                    y.push(rng.range(10.0, 90.0));
1032                    cats.push(*segment);
1033                    facets.push(*region);
1034                }
1035            }
1036        }
1037        let chart = Chart::new()
1038            .layer(
1039                Layer::new(MarkType::Point)
1040                    .with_x(x)
1041                    .with_y(y)
1042                    .with_categories(cats.iter().map(|s| s.to_string()).collect())
1043                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1044            )
1045            .facet(Facet::Wrap { ncol: 3 })
1046            .title("Faceted — Region × Segment")
1047            .x_label("Deal Size ($K)")
1048            .y_label("Win Rate (%)")
1049            .size(700.0, 350.0);
1050        sections.push(("Facet — Categories+Legend", chart.to_svg()?));
1051    }
1052
1053    // 9c. Faceted scatter — FreeY scales
1054    {
1055        let mut x = Vec::new();
1056        let mut y = Vec::new();
1057        let mut facets = Vec::new();
1058        // Panel A: small y range
1059        for _ in 0..25 {
1060            x.push(rng.range(0.0, 10.0));
1061            y.push(rng.range(0.0, 5.0));
1062            facets.push("Small Range");
1063        }
1064        // Panel B: large y range
1065        for _ in 0..25 {
1066            x.push(rng.range(0.0, 10.0));
1067            y.push(rng.range(0.0, 500.0));
1068            facets.push("Large Range");
1069        }
1070        let chart = Chart::new()
1071            .layer(
1072                Layer::new(MarkType::Point)
1073                    .with_x(x)
1074                    .with_y(y)
1075                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1076            )
1077            .facet(Facet::Wrap { ncol: 2 })
1078            .facet_scales(FacetScales::FreeY)
1079            .title("Faceted — FreeY (different Y scales)")
1080            .x_label("X")
1081            .y_label("Y")
1082            .size(600.0, 350.0);
1083        sections.push(("Facet — FreeY", chart.to_svg()?));
1084    }
1085
1086    // 9d. Faceted with FreeX
1087    {
1088        let mut x = Vec::new();
1089        let mut y = Vec::new();
1090        let mut facets = Vec::new();
1091        for _ in 0..25 {
1092            x.push(rng.range(0.0, 10.0));
1093            y.push(rng.range(0.0, 50.0));
1094            facets.push("Narrow X");
1095        }
1096        for _ in 0..25 {
1097            x.push(rng.range(0.0, 1000.0));
1098            y.push(rng.range(0.0, 50.0));
1099            facets.push("Wide X");
1100        }
1101        let chart = Chart::new()
1102            .layer(
1103                Layer::new(MarkType::Point)
1104                    .with_x(x)
1105                    .with_y(y)
1106                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1107            )
1108            .facet(Facet::Wrap { ncol: 2 })
1109            .facet_scales(FacetScales::FreeX)
1110            .title("Faceted — FreeX (different X scales)")
1111            .x_label("X")
1112            .y_label("Y")
1113            .size(600.0, 350.0);
1114        sections.push(("Facet — FreeX", chart.to_svg()?));
1115    }
1116
1117    // 9e. Faceted with Free (both axes)
1118    {
1119        let mut x = Vec::new();
1120        let mut y = Vec::new();
1121        let mut facets = Vec::new();
1122        for _ in 0..20 {
1123            x.push(rng.range(0.0, 5.0));
1124            y.push(rng.range(0.0, 5.0));
1125            facets.push("Small");
1126        }
1127        for _ in 0..20 {
1128            x.push(rng.range(100.0, 200.0));
1129            y.push(rng.range(1000.0, 2000.0));
1130            facets.push("Large");
1131        }
1132        let chart = Chart::new()
1133            .layer(
1134                Layer::new(MarkType::Point)
1135                    .with_x(x)
1136                    .with_y(y)
1137                    .with_facet_values(facets.iter().map(|s| s.to_string()).collect()),
1138            )
1139            .facet(Facet::Wrap { ncol: 2 })
1140            .facet_scales(FacetScales::Free)
1141            .title("Faceted — Free (both axes independent)")
1142            .x_label("X")
1143            .y_label("Y")
1144            .size(600.0, 350.0);
1145        sections.push(("Facet — Free Both", chart.to_svg()?));
1146    }
1147
1148    // 9f. 6 panels (3 cols)
1149    {
1150        let mut x = Vec::new();
1151        let mut y = Vec::new();
1152        let mut facets = Vec::new();
1153        let panels = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"];
1154        for panel in panels {
1155            for _ in 0..15 {
1156                x.push(rng.range(0.0, 10.0));
1157                y.push(rng.range(0.0, 10.0));
1158                facets.push(panel);
1159            }
1160        }
1161        let svg = scatter(&x, &y)
1162            .facet_wrap(&facets, 3)
1163            .title("6 Panels (3 cols)")
1164            .x_label("X")
1165            .y_label("Y")
1166            .size(700.0, 500.0)
1167            .to_svg()?;
1168        sections.push(("Facet — 6 Panels", svg));
1169    }
1170
1171    // ═════════════════════════════════════════════════════════════════
1172    //  SECTION 10: THEME VARIATIONS
1173    // ═════════════════════════════════════════════════════════════════
1174
1175    // 10a. Dark theme — scatter
1176    {
1177        let x: Vec<f64> = (0..40).map(|_| rng.range(0.0, 100.0)).collect();
1178        let y: Vec<f64> = x.iter().map(|&xi| xi * 0.7 + rng.normal() * 10.0).collect();
1179        let svg = scatter(&x, &y)
1180            .title("Dark Theme — Scatter")
1181            .x_label("X")
1182            .y_label("Y")
1183            .theme(NewTheme::dark())
1184            .size(500.0, 380.0)
1185            .to_svg()?;
1186        sections.push(("Theme — Dark Scatter", svg));
1187    }
1188
1189    // 10b. Dark theme — multi-line
1190    {
1191        let x: Vec<f64> = (0..30).map(f64::from).collect();
1192        let chart = Chart::new()
1193            .layer(
1194                Layer::new(MarkType::Line)
1195                    .with_x(x.clone())
1196                    .with_y(x.iter().map(|&v| (v * 0.3).sin() * 5.0 + 10.0).collect())
1197                    .with_label("CPU"),
1198            )
1199            .layer(
1200                Layer::new(MarkType::Line)
1201                    .with_x(x.clone())
1202                    .with_y(x.iter().map(|&v| (v * 0.2).cos() * 4.0 + 8.0).collect())
1203                    .with_label("Memory"),
1204            )
1205            .layer(
1206                Layer::new(MarkType::Line)
1207                    .with_x(x.clone())
1208                    .with_y(x.iter().map(|&v| v * 0.3 + 2.0).collect())
1209                    .with_label("Disk"),
1210            )
1211            .title("Dark Theme — System Monitor")
1212            .x_label("Time (s)")
1213            .y_label("Usage (%)")
1214            .theme(NewTheme::dark())
1215            .size(550.0, 380.0);
1216        sections.push(("Theme — Dark Lines", chart.to_svg()?));
1217    }
1218
1219    // 10c. Dark theme — bar
1220    {
1221        let svg = bar(
1222            &["Mon", "Tue", "Wed", "Thu", "Fri"],
1223            &[120.0, 95.0, 150.0, 88.0, 110.0],
1224        )
1225        .title("Dark Theme — Bars")
1226        .x_label("Day")
1227        .y_label("Tickets Closed")
1228        .theme(NewTheme::dark())
1229        .size(500.0, 350.0)
1230        .to_svg()?;
1231        sections.push(("Theme — Dark Bar", svg));
1232    }
1233
1234    // 10d. Dark theme — area
1235    {
1236        let x: Vec<f64> = (0..30).map(f64::from).collect();
1237        let y: Vec<f64> = x
1238            .iter()
1239            .map(|&v| (v * 0.2).sin().abs() * 20.0 + 5.0)
1240            .collect();
1241        let svg = area(&x, &y)
1242            .title("Dark Theme — Area")
1243            .x_label("Day")
1244            .y_label("Events")
1245            .theme(NewTheme::dark())
1246            .size(500.0, 350.0)
1247            .to_svg()?;
1248        sections.push(("Theme — Dark Area", svg));
1249    }
1250
1251    // 10e. Publication theme — scatter
1252    {
1253        let x: Vec<f64> = (0..30).map(|_| rng.range(0.0, 10.0)).collect();
1254        let y: Vec<f64> = x.iter().map(|&xi| xi * 1.2 + rng.normal() * 1.5).collect();
1255        let svg = scatter(&x, &y)
1256            .title("Publication Theme")
1257            .x_label("Independent Variable")
1258            .y_label("Dependent Variable")
1259            .theme(NewTheme::publication())
1260            .size(500.0, 380.0)
1261            .to_svg()?;
1262        sections.push(("Theme — Publication", svg));
1263    }
1264
1265    // 10f. Custom theme — big fonts
1266    {
1267        let mut theme = NewTheme::light();
1268        theme.title_font_size = 22.0;
1269        theme.label_font_size = 16.0;
1270        theme.tick_font_size = 14.0;
1271        theme.line_width = 3.0;
1272        theme.point_size = 8.0;
1273        theme.grid_width = 1.5;
1274
1275        let x: Vec<f64> = (0..10).map(f64::from).collect();
1276        let y: Vec<f64> = vec![3.0, 5.0, 4.0, 7.0, 6.0, 9.0, 8.0, 10.0, 9.5, 12.0];
1277        let svg = scatter(&x, &y)
1278            .title("Custom Theme — Big Fonts")
1279            .x_label("Week")
1280            .y_label("Score")
1281            .theme(theme)
1282            .size(500.0, 400.0)
1283            .to_svg()?;
1284        sections.push(("Theme — Big Fonts", svg));
1285    }
1286
1287    // 10g. Custom theme — no grid
1288    {
1289        let mut theme = NewTheme::light();
1290        theme.show_grid = false;
1291
1292        let svg = bar(&["A", "B", "C", "D"], &[30.0, 45.0, 25.0, 55.0])
1293            .title("Custom — No Grid")
1294            .y_label("Value")
1295            .theme(theme)
1296            .size(400.0, 320.0)
1297            .to_svg()?;
1298        sections.push(("Theme — No Grid", svg));
1299    }
1300
1301    // 10h. Custom palette
1302    {
1303        let mut theme = NewTheme::light();
1304        theme.palette = Palette::diverging(
1305            Color::from_hex("#e74c3c").unwrap(),
1306            Color::from_hex("#f1c40f").unwrap(),
1307            Color::from_hex("#2ecc71").unwrap(),
1308            6,
1309        );
1310
1311        let mut x = Vec::new();
1312        let mut y = Vec::new();
1313        let mut cats = Vec::new();
1314        for (i, name) in ["Bad", "Poor", "Ok", "Good", "Great", "Excellent"]
1315            .iter()
1316            .enumerate()
1317        {
1318            for _ in 0..10 {
1319                x.push(i as f64 + rng.normal() * 0.3);
1320                y.push(rng.range(0.0, 10.0));
1321                cats.push(*name);
1322            }
1323        }
1324        let svg = scatter(&x, &y)
1325            .color_by(&cats)
1326            .title("Custom Diverging Palette")
1327            .x_label("Rating")
1328            .y_label("Score")
1329            .theme(theme)
1330            .size(550.0, 380.0)
1331            .to_svg()?;
1332        sections.push(("Theme — Custom Palette", svg));
1333    }
1334
1335    // ═════════════════════════════════════════════════════════════════
1336    //  SECTION 11: EDGE CASES & REALISTIC SCENARIOS
1337    // ═════════════════════════════════════════════════════════════════
1338
1339    // 11a. All negative values
1340    {
1341        let svg = bar(&["Loss A", "Loss B", "Loss C"], &[-15.0, -30.0, -22.0])
1342            .title("All Negative Values")
1343            .y_label("P&L ($K)")
1344            .size(400.0, 320.0)
1345            .to_svg()?;
1346        sections.push(("Edge — All Negative", svg));
1347    }
1348
1349    // 11b. Mixed positive/negative bar (non-stacked)
1350    {
1351        let svg = bar(
1352            &["Jan", "Feb", "Mar", "Apr", "May"],
1353            &[10.0, -5.0, 15.0, -3.0, 8.0],
1354        )
1355        .title("Mixed +/- Bar (no stack)")
1356        .y_label("Net Change")
1357        .size(450.0, 320.0)
1358        .to_svg()?;
1359        sections.push(("Edge — Mixed +/-", svg));
1360    }
1361
1362    // 11c. Very wide chart
1363    {
1364        let x: Vec<f64> = (0..100).map(f64::from).collect();
1365        let y: Vec<f64> = x.iter().map(|&v| (v * 0.1).sin() * 10.0 + 50.0).collect();
1366        let svg = line(&x, &y)
1367            .title("Very Wide Chart (900×200)")
1368            .x_label("Sample")
1369            .y_label("Value")
1370            .size(900.0, 200.0)
1371            .to_svg()?;
1372        sections.push(("Edge — Very Wide", svg));
1373    }
1374
1375    // 11d. Very tall chart
1376    {
1377        let svg = bar(&["A", "B", "C"], &[100.0, 200.0, 150.0])
1378            .title("Very Tall (300×600)")
1379            .y_label("Val")
1380            .size(300.0, 600.0)
1381            .to_svg()?;
1382        sections.push(("Edge — Very Tall", svg));
1383    }
1384
1385    // 11e. Tiny chart
1386    {
1387        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1388            .title("Tiny (200×150)")
1389            .size(200.0, 150.0)
1390            .to_svg()?;
1391        sections.push(("Edge — Tiny", svg));
1392    }
1393
1394    // 11f. Very long title
1395    {
1396        let svg = scatter(&[1.0, 2.0, 3.0], &[1.0, 4.0, 2.0])
1397            .title("This Is an Extremely Long Title That Might Overflow the Chart Boundary — How Does It Look?")
1398            .x_label("X Axis With A Somewhat Long Label Too")
1399            .y_label("Y Label")
1400            .size(500.0, 380.0)
1401            .to_svg()?;
1402        sections.push(("Edge — Long Title", svg));
1403    }
1404
1405    // 11g. Scatter with identical Y values (flat line)
1406    {
1407        let x: Vec<f64> = (0..10).map(f64::from).collect();
1408        let y = vec![5.0; 10];
1409        let svg = scatter(&x, &y)
1410            .title("Flat — All Y=5")
1411            .x_label("X")
1412            .y_label("Y")
1413            .size(400.0, 300.0)
1414            .to_svg()?;
1415        sections.push(("Edge — Flat Y", svg));
1416    }
1417
1418    // 11h. Aggregate stat — mean
1419    {
1420        let x = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0];
1421        let y = vec![10.0, 12.0, 11.0, 20.0, 22.0, 18.0, 15.0, 17.0, 16.0];
1422        let cats: Vec<String> = vec!["A", "A", "A", "B", "B", "B", "C", "C", "C"]
1423            .into_iter()
1424            .map(String::from)
1425            .collect();
1426        let chart = Chart::new()
1427            .layer(
1428                Layer::new(MarkType::Bar)
1429                    .with_x(x)
1430                    .with_y(y)
1431                    .with_categories(cats)
1432                    .stat(Stat::Aggregate {
1433                        func: AggregateFunc::Mean,
1434                    }),
1435            )
1436            .title("Aggregate — Mean per Category")
1437            .x_label("Category")
1438            .y_label("Mean Value")
1439            .size(450.0, 350.0);
1440        sections.push(("Stat — Aggregate Mean", chart.to_svg()?));
1441    }
1442
1443    // 11i. Fill position (100% stacked)
1444    {
1445        let chart = Chart::new()
1446            .layer(
1447                Layer::new(MarkType::Bar)
1448                    .with_x(vec![0.0, 1.0, 2.0])
1449                    .with_y(vec![30.0, 40.0, 25.0])
1450                    .with_label("TypeA")
1451                    .position(Position::Fill),
1452            )
1453            .layer(
1454                Layer::new(MarkType::Bar)
1455                    .with_x(vec![0.0, 1.0, 2.0])
1456                    .with_y(vec![70.0, 60.0, 75.0])
1457                    .with_label("TypeB")
1458                    .position(Position::Fill),
1459            )
1460            .title("100% Stacked (Fill Position)")
1461            .x_label("Group")
1462            .y_label("Proportion")
1463            .size(450.0, 350.0);
1464        sections.push(("Position — Fill", chart.to_svg()?));
1465    }
1466
1467    // 11j. Line + Area combo (confidence band look)
1468    {
1469        let x: Vec<f64> = (0..30).map(f64::from).collect();
1470        let y_main: Vec<f64> = x
1471            .iter()
1472            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0)
1473            .collect();
1474        let y_area: Vec<f64> = x
1475            .iter()
1476            .map(|&v| v * 0.5 + 10.0 + (v * 0.3).sin() * 3.0 + 5.0)
1477            .collect();
1478        let chart = Chart::new()
1479            .layer(
1480                Layer::new(MarkType::Area)
1481                    .with_x(x.clone())
1482                    .with_y(y_area)
1483                    .with_label("Upper Bound"),
1484            )
1485            .layer(
1486                Layer::new(MarkType::Line)
1487                    .with_x(x.clone())
1488                    .with_y(y_main.clone())
1489                    .with_label("Forecast"),
1490            )
1491            .layer(
1492                Layer::new(MarkType::Point)
1493                    .with_x(x)
1494                    .with_y(y_main)
1495                    .with_label("Data"),
1496            )
1497            .title("Forecast with Confidence Band")
1498            .x_label("Day")
1499            .y_label("Metric")
1500            .size(550.0, 380.0);
1501        sections.push(("Combo — Area+Line+Point", chart.to_svg()?));
1502    }
1503
1504    // ═════════════════════════════════════════════════════════════════
1505    //  BUILD HTML
1506    // ═════════════════════════════════════════════════════════════════
1507
1508    let mut html = String::from(
1509        r#"<!DOCTYPE html>
1510<html lang="en">
1511<head>
1512<meta charset="UTF-8">
1513<meta name="viewport" content="width=device-width, initial-scale=1.0">
1514<title>esoc-chart Stress Test</title>
1515<style>
1516  * { margin: 0; padding: 0; box-sizing: border-box; }
1517  body { font-family: system-ui, -apple-system, sans-serif; background: #f0f0f4; color: #333; }
1518  header { background: linear-gradient(135deg, #2d1b69, #11998e); color: white; padding: 2.5rem 2rem; text-align: center; }
1519  header h1 { font-size: 2rem; font-weight: 300; letter-spacing: 0.02em; }
1520  header p { margin-top: 0.5rem; opacity: 0.7; font-size: 0.95rem; }
1521  .stats { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; flex-wrap: wrap; }
1522  .stats span { background: rgba(255,255,255,0.15); padding: 0.3rem 0.8rem; border-radius: 4px; font-size: 0.85rem; }
1523  .section-title { font-size: 1.2rem; font-weight: 600; color: #555; padding: 1.5rem 2rem 0.5rem; max-width: 1600px; margin: 0 auto; border-top: 2px solid #ddd; margin-top: 1rem; }
1524  .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(480px, 1fr)); gap: 1.5rem; padding: 1rem 2rem; max-width: 1600px; margin: 0 auto; }
1525  .card { background: white; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.06); overflow: hidden; transition: box-shadow 0.2s; }
1526  .card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.12); }
1527  .card h2 { font-size: 0.85rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #555; padding: 1rem 1.5rem 0; }
1528  .card .chart-wrap { padding: 0.5rem 1rem 0.75rem; }
1529  .card svg { display: block; width: 100%; height: auto; }
1530  .card.dark-bg .chart-wrap { background: #1e1e2e; border-radius: 0 0 8px 8px; }
1531  .feedback { padding: 0 1rem 1rem; }
1532  .feedback textarea { width: 100%; min-height: 50px; border: 1px solid #e0e0e0; border-radius: 4px; padding: 0.5rem; font-family: inherit; font-size: 0.82rem; resize: vertical; }
1533  .feedback textarea:focus { outline: none; border-color: #2d1b69; }
1534  .feedback .status { font-size: 0.72rem; color: #aaa; margin-top: 0.2rem; }
1535  .actions { padding: 1.5rem 2rem; text-align: center; }
1536  .actions button { background: #2d1b69; color: white; border: none; border-radius: 4px; padding: 0.6rem 1.5rem; font-size: 0.9rem; cursor: pointer; margin: 0 0.5rem; }
1537  .actions button:hover { background: #3d2b79; }
1538</style>
1539<script>
1540  const feedback = {};
1541  function loadFeedback() {
1542    try { Object.assign(feedback, JSON.parse(localStorage.getItem('stress_test_feedback') || '{}')); } catch {}
1543    document.querySelectorAll('.feedback textarea').forEach(ta => {
1544      const key = ta.dataset.chart;
1545      if (feedback[key]) ta.value = feedback[key];
1546    });
1547  }
1548  function saveFeedback(key, value) {
1549    feedback[key] = value;
1550    localStorage.setItem('stress_test_feedback', JSON.stringify(feedback));
1551  }
1552  function exportFeedback() {
1553    const blob = new Blob([JSON.stringify(feedback, null, 2)], {type: 'application/json'});
1554    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
1555    a.download = 'stress_test_feedback.json'; a.click();
1556  }
1557  window.addEventListener('DOMContentLoaded', loadFeedback);
1558</script>
1559</head>
1560<body>
1561<header>
1562  <h1>esoc-chart Stress Test</h1>
1563  <p>Comprehensive exercise of every chart type, option, theme, position, stat, and edge case</p>
1564  <div class="stats">
1565"#,
1566    );
1567
1568    writeln!(html, "    <span>{} charts</span>", sections.len()).unwrap();
1569    html.push_str("    <span>11 categories</span>\n");
1570    html.push_str("    <span>3 themes</span>\n");
1571    html.push_str("    <span>5 positions</span>\n");
1572    html.push_str("    <span>4 facet scale modes</span>\n");
1573    html.push_str("  </div>\n</header>\n");
1574
1575    for (title, svg) in &sections {
1576        let key = title
1577            .to_lowercase()
1578            .replace([' ', '–', '—', '+', '/', '(', ')', '%'], "_")
1579            .replace("__", "_");
1580        let dark_class = if title.contains("Dark") {
1581            " dark-bg"
1582        } else {
1583            ""
1584        };
1585        html.push_str("<div class=\"grid\">\n");
1586        write!(
1587            html,
1588            concat!(
1589                "<div class=\"card{dark_class}\">\n",
1590                "  <h2>{title}</h2>\n",
1591                "  <div class=\"chart-wrap\">{svg}</div>\n",
1592                "  <div class=\"feedback\">\n",
1593                "    <textarea data-chart=\"{key}\" placeholder=\"Notes on {title}…\" ",
1594                "oninput=\"saveFeedback('{key}', this.value)\"></textarea>\n",
1595                "    <div class=\"status\">Auto-saved</div>\n",
1596                "  </div>\n",
1597                "</div>\n",
1598            ),
1599            title = title,
1600            svg = svg,
1601            key = key,
1602            dark_class = dark_class,
1603        )
1604        .unwrap();
1605        html.push_str("</div>\n");
1606    }
1607
1608    html.push_str(concat!(
1609        "<div class=\"actions\">\n",
1610        "  <button onclick=\"exportFeedback()\">Export Feedback JSON</button>\n",
1611        "</div>\n",
1612        "</body>\n</html>\n",
1613    ));
1614
1615    let out_path = "stress_test.html";
1616    std::fs::write(out_path, &html).expect("failed to write HTML");
1617    println!("Saved {} ({} charts)", out_path, sections.len());
1618
1619    Ok(())
1620}
Source

pub fn build(self) -> Chart

Build the chart.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.