1use std::fmt::Write;
5
6use esoc_chart::express::{
7 area, bar, boxplot, grouped_bar, heatmap, histogram, line, pie_labeled, scatter, stacked_bar,
8};
9use esoc_chart::grammar::annotation::Annotation;
10use esoc_chart::grammar::chart::Chart;
11use esoc_chart::grammar::coord::CoordSystem;
12use esoc_chart::grammar::facet::{Facet, FacetScales};
13use esoc_chart::grammar::layer::{Layer, MarkType};
14use esoc_chart::grammar::position::Position;
15use esoc_chart::grammar::stat::Stat;
16use esoc_chart::new_theme::NewTheme;
17
18fn main() -> esoc_chart::error::Result<()> {
19 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 {
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 {
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 {
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 {
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 {
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 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
285 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 {
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 {
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 {
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 {
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 {
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 {
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 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 {
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 {
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 {
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 {
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 {
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 {
565 let mut x = Vec::new();
566 let mut y = Vec::new();
567 let mut facets = Vec::new();
568 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 {
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 {
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 {
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 {
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 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}