pub struct HistogramBuilder { /* private fields */ }Expand description
Builder for histograms.
Implementations§
Source§impl HistogramBuilder
impl HistogramBuilder
Sourcepub fn title(self, title: impl Into<String>) -> Self
pub fn title(self, title: impl Into<String>) -> Self
Set title.
Examples found in repository?
examples/audit_edge_cases.rs (line 190)
22fn main() {
23 let mut sections: Vec<(String, String)> = Vec::new();
24
25 // 1. Single data point
26 if let Some(s) = try_chart("Single Point", || {
27 scatter(&[5.0], &[10.0])
28 .title("Single Point")
29 .size(400.0, 300.0)
30 .to_svg()
31 }) {
32 sections.push(s);
33 }
34
35 // 2. Two points
36 if let Some(s) = try_chart("Two Points", || {
37 scatter(&[0.0, 100.0], &[0.0, 100.0])
38 .title("Two Points")
39 .size(400.0, 300.0)
40 .to_svg()
41 }) {
42 sections.push(s);
43 }
44
45 // 3. Very long category labels
46 if let Some(s) = try_chart("Long Labels", || {
47 let cats = vec![
48 "This is an extremely long category label",
49 "Another very very long label here",
50 "Short",
51 "Medium length label",
52 "Yet another verbose category name",
53 ];
54 bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55 .title("Long Category Labels")
56 .x_label("Categories")
57 .y_label("Value")
58 .size(600.0, 400.0)
59 .to_svg()
60 }) {
61 sections.push(s);
62 }
63
64 // 4. Large numeric values (millions)
65 if let Some(s) = try_chart("Large Numbers", || {
66 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67 let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68 scatter(&x, &y)
69 .title("Large Numbers (Millions)")
70 .x_label("Revenue ($)")
71 .y_label("Profit ($)")
72 .size(600.0, 350.0)
73 .to_svg()
74 }) {
75 sections.push(s);
76 }
77
78 // 5. Very small numbers
79 if let Some(s) = try_chart("Small Numbers", || {
80 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81 let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82 scatter(&x, &y)
83 .title("Small Numbers")
84 .size(600.0, 350.0)
85 .to_svg()
86 }) {
87 sections.push(s);
88 }
89
90 // 6. All-negative bars
91 if let Some(s) = try_chart("Negative Bars", || {
92 bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93 .title("All-Negative Bars")
94 .y_label("Loss")
95 .size(500.0, 350.0)
96 .to_svg()
97 }) {
98 sections.push(s);
99 }
100
101 // 7. Mixed +/- bars
102 if let Some(s) = try_chart("Mixed Bars", || {
103 bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104 .title("Mixed +/- Bars")
105 .y_label("P&L")
106 .size(500.0, 350.0)
107 .to_svg()
108 }) {
109 sections.push(s);
110 }
111
112 // 8. Dense scatter (1000 points)
113 if let Some(s) = try_chart("Dense Scatter", || {
114 struct Rng(u64);
115 impl Rng {
116 fn next(&mut self) -> f64 {
117 self.0 = self
118 .0
119 .wrapping_mul(6_364_136_223_846_793_005)
120 .wrapping_add(1);
121 (self.0 >> 11) as f64 / (1u64 << 53) as f64
122 }
123 }
124 let mut rng = Rng(123);
125 let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126 let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127 scatter(&x, &y)
128 .title("Dense Scatter (n=1000)")
129 .size(500.0, 400.0)
130 .to_svg()
131 }) {
132 sections.push(s);
133 }
134
135 // 9. Single bar
136 if let Some(s) = try_chart("Single Bar", || {
137 bar(&["Only One"], &[42.0])
138 .title("Single Bar")
139 .size(500.0, 350.0)
140 .to_svg()
141 }) {
142 sections.push(s);
143 }
144
145 // 10. Many categories (20 bars)
146 if let Some(s) = try_chart("Many Categories", || {
147 let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148 let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149 let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150 bar(&cats_ref, &vals)
151 .title("Many Categories (20)")
152 .size(700.0, 400.0)
153 .to_svg()
154 }) {
155 sections.push(s);
156 }
157
158 // 11. Narrow chart
159 if let Some(s) = try_chart("Narrow Chart", || {
160 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162 line(&x, &y)
163 .title("Narrow Chart")
164 .x_label("X")
165 .y_label("Y")
166 .size(300.0, 400.0)
167 .to_svg()
168 }) {
169 sections.push(s);
170 }
171
172 // 12. Wide chart
173 if let Some(s) = try_chart("Wide Chart", || {
174 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176 line(&x, &y)
177 .title("Wide Chart")
178 .x_label("X")
179 .y_label("Y")
180 .size(1000.0, 250.0)
181 .to_svg()
182 }) {
183 sections.push(s);
184 }
185
186 // 13. Histogram (few bins)
187 if let Some(s) = try_chart("Few Bins", || {
188 histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189 .bins(3)
190 .title("Histogram (3 bins)")
191 .size(400.0, 300.0)
192 .to_svg()
193 }) {
194 sections.push(s);
195 }
196
197 // 14. Dark theme scatter
198 if let Some(s) = try_chart("Dark Scatter", || {
199 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200 let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201 let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202 scatter(&x, &y)
203 .color_by(&cats)
204 .title("Dark Theme Scatter")
205 .x_label("X")
206 .y_label("Y")
207 .theme(NewTheme::dark())
208 .size(500.0, 350.0)
209 .to_svg()
210 }) {
211 sections.push(s);
212 }
213
214 // 15. Dark theme bars
215 if let Some(s) = try_chart("Dark Bars", || {
216 bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217 .title("Dark Theme Bars")
218 .theme(NewTheme::dark())
219 .size(500.0, 350.0)
220 .to_svg()
221 }) {
222 sections.push(s);
223 }
224
225 // 16. Flat line (constant y)
226 if let Some(s) = try_chart("Flat Line", || {
227 let x: Vec<f64> = (0..10).map(f64::from).collect();
228 line(&x, &[5.0; 10])
229 .title("Constant Y")
230 .size(400.0, 300.0)
231 .to_svg()
232 }) {
233 sections.push(s);
234 }
235
236 // 17. Pie with many small slices
237 if let Some(s) = try_chart("Many Pie Slices", || {
238 let mut vals = vec![50.0, 30.0, 15.0];
239 vals.extend(std::iter::repeat_n(0.5, 10));
240 let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241 let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242 pie_labeled(&labels_ref, &vals)
243 .title("Pie: Many Slices")
244 .size(500.0, 400.0)
245 .to_svg()
246 }) {
247 sections.push(s);
248 }
249
250 // 18. Heatmap
251 if let Some(s) = try_chart("Heatmap", || {
252 let data = vec![
253 vec![1.0, 2.0, 3.0, 4.0],
254 vec![5.0, 6.0, 7.0, 8.0],
255 vec![9.0, 10.0, 11.0, 12.0],
256 ];
257 heatmap(data)
258 .title("Basic Heatmap")
259 .with_row_labels(&["R1", "R2", "R3"])
260 .with_col_labels(&["C1", "C2", "C3", "C4"])
261 .size(500.0, 350.0)
262 .to_svg()
263 }) {
264 sections.push(s);
265 }
266
267 // ── Write HTML ─────────────────────────────────────────────────────
268 let mut html = String::from(
269 r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274 box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279 );
280 for (title, svg) in §ions {
281 writeln!(
282 html,
283 "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284 )
285 .unwrap();
286 }
287 html.push_str("</body></html>");
288 std::fs::write("audit_edge_cases.html", &html).unwrap();
289 println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}More examples
examples/p1_showcase.rs (line 50)
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}examples/readme_charts.rs (line 161)
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 219)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 330)
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 & 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 §ions {
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 583)
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 §ions {
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}Sourcepub fn x_label(self, label: impl Into<String>) -> Self
pub fn x_label(self, label: impl Into<String>) -> Self
Set X-axis label.
Examples found in repository?
examples/p1_showcase.rs (line 51)
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
examples/readme_charts.rs (line 162)
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 220)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 331)
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 & 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 §ions {
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 584)
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 §ions {
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}Sourcepub fn y_label(self, label: impl Into<String>) -> Self
pub fn y_label(self, label: impl Into<String>) -> Self
Set Y-axis label.
Examples found in repository?
examples/p1_showcase.rs (line 52)
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
examples/readme_charts.rs (line 163)
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 221)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 332)
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 & 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 §ions {
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 585)
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 §ions {
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}Sourcepub fn size(self, width: f32, height: f32) -> Self
pub fn size(self, width: f32, height: f32) -> Self
Set dimensions.
Examples found in repository?
examples/audit_edge_cases.rs (line 191)
22fn main() {
23 let mut sections: Vec<(String, String)> = Vec::new();
24
25 // 1. Single data point
26 if let Some(s) = try_chart("Single Point", || {
27 scatter(&[5.0], &[10.0])
28 .title("Single Point")
29 .size(400.0, 300.0)
30 .to_svg()
31 }) {
32 sections.push(s);
33 }
34
35 // 2. Two points
36 if let Some(s) = try_chart("Two Points", || {
37 scatter(&[0.0, 100.0], &[0.0, 100.0])
38 .title("Two Points")
39 .size(400.0, 300.0)
40 .to_svg()
41 }) {
42 sections.push(s);
43 }
44
45 // 3. Very long category labels
46 if let Some(s) = try_chart("Long Labels", || {
47 let cats = vec![
48 "This is an extremely long category label",
49 "Another very very long label here",
50 "Short",
51 "Medium length label",
52 "Yet another verbose category name",
53 ];
54 bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55 .title("Long Category Labels")
56 .x_label("Categories")
57 .y_label("Value")
58 .size(600.0, 400.0)
59 .to_svg()
60 }) {
61 sections.push(s);
62 }
63
64 // 4. Large numeric values (millions)
65 if let Some(s) = try_chart("Large Numbers", || {
66 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67 let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68 scatter(&x, &y)
69 .title("Large Numbers (Millions)")
70 .x_label("Revenue ($)")
71 .y_label("Profit ($)")
72 .size(600.0, 350.0)
73 .to_svg()
74 }) {
75 sections.push(s);
76 }
77
78 // 5. Very small numbers
79 if let Some(s) = try_chart("Small Numbers", || {
80 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81 let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82 scatter(&x, &y)
83 .title("Small Numbers")
84 .size(600.0, 350.0)
85 .to_svg()
86 }) {
87 sections.push(s);
88 }
89
90 // 6. All-negative bars
91 if let Some(s) = try_chart("Negative Bars", || {
92 bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93 .title("All-Negative Bars")
94 .y_label("Loss")
95 .size(500.0, 350.0)
96 .to_svg()
97 }) {
98 sections.push(s);
99 }
100
101 // 7. Mixed +/- bars
102 if let Some(s) = try_chart("Mixed Bars", || {
103 bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104 .title("Mixed +/- Bars")
105 .y_label("P&L")
106 .size(500.0, 350.0)
107 .to_svg()
108 }) {
109 sections.push(s);
110 }
111
112 // 8. Dense scatter (1000 points)
113 if let Some(s) = try_chart("Dense Scatter", || {
114 struct Rng(u64);
115 impl Rng {
116 fn next(&mut self) -> f64 {
117 self.0 = self
118 .0
119 .wrapping_mul(6_364_136_223_846_793_005)
120 .wrapping_add(1);
121 (self.0 >> 11) as f64 / (1u64 << 53) as f64
122 }
123 }
124 let mut rng = Rng(123);
125 let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126 let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127 scatter(&x, &y)
128 .title("Dense Scatter (n=1000)")
129 .size(500.0, 400.0)
130 .to_svg()
131 }) {
132 sections.push(s);
133 }
134
135 // 9. Single bar
136 if let Some(s) = try_chart("Single Bar", || {
137 bar(&["Only One"], &[42.0])
138 .title("Single Bar")
139 .size(500.0, 350.0)
140 .to_svg()
141 }) {
142 sections.push(s);
143 }
144
145 // 10. Many categories (20 bars)
146 if let Some(s) = try_chart("Many Categories", || {
147 let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148 let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149 let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150 bar(&cats_ref, &vals)
151 .title("Many Categories (20)")
152 .size(700.0, 400.0)
153 .to_svg()
154 }) {
155 sections.push(s);
156 }
157
158 // 11. Narrow chart
159 if let Some(s) = try_chart("Narrow Chart", || {
160 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162 line(&x, &y)
163 .title("Narrow Chart")
164 .x_label("X")
165 .y_label("Y")
166 .size(300.0, 400.0)
167 .to_svg()
168 }) {
169 sections.push(s);
170 }
171
172 // 12. Wide chart
173 if let Some(s) = try_chart("Wide Chart", || {
174 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176 line(&x, &y)
177 .title("Wide Chart")
178 .x_label("X")
179 .y_label("Y")
180 .size(1000.0, 250.0)
181 .to_svg()
182 }) {
183 sections.push(s);
184 }
185
186 // 13. Histogram (few bins)
187 if let Some(s) = try_chart("Few Bins", || {
188 histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189 .bins(3)
190 .title("Histogram (3 bins)")
191 .size(400.0, 300.0)
192 .to_svg()
193 }) {
194 sections.push(s);
195 }
196
197 // 14. Dark theme scatter
198 if let Some(s) = try_chart("Dark Scatter", || {
199 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200 let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201 let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202 scatter(&x, &y)
203 .color_by(&cats)
204 .title("Dark Theme Scatter")
205 .x_label("X")
206 .y_label("Y")
207 .theme(NewTheme::dark())
208 .size(500.0, 350.0)
209 .to_svg()
210 }) {
211 sections.push(s);
212 }
213
214 // 15. Dark theme bars
215 if let Some(s) = try_chart("Dark Bars", || {
216 bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217 .title("Dark Theme Bars")
218 .theme(NewTheme::dark())
219 .size(500.0, 350.0)
220 .to_svg()
221 }) {
222 sections.push(s);
223 }
224
225 // 16. Flat line (constant y)
226 if let Some(s) = try_chart("Flat Line", || {
227 let x: Vec<f64> = (0..10).map(f64::from).collect();
228 line(&x, &[5.0; 10])
229 .title("Constant Y")
230 .size(400.0, 300.0)
231 .to_svg()
232 }) {
233 sections.push(s);
234 }
235
236 // 17. Pie with many small slices
237 if let Some(s) = try_chart("Many Pie Slices", || {
238 let mut vals = vec![50.0, 30.0, 15.0];
239 vals.extend(std::iter::repeat_n(0.5, 10));
240 let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241 let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242 pie_labeled(&labels_ref, &vals)
243 .title("Pie: Many Slices")
244 .size(500.0, 400.0)
245 .to_svg()
246 }) {
247 sections.push(s);
248 }
249
250 // 18. Heatmap
251 if let Some(s) = try_chart("Heatmap", || {
252 let data = vec![
253 vec![1.0, 2.0, 3.0, 4.0],
254 vec![5.0, 6.0, 7.0, 8.0],
255 vec![9.0, 10.0, 11.0, 12.0],
256 ];
257 heatmap(data)
258 .title("Basic Heatmap")
259 .with_row_labels(&["R1", "R2", "R3"])
260 .with_col_labels(&["C1", "C2", "C3", "C4"])
261 .size(500.0, 350.0)
262 .to_svg()
263 }) {
264 sections.push(s);
265 }
266
267 // ── Write HTML ─────────────────────────────────────────────────────
268 let mut html = String::from(
269 r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274 box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279 );
280 for (title, svg) in §ions {
281 writeln!(
282 html,
283 "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284 )
285 .unwrap();
286 }
287 html.push_str("</body></html>");
288 std::fs::write("audit_edge_cases.html", &html).unwrap();
289 println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}More examples
examples/p1_showcase.rs (line 53)
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}examples/readme_charts.rs (line 164)
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 222)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 333)
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 & 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 §ions {
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 586)
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 §ions {
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}Sourcepub fn to_svg(self) -> Result<String>
pub fn to_svg(self) -> Result<String>
Build and render to SVG.
Examples found in repository?
examples/audit_edge_cases.rs (line 192)
22fn main() {
23 let mut sections: Vec<(String, String)> = Vec::new();
24
25 // 1. Single data point
26 if let Some(s) = try_chart("Single Point", || {
27 scatter(&[5.0], &[10.0])
28 .title("Single Point")
29 .size(400.0, 300.0)
30 .to_svg()
31 }) {
32 sections.push(s);
33 }
34
35 // 2. Two points
36 if let Some(s) = try_chart("Two Points", || {
37 scatter(&[0.0, 100.0], &[0.0, 100.0])
38 .title("Two Points")
39 .size(400.0, 300.0)
40 .to_svg()
41 }) {
42 sections.push(s);
43 }
44
45 // 3. Very long category labels
46 if let Some(s) = try_chart("Long Labels", || {
47 let cats = vec![
48 "This is an extremely long category label",
49 "Another very very long label here",
50 "Short",
51 "Medium length label",
52 "Yet another verbose category name",
53 ];
54 bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55 .title("Long Category Labels")
56 .x_label("Categories")
57 .y_label("Value")
58 .size(600.0, 400.0)
59 .to_svg()
60 }) {
61 sections.push(s);
62 }
63
64 // 4. Large numeric values (millions)
65 if let Some(s) = try_chart("Large Numbers", || {
66 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67 let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68 scatter(&x, &y)
69 .title("Large Numbers (Millions)")
70 .x_label("Revenue ($)")
71 .y_label("Profit ($)")
72 .size(600.0, 350.0)
73 .to_svg()
74 }) {
75 sections.push(s);
76 }
77
78 // 5. Very small numbers
79 if let Some(s) = try_chart("Small Numbers", || {
80 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81 let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82 scatter(&x, &y)
83 .title("Small Numbers")
84 .size(600.0, 350.0)
85 .to_svg()
86 }) {
87 sections.push(s);
88 }
89
90 // 6. All-negative bars
91 if let Some(s) = try_chart("Negative Bars", || {
92 bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93 .title("All-Negative Bars")
94 .y_label("Loss")
95 .size(500.0, 350.0)
96 .to_svg()
97 }) {
98 sections.push(s);
99 }
100
101 // 7. Mixed +/- bars
102 if let Some(s) = try_chart("Mixed Bars", || {
103 bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104 .title("Mixed +/- Bars")
105 .y_label("P&L")
106 .size(500.0, 350.0)
107 .to_svg()
108 }) {
109 sections.push(s);
110 }
111
112 // 8. Dense scatter (1000 points)
113 if let Some(s) = try_chart("Dense Scatter", || {
114 struct Rng(u64);
115 impl Rng {
116 fn next(&mut self) -> f64 {
117 self.0 = self
118 .0
119 .wrapping_mul(6_364_136_223_846_793_005)
120 .wrapping_add(1);
121 (self.0 >> 11) as f64 / (1u64 << 53) as f64
122 }
123 }
124 let mut rng = Rng(123);
125 let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126 let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127 scatter(&x, &y)
128 .title("Dense Scatter (n=1000)")
129 .size(500.0, 400.0)
130 .to_svg()
131 }) {
132 sections.push(s);
133 }
134
135 // 9. Single bar
136 if let Some(s) = try_chart("Single Bar", || {
137 bar(&["Only One"], &[42.0])
138 .title("Single Bar")
139 .size(500.0, 350.0)
140 .to_svg()
141 }) {
142 sections.push(s);
143 }
144
145 // 10. Many categories (20 bars)
146 if let Some(s) = try_chart("Many Categories", || {
147 let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148 let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149 let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150 bar(&cats_ref, &vals)
151 .title("Many Categories (20)")
152 .size(700.0, 400.0)
153 .to_svg()
154 }) {
155 sections.push(s);
156 }
157
158 // 11. Narrow chart
159 if let Some(s) = try_chart("Narrow Chart", || {
160 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162 line(&x, &y)
163 .title("Narrow Chart")
164 .x_label("X")
165 .y_label("Y")
166 .size(300.0, 400.0)
167 .to_svg()
168 }) {
169 sections.push(s);
170 }
171
172 // 12. Wide chart
173 if let Some(s) = try_chart("Wide Chart", || {
174 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176 line(&x, &y)
177 .title("Wide Chart")
178 .x_label("X")
179 .y_label("Y")
180 .size(1000.0, 250.0)
181 .to_svg()
182 }) {
183 sections.push(s);
184 }
185
186 // 13. Histogram (few bins)
187 if let Some(s) = try_chart("Few Bins", || {
188 histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189 .bins(3)
190 .title("Histogram (3 bins)")
191 .size(400.0, 300.0)
192 .to_svg()
193 }) {
194 sections.push(s);
195 }
196
197 // 14. Dark theme scatter
198 if let Some(s) = try_chart("Dark Scatter", || {
199 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200 let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201 let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202 scatter(&x, &y)
203 .color_by(&cats)
204 .title("Dark Theme Scatter")
205 .x_label("X")
206 .y_label("Y")
207 .theme(NewTheme::dark())
208 .size(500.0, 350.0)
209 .to_svg()
210 }) {
211 sections.push(s);
212 }
213
214 // 15. Dark theme bars
215 if let Some(s) = try_chart("Dark Bars", || {
216 bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217 .title("Dark Theme Bars")
218 .theme(NewTheme::dark())
219 .size(500.0, 350.0)
220 .to_svg()
221 }) {
222 sections.push(s);
223 }
224
225 // 16. Flat line (constant y)
226 if let Some(s) = try_chart("Flat Line", || {
227 let x: Vec<f64> = (0..10).map(f64::from).collect();
228 line(&x, &[5.0; 10])
229 .title("Constant Y")
230 .size(400.0, 300.0)
231 .to_svg()
232 }) {
233 sections.push(s);
234 }
235
236 // 17. Pie with many small slices
237 if let Some(s) = try_chart("Many Pie Slices", || {
238 let mut vals = vec![50.0, 30.0, 15.0];
239 vals.extend(std::iter::repeat_n(0.5, 10));
240 let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241 let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242 pie_labeled(&labels_ref, &vals)
243 .title("Pie: Many Slices")
244 .size(500.0, 400.0)
245 .to_svg()
246 }) {
247 sections.push(s);
248 }
249
250 // 18. Heatmap
251 if let Some(s) = try_chart("Heatmap", || {
252 let data = vec![
253 vec![1.0, 2.0, 3.0, 4.0],
254 vec![5.0, 6.0, 7.0, 8.0],
255 vec![9.0, 10.0, 11.0, 12.0],
256 ];
257 heatmap(data)
258 .title("Basic Heatmap")
259 .with_row_labels(&["R1", "R2", "R3"])
260 .with_col_labels(&["C1", "C2", "C3", "C4"])
261 .size(500.0, 350.0)
262 .to_svg()
263 }) {
264 sections.push(s);
265 }
266
267 // ── Write HTML ─────────────────────────────────────────────────────
268 let mut html = String::from(
269 r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274 box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279 );
280 for (title, svg) in §ions {
281 writeln!(
282 html,
283 "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284 )
285 .unwrap();
286 }
287 html.push_str("</body></html>");
288 std::fs::write("audit_edge_cases.html", &html).unwrap();
289 println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}More examples
examples/p1_showcase.rs (line 54)
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}examples/gallery.rs (line 223)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 334)
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 & 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 §ions {
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 587)
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 §ions {
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}Sourcepub fn save_svg(self, path: impl AsRef<Path>) -> Result<()>
pub fn save_svg(self, path: impl AsRef<Path>) -> Result<()>
Build and save as SVG file.
Examples found in repository?
examples/readme_charts.rs (line 165)
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}Sourcepub fn x_domain(self, min: f64, max: f64) -> Self
pub fn x_domain(self, min: f64, max: f64) -> Self
Set explicit X-axis domain (overrides auto-computed bounds).
Sourcepub fn y_domain(self, min: f64, max: f64) -> Self
pub fn y_domain(self, min: f64, max: f64) -> Self
Set explicit Y-axis domain (overrides auto-computed bounds).
Sourcepub fn bins(self, bins: usize) -> Self
pub fn bins(self, bins: usize) -> Self
Set the number of bins.
Examples found in repository?
examples/audit_edge_cases.rs (line 189)
22fn main() {
23 let mut sections: Vec<(String, String)> = Vec::new();
24
25 // 1. Single data point
26 if let Some(s) = try_chart("Single Point", || {
27 scatter(&[5.0], &[10.0])
28 .title("Single Point")
29 .size(400.0, 300.0)
30 .to_svg()
31 }) {
32 sections.push(s);
33 }
34
35 // 2. Two points
36 if let Some(s) = try_chart("Two Points", || {
37 scatter(&[0.0, 100.0], &[0.0, 100.0])
38 .title("Two Points")
39 .size(400.0, 300.0)
40 .to_svg()
41 }) {
42 sections.push(s);
43 }
44
45 // 3. Very long category labels
46 if let Some(s) = try_chart("Long Labels", || {
47 let cats = vec![
48 "This is an extremely long category label",
49 "Another very very long label here",
50 "Short",
51 "Medium length label",
52 "Yet another verbose category name",
53 ];
54 bar(&cats, &[10.0, 25.0, 15.0, 30.0, 20.0])
55 .title("Long Category Labels")
56 .x_label("Categories")
57 .y_label("Value")
58 .size(600.0, 400.0)
59 .to_svg()
60 }) {
61 sections.push(s);
62 }
63
64 // 4. Large numeric values (millions)
65 if let Some(s) = try_chart("Large Numbers", || {
66 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 1_000_000.0).collect();
67 let y: Vec<f64> = (0..10).map(|i| f64::from(i).powi(2) * 500_000.0).collect();
68 scatter(&x, &y)
69 .title("Large Numbers (Millions)")
70 .x_label("Revenue ($)")
71 .y_label("Profit ($)")
72 .size(600.0, 350.0)
73 .to_svg()
74 }) {
75 sections.push(s);
76 }
77
78 // 5. Very small numbers
79 if let Some(s) = try_chart("Small Numbers", || {
80 let x: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.0001).collect();
81 let y: Vec<f64> = (0..10).map(|i| f64::from(i) * 0.00005).collect();
82 scatter(&x, &y)
83 .title("Small Numbers")
84 .size(600.0, 350.0)
85 .to_svg()
86 }) {
87 sections.push(s);
88 }
89
90 // 6. All-negative bars
91 if let Some(s) = try_chart("Negative Bars", || {
92 bar(&["A", "B", "C", "D"], &[-10.0, -25.0, -5.0, -30.0])
93 .title("All-Negative Bars")
94 .y_label("Loss")
95 .size(500.0, 350.0)
96 .to_svg()
97 }) {
98 sections.push(s);
99 }
100
101 // 7. Mixed +/- bars
102 if let Some(s) = try_chart("Mixed Bars", || {
103 bar(&["Q1", "Q2", "Q3", "Q4"], &[15.0, -10.0, 25.0, -5.0])
104 .title("Mixed +/- Bars")
105 .y_label("P&L")
106 .size(500.0, 350.0)
107 .to_svg()
108 }) {
109 sections.push(s);
110 }
111
112 // 8. Dense scatter (1000 points)
113 if let Some(s) = try_chart("Dense Scatter", || {
114 struct Rng(u64);
115 impl Rng {
116 fn next(&mut self) -> f64 {
117 self.0 = self
118 .0
119 .wrapping_mul(6_364_136_223_846_793_005)
120 .wrapping_add(1);
121 (self.0 >> 11) as f64 / (1u64 << 53) as f64
122 }
123 }
124 let mut rng = Rng(123);
125 let x: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
126 let y: Vec<f64> = (0..1000).map(|_| rng.next() * 100.0).collect();
127 scatter(&x, &y)
128 .title("Dense Scatter (n=1000)")
129 .size(500.0, 400.0)
130 .to_svg()
131 }) {
132 sections.push(s);
133 }
134
135 // 9. Single bar
136 if let Some(s) = try_chart("Single Bar", || {
137 bar(&["Only One"], &[42.0])
138 .title("Single Bar")
139 .size(500.0, 350.0)
140 .to_svg()
141 }) {
142 sections.push(s);
143 }
144
145 // 10. Many categories (20 bars)
146 if let Some(s) = try_chart("Many Categories", || {
147 let cats: Vec<String> = (0..20).map(|i| format!("Cat_{i}")).collect();
148 let cats_ref: Vec<&str> = cats.iter().map(|s| s.as_str()).collect();
149 let vals: Vec<f64> = (0..20).map(|i| (f64::from(i) * 7.0) % 50.0 + 5.0).collect();
150 bar(&cats_ref, &vals)
151 .title("Many Categories (20)")
152 .size(700.0, 400.0)
153 .to_svg()
154 }) {
155 sections.push(s);
156 }
157
158 // 11. Narrow chart
159 if let Some(s) = try_chart("Narrow Chart", || {
160 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
161 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
162 line(&x, &y)
163 .title("Narrow Chart")
164 .x_label("X")
165 .y_label("Y")
166 .size(300.0, 400.0)
167 .to_svg()
168 }) {
169 sections.push(s);
170 }
171
172 // 12. Wide chart
173 if let Some(s) = try_chart("Wide Chart", || {
174 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
175 let y = vec![10.0, 20.0, 15.0, 25.0, 30.0];
176 line(&x, &y)
177 .title("Wide Chart")
178 .x_label("X")
179 .y_label("Y")
180 .size(1000.0, 250.0)
181 .to_svg()
182 }) {
183 sections.push(s);
184 }
185
186 // 13. Histogram (few bins)
187 if let Some(s) = try_chart("Few Bins", || {
188 histogram(&[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0])
189 .bins(3)
190 .title("Histogram (3 bins)")
191 .size(400.0, 300.0)
192 .to_svg()
193 }) {
194 sections.push(s);
195 }
196
197 // 14. Dark theme scatter
198 if let Some(s) = try_chart("Dark Scatter", || {
199 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
200 let y = vec![2.3, 4.1, 3.0, 5.8, 4.9, 7.2, 6.5, 8.1];
201 let cats = vec!["A", "B", "A", "B", "A", "B", "A", "B"];
202 scatter(&x, &y)
203 .color_by(&cats)
204 .title("Dark Theme Scatter")
205 .x_label("X")
206 .y_label("Y")
207 .theme(NewTheme::dark())
208 .size(500.0, 350.0)
209 .to_svg()
210 }) {
211 sections.push(s);
212 }
213
214 // 15. Dark theme bars
215 if let Some(s) = try_chart("Dark Bars", || {
216 bar(&["A", "B", "C", "D"], &[10.0, 25.0, 15.0, 30.0])
217 .title("Dark Theme Bars")
218 .theme(NewTheme::dark())
219 .size(500.0, 350.0)
220 .to_svg()
221 }) {
222 sections.push(s);
223 }
224
225 // 16. Flat line (constant y)
226 if let Some(s) = try_chart("Flat Line", || {
227 let x: Vec<f64> = (0..10).map(f64::from).collect();
228 line(&x, &[5.0; 10])
229 .title("Constant Y")
230 .size(400.0, 300.0)
231 .to_svg()
232 }) {
233 sections.push(s);
234 }
235
236 // 17. Pie with many small slices
237 if let Some(s) = try_chart("Many Pie Slices", || {
238 let mut vals = vec![50.0, 30.0, 15.0];
239 vals.extend(std::iter::repeat_n(0.5, 10));
240 let labels: Vec<String> = (0..vals.len()).map(|i| format!("Slice {i}")).collect();
241 let labels_ref: Vec<&str> = labels.iter().map(|s| s.as_str()).collect();
242 pie_labeled(&labels_ref, &vals)
243 .title("Pie: Many Slices")
244 .size(500.0, 400.0)
245 .to_svg()
246 }) {
247 sections.push(s);
248 }
249
250 // 18. Heatmap
251 if let Some(s) = try_chart("Heatmap", || {
252 let data = vec![
253 vec![1.0, 2.0, 3.0, 4.0],
254 vec![5.0, 6.0, 7.0, 8.0],
255 vec![9.0, 10.0, 11.0, 12.0],
256 ];
257 heatmap(data)
258 .title("Basic Heatmap")
259 .with_row_labels(&["R1", "R2", "R3"])
260 .with_col_labels(&["C1", "C2", "C3", "C4"])
261 .size(500.0, 350.0)
262 .to_svg()
263 }) {
264 sections.push(s);
265 }
266
267 // ── Write HTML ─────────────────────────────────────────────────────
268 let mut html = String::from(
269 r#"<!DOCTYPE html><html><head><meta charset="utf-8">
270<title>Audit Edge Cases</title>
271<style>
272body { font-family: system-ui; background: #f5f5f5; padding: 20px; }
273.chart-card { background: white; border-radius: 8px; padding: 16px; margin: 16px 0;
274 box-shadow: 0 1px 3px rgba(0,0,0,0.12); display: inline-block; vertical-align: top; }
275h2 { color: #333; font-size: 14px; margin: 0 0 8px 0; }
276.fail { background: #fff0f0; border: 1px solid #fcc; }
277</style></head><body>
278<h1>Audit Edge Cases</h1>"#,
279 );
280 for (title, svg) in §ions {
281 writeln!(
282 html,
283 "<div class=\"chart-card\"><h2>{title}</h2>{svg}</div>"
284 )
285 .unwrap();
286 }
287 html.push_str("</body></html>");
288 std::fs::write("audit_edge_cases.html", &html).unwrap();
289 println!("Saved audit_edge_cases.html ({} charts)", sections.len());
290}More examples
examples/p1_showcase.rs (line 49)
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}examples/readme_charts.rs (line 160)
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 218)
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 & grammar APIs</p>
468</header>
469<div class="grid">
470"#,
471 );
472
473 for (title, svg) in §ions {
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 329)
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 & 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 §ions {
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 582)
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 §ions {
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}Auto Trait Implementations§
impl Freeze for HistogramBuilder
impl RefUnwindSafe for HistogramBuilder
impl Send for HistogramBuilder
impl Sync for HistogramBuilder
impl Unpin for HistogramBuilder
impl UnsafeUnpin for HistogramBuilder
impl UnwindSafe for HistogramBuilder
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more