Skip to main content

boxplot

Function boxplot 

Source
pub fn boxplot(categories: &[impl ToString], values: &[f64]) -> BoxPlotBuilder
Expand description

Create a box plot.

Examples found in repository?
examples/p1_showcase.rs (line 181)
18fn main() -> esoc_chart::error::Result<()> {
19    // ── Simple LCG for reproducibility ────────────────────────────────
20    let mut seed: u64 = 42;
21    let mut rng = || -> f64 {
22        seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
23        (seed >> 11) as f64 / (1u64 << 53) as f64
24    };
25    let mut normal = || -> f64 {
26        let u1 = rng().max(1e-15);
27        let u2 = rng();
28        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
29    };
30
31    // ── 1. Dense scatter (opacity demo) ───────────────────────────────
32    let n = 500;
33    let x: Vec<f64> = (0..n).map(|_| normal() * 3.0 + 5.0).collect();
34    let y: Vec<f64> = x.iter().map(|&xi| xi * 0.8 + normal() * 2.0).collect();
35
36    let svg = scatter(&x, &y)
37        .title("Dense Scatter — Opacity & Point Sizing")
38        .x_label("feature A")
39        .y_label("feature B")
40        .size(700.0, 500.0)
41        .to_svg()?;
42    std::fs::write("dense_scatter.svg", &svg)?;
43    println!("Saved dense_scatter.svg");
44
45    // ── 2. Histogram (bins touching) ──────────────────────────────────
46    let hist_data: Vec<f64> = (0..400).map(|_| normal() * 1.5 + 10.0).collect();
47
48    let svg = histogram(&hist_data)
49        .bins(30)
50        .title("Normal Distribution — Tight Bins")
51        .x_label("value")
52        .y_label("count")
53        .size(700.0, 450.0)
54        .to_svg()?;
55    std::fs::write("hist_tight_bins.svg", &svg)?;
56    println!("Saved hist_tight_bins.svg");
57
58    // ── 3. Bar chart (horizontal-only gridlines) ──────────────────────
59    let langs = [
60        "Rust",
61        "Python",
62        "TypeScript",
63        "Go",
64        "Java",
65        "C++",
66        "Ruby",
67        "Swift",
68    ];
69    let users: Vec<f64> = vec![
70        85_000.0,
71        1_200_000.0,
72        950_000.0,
73        420_000.0,
74        780_000.0,
75        650_000.0,
76        180_000.0,
77        310_000.0,
78    ];
79
80    let svg = bar(&langs, &users)
81        .title("Language Users (thousands)")
82        .size(700.0, 450.0)
83        .to_svg()?;
84    std::fs::write("bar_large_values.svg", &svg)?;
85    println!("Saved bar_large_values.svg");
86
87    // ── 4. Area chart ─────────────────────────────────────────────────
88    let x_area: Vec<f64> = (0..60).map(|i| f64::from(i) * 0.5).collect();
89    let y_area: Vec<f64> = x_area
90        .iter()
91        .map(|&xi| (xi * 0.3).sin() * 20.0 + 25.0 + (xi * 0.1).cos() * 5.0)
92        .collect();
93
94    let svg = area(&x_area, &y_area)
95        .title("Server Load Over Time")
96        .x_label("minutes")
97        .y_label("requests / sec")
98        .size(700.0, 400.0)
99        .to_svg()?;
100    std::fs::write("area_chart.svg", &svg)?;
101    println!("Saved area_chart.svg");
102
103    // ── 5. Pie chart ──────────────────────────────────────────────────
104    let pie_vals = [35.0, 25.0, 20.0, 12.0, 8.0];
105    let pie_labels = ["Chrome", "Safari", "Firefox", "Edge", "Other"];
106
107    let svg = pie_labeled(&pie_labels, &pie_vals)
108        .title("Browser Market Share")
109        .size(500.0, 500.0)
110        .to_svg()?;
111    std::fs::write("pie_chart.svg", &svg)?;
112    println!("Saved pie_chart.svg");
113
114    // ── 6. Donut chart ────────────────────────────────────────────────
115    let svg = pie_labeled(&pie_labels, &pie_vals)
116        .donut(0.5)
117        .title("Browser Share (Donut)")
118        .size(500.0, 500.0)
119        .to_svg()?;
120    std::fs::write("donut_chart.svg", &svg)?;
121    println!("Saved donut_chart.svg");
122
123    // ── 7. Stacked bar ───────────────────────────────────────────────
124    let stack_cats = ["Q1", "Q2", "Q3", "Q4"];
125    let stack_groups = [
126        "Product A",
127        "Product A",
128        "Product A",
129        "Product A",
130        "Product B",
131        "Product B",
132        "Product B",
133        "Product B",
134        "Product C",
135        "Product C",
136        "Product C",
137        "Product C",
138    ];
139    let stack_vals = [
140        30.0, 45.0, 55.0, 40.0, // Product A
141        20.0, 25.0, 30.0, 35.0, // Product B
142        15.0, 10.0, 20.0, 25.0, // Product C
143    ];
144    // stacked_bar expects (categories, groups, values) where each row is (cat, group, value)
145    let cats_expanded: Vec<&str> = stack_cats.iter().copied().cycle().take(12).collect();
146    // Groups need to match the value ordering
147    let svg = stacked_bar(&cats_expanded, &stack_groups, &stack_vals)
148        .title("Quarterly Revenue by Product")
149        .x_label("Quarter")
150        .y_label("Revenue ($M)")
151        .size(700.0, 450.0)
152        .to_svg()?;
153    std::fs::write("stacked_bar.svg", &svg)?;
154    println!("Saved stacked_bar.svg");
155
156    // ── 8. Grouped bar ───────────────────────────────────────────────
157    let svg = grouped_bar(&cats_expanded, &stack_groups, &stack_vals)
158        .title("Quarterly Revenue — Grouped")
159        .x_label("Quarter")
160        .y_label("Revenue ($M)")
161        .size(700.0, 450.0)
162        .to_svg()?;
163    std::fs::write("grouped_bar.svg", &svg)?;
164    println!("Saved grouped_bar.svg");
165
166    // ── 9. Boxplot via v2 API ────────────────────────────────────────
167    let mut box_cats = Vec::new();
168    let mut box_vals = Vec::new();
169    for label in ["Setosa", "Versicolor", "Virginica"] {
170        let center = match label {
171            "Setosa" => 1.5,
172            "Versicolor" => 4.3,
173            _ => 5.8,
174        };
175        for _ in 0..60 {
176            box_cats.push(label);
177            box_vals.push(center + normal() * 0.5);
178        }
179    }
180
181    let svg = boxplot(&box_cats, &box_vals)
182        .title("Petal Length by Species")
183        .x_label("Species")
184        .y_label("Petal Length (cm)")
185        .size(600.0, 450.0)
186        .to_svg()?;
187    std::fs::write("boxplot_v2.svg", &svg)?;
188    println!("Saved boxplot_v2.svg");
189
190    // ── 10. Scatter with subtitle & caption (font hierarchy demo) ────
191    let x_sm: Vec<f64> = (0..30).map(f64::from).collect();
192    let y_sm: Vec<f64> = x_sm.iter().map(|&xi| xi.sqrt() * 3.0 + normal()).collect();
193
194    let chart = Chart::new()
195        .layer(Layer::new(MarkType::Point).with_x(x_sm).with_y(y_sm))
196        .title("Growth Trend Analysis")
197        .subtitle("Subtitle uses muted color and smaller font")
198        .caption("Source: synthetic data")
199        .x_label("Day")
200        .y_label("Value")
201        .size(700.0, 500.0);
202
203    let svg = chart.to_svg()?;
204    std::fs::write("font_hierarchy.svg", &svg)?;
205    println!("Saved font_hierarchy.svg");
206
207    // ── 11. Categorical scatter with many points (opacity per category) ─
208    let mut cx = Vec::new();
209    let mut cy = Vec::new();
210    let mut cc = Vec::new();
211    for (label, cx_off, cy_off) in [
212        ("Group A", 0.0, 0.0),
213        ("Group B", 5.0, 3.0),
214        ("Group C", 2.5, 6.0),
215    ] {
216        for _ in 0..150 {
217            cx.push(cx_off + normal() * 1.2);
218            cy.push(cy_off + normal() * 1.2);
219            cc.push(label);
220        }
221    }
222
223    let svg = scatter(&cx, &cy)
224        .color_by(&cc)
225        .title("Dense Categorical Scatter")
226        .x_label("x")
227        .y_label("y")
228        .size(700.0, 500.0)
229        .to_svg()?;
230    std::fs::write("dense_categorical.svg", &svg)?;
231    println!("Saved dense_categorical.svg");
232
233    // ── 12. Multi-line with grammar API (dark theme) ──────────────────
234    let epochs: Vec<f64> = (1..=40).map(f64::from).collect();
235    let loss1: Vec<f64> = epochs
236        .iter()
237        .map(|&e| 2.5 * (-e / 10.0).exp() + 0.1 + normal() * 0.02)
238        .collect();
239    let loss2: Vec<f64> = epochs
240        .iter()
241        .map(|&e| 2.0 * (-e / 15.0).exp() + 0.15 + normal() * 0.03)
242        .collect();
243
244    let chart = Chart::new()
245        .layer(
246            Layer::new(MarkType::Line)
247                .with_x(epochs.clone())
248                .with_y(loss1),
249        )
250        .layer(Layer::new(MarkType::Line).with_x(epochs).with_y(loss2))
251        .title("Model Comparison — Dark Theme")
252        .subtitle("Lower is better")
253        .x_label("Epoch")
254        .y_label("Loss")
255        .theme(NewTheme::dark())
256        .size(700.0, 450.0);
257
258    let svg = chart.to_svg()?;
259    std::fs::write("dark_theme.svg", &svg)?;
260    println!("Saved dark_theme.svg");
261
262    println!("\nAll P1 showcase charts generated!");
263    Ok(())
264}
More examples
Hide additional examples
examples/readme_charts.rs (line 218)
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 313)
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 435)
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 844)
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}