pub struct Palette { /* private fields */ }Expand description
An ordered list of colors for data visualization.
Implementations§
Source§impl Palette
impl Palette
Sourcepub fn sequential(start: Color, end: Color, n: usize) -> Palette
pub fn sequential(start: Color, end: Color, n: usize) -> Palette
Generate a sequential palette by interpolating in OKLab.
Sourcepub fn diverging(low: Color, mid: Color, high: Color, n: usize) -> Palette
pub fn diverging(low: Color, mid: Color, high: Color, n: usize) -> Palette
Generate a diverging palette with a neutral midpoint.
Examples found in repository?
examples/stress_test.rs (lines 1304-1309)
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 categorical(n: usize) -> Palette
pub fn categorical(n: usize) -> Palette
Generate n evenly-spaced categorical colors in OKLCH.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for Palette
impl RefUnwindSafe for Palette
impl Send for Palette
impl Sync for Palette
impl Unpin for Palette
impl UnsafeUnpin for Palette
impl UnwindSafe for Palette
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