1use crate::error::Result;
5use crate::grammar::chart::Chart;
6use crate::grammar::facet::Facet;
7use crate::grammar::layer::{Layer, MarkType};
8use crate::grammar::position::Position;
9use crate::grammar::stat::Stat;
10use crate::new_theme::NewTheme;
11
12macro_rules! xy_builder_methods {
16 () => {
17 pub fn title(mut self, title: impl Into<String>) -> Self {
19 self.title = Some(title.into());
20 self
21 }
22
23 pub fn x_label(mut self, label: impl Into<String>) -> Self {
25 self.x_label = Some(label.into());
26 self
27 }
28
29 pub fn y_label(mut self, label: impl Into<String>) -> Self {
31 self.y_label = Some(label.into());
32 self
33 }
34
35 pub fn theme(mut self, theme: NewTheme) -> Self {
37 self.theme = theme;
38 self
39 }
40
41 pub fn size(mut self, width: f32, height: f32) -> Self {
43 self.width = width;
44 self.height = height;
45 self
46 }
47
48 #[allow(deprecated)]
50 pub fn to_svg(self) -> Result<String> {
51 self.build().to_svg()
52 }
53
54 #[allow(deprecated)]
56 pub fn save_svg(self, path: impl AsRef<std::path::Path>) -> Result<()> {
57 let svg = self.build().to_svg()?;
58 std::fs::write(path, svg)?;
59 Ok(())
60 }
61
62 pub fn x_domain(mut self, min: f64, max: f64) -> Self {
64 self.x_domain = Some((min, max));
65 self
66 }
67
68 pub fn y_domain(mut self, min: f64, max: f64) -> Self {
70 self.y_domain = Some((min, max));
71 self
72 }
73
74 pub fn opacity(mut self, alpha: f32) -> Self {
76 self.opacity = Some(alpha.clamp(0.0, 1.0));
77 self
78 }
79
80 pub fn hline(mut self, y: f64) -> Self {
82 self.annotations
83 .push(crate::grammar::annotation::Annotation::hline(y));
84 self
85 }
86
87 pub fn vline(mut self, x: f64) -> Self {
89 self.annotations
90 .push(crate::grammar::annotation::Annotation::vline(x));
91 self
92 }
93 };
94}
95
96macro_rules! pie_builder_methods {
98 () => {
99 pub fn title(mut self, title: impl Into<String>) -> Self {
101 self.title = Some(title.into());
102 self
103 }
104
105 pub fn theme(mut self, theme: NewTheme) -> Self {
107 self.theme = theme;
108 self
109 }
110
111 pub fn size(mut self, width: f32, height: f32) -> Self {
113 self.width = width;
114 self.height = height;
115 self
116 }
117
118 pub fn to_svg(self) -> Result<String> {
120 self.build().to_svg()
121 }
122
123 pub fn save_svg(self, path: impl AsRef<std::path::Path>) -> Result<()> {
125 let svg = self.build().to_svg()?;
126 std::fs::write(path, svg)?;
127 Ok(())
128 }
129 };
130}
131
132macro_rules! apply_chart_labels {
134 (xy: $chart:expr, $self:expr) => {{
135 let mut chart = $chart;
136 if let Some(t) = $self.title {
137 chart = chart.title(t);
138 }
139 if let Some(l) = $self.x_label {
140 chart = chart.x_label(l);
141 }
142 if let Some(l) = $self.y_label {
143 chart = chart.y_label(l);
144 }
145 chart
146 }};
147 (pie: $chart:expr, $self:expr) => {{
148 let mut chart = $chart;
149 if let Some(t) = $self.title {
150 chart = chart.title(t);
151 }
152 chart
153 }};
154}
155
156#[must_use]
160pub fn scatter(x: &[f64], y: &[f64]) -> ScatterBuilder {
161 ScatterBuilder {
162 x: x.to_vec(),
163 y: y.to_vec(),
164 categories: None,
165 facet_values: None,
166 facet_ncol: 2,
167 title: None,
168 x_label: None,
169 y_label: None,
170 theme: NewTheme::default(),
171 width: 800.0,
172 height: 600.0,
173 x_domain: None,
174 y_domain: None,
175 opacity: None,
176 error_bars: None,
177 add_trend: false,
178 annotations: Vec::new(),
179 }
180}
181
182pub struct ScatterBuilder {
184 x: Vec<f64>,
185 y: Vec<f64>,
186 categories: Option<Vec<String>>,
187 facet_values: Option<Vec<String>>,
188 facet_ncol: usize,
189 title: Option<String>,
190 x_label: Option<String>,
191 y_label: Option<String>,
192 theme: NewTheme,
193 width: f32,
194 height: f32,
195 x_domain: Option<(f64, f64)>,
196 y_domain: Option<(f64, f64)>,
197 opacity: Option<f32>,
198 error_bars: Option<Vec<f64>>,
199 add_trend: bool,
200 annotations: Vec<crate::grammar::annotation::Annotation>,
201}
202
203impl ScatterBuilder {
204 xy_builder_methods!();
205
206 pub fn color_by(mut self, categories: &[impl ToString]) -> Self {
208 self.categories = Some(categories.iter().map(|c| c.to_string()).collect());
209 self
210 }
211
212 pub fn facet_wrap(mut self, facet_values: &[impl ToString], ncol: usize) -> Self {
214 self.facet_values = Some(facet_values.iter().map(|v| v.to_string()).collect());
215 self.facet_ncol = ncol;
216 self
217 }
218
219 pub fn error_bars(mut self, errors: &[f64]) -> Self {
221 self.error_bars = Some(errors.to_vec());
222 self
223 }
224
225 pub fn trend_line(mut self) -> Self {
227 self.add_trend = true;
228 self
229 }
230
231 pub fn build(self) -> Chart {
233 let mut layer = Layer::new(MarkType::Point)
234 .with_x(self.x.clone())
235 .with_y(self.y.clone());
236 if let Some(cats) = self.categories {
237 layer = layer.with_categories(cats);
238 }
239 if let Some(fv) = self.facet_values {
240 layer = layer.with_facet_values(fv);
241 }
242 if let Some(eb) = self.error_bars {
243 layer = layer.with_error_bars(eb);
244 }
245 let mut chart = Chart::new()
246 .layer(layer)
247 .size(self.width, self.height)
248 .theme(self.theme.clone());
249 if self.add_trend && self.x.len() >= 3 {
251 let trend_layer = Layer::new(MarkType::Line)
252 .with_x(self.x)
253 .with_y(self.y)
254 .stat(Stat::Smooth { bandwidth: 0.3 });
255 chart = chart.layer(trend_layer);
256 }
257 if (!matches!(chart.facet, Facet::None) || self.facet_ncol > 0)
258 && chart.layers.iter().any(|l| l.facet_values.is_some())
259 {
260 chart = chart.facet(Facet::Wrap {
261 ncol: self.facet_ncol,
262 });
263 }
264 if let Some((lo, hi)) = self.x_domain {
265 chart = chart.x_domain(lo, hi);
266 }
267 if let Some((lo, hi)) = self.y_domain {
268 chart = chart.y_domain(lo, hi);
269 }
270 for ann in self.annotations {
271 chart = chart.annotate(ann);
272 }
273 apply_chart_labels!(xy: chart, self)
274 }
275}
276
277#[must_use]
281pub fn line(x: &[f64], y: &[f64]) -> LineBuilder {
282 LineBuilder {
283 x: x.to_vec(),
284 y: y.to_vec(),
285 categories: None,
286 title: None,
287 x_label: None,
288 y_label: None,
289 theme: NewTheme::default(),
290 width: 800.0,
291 height: 600.0,
292 x_domain: None,
293 y_domain: None,
294 opacity: None,
295 annotations: Vec::new(),
296 }
297}
298
299pub struct LineBuilder {
301 x: Vec<f64>,
302 y: Vec<f64>,
303 categories: Option<Vec<String>>,
304 title: Option<String>,
305 x_label: Option<String>,
306 y_label: Option<String>,
307 theme: NewTheme,
308 width: f32,
309 height: f32,
310 x_domain: Option<(f64, f64)>,
311 y_domain: Option<(f64, f64)>,
312 opacity: Option<f32>,
313 annotations: Vec<crate::grammar::annotation::Annotation>,
314}
315
316impl LineBuilder {
317 xy_builder_methods!();
318
319 pub fn color_by(mut self, categories: &[impl ToString]) -> Self {
321 self.categories = Some(categories.iter().map(|c| c.to_string()).collect());
322 self
323 }
324
325 pub fn build(self) -> Chart {
327 let mut layer = Layer::new(MarkType::Line).with_x(self.x).with_y(self.y);
328 if let Some(cats) = self.categories {
329 layer = layer.with_categories(cats);
330 }
331 let mut chart = Chart::new()
332 .layer(layer)
333 .size(self.width, self.height)
334 .theme(self.theme);
335 if let Some((lo, hi)) = self.x_domain {
336 chart = chart.x_domain(lo, hi);
337 }
338 if let Some((lo, hi)) = self.y_domain {
339 chart = chart.y_domain(lo, hi);
340 }
341 for ann in self.annotations {
342 chart = chart.annotate(ann);
343 }
344 apply_chart_labels!(xy: chart, self)
345 }
346}
347
348#[must_use]
352pub fn bar(categories: &[impl ToString], values: &[f64]) -> BarBuilder {
353 let x: Vec<f64> = (0..categories.len()).map(|i| i as f64).collect();
354 BarBuilder {
355 x,
356 y: values.to_vec(),
357 labels: categories.iter().map(|c| c.to_string()).collect(),
358 error_bars: None,
359 title: None,
360 x_label: None,
361 y_label: None,
362 theme: NewTheme::default(),
363 width: 800.0,
364 height: 600.0,
365 x_domain: None,
366 y_domain: None,
367 opacity: None,
368 annotations: Vec::new(),
369 }
370}
371
372pub struct BarBuilder {
374 x: Vec<f64>,
375 y: Vec<f64>,
376 labels: Vec<String>,
377 error_bars: Option<Vec<f64>>,
378 title: Option<String>,
379 x_label: Option<String>,
380 y_label: Option<String>,
381 theme: NewTheme,
382 width: f32,
383 height: f32,
384 x_domain: Option<(f64, f64)>,
385 y_domain: Option<(f64, f64)>,
386 opacity: Option<f32>,
387 annotations: Vec<crate::grammar::annotation::Annotation>,
388}
389
390impl BarBuilder {
391 xy_builder_methods!();
392
393 pub fn error_bars(mut self, errors: &[f64]) -> Self {
395 self.error_bars = Some(errors.to_vec());
396 self
397 }
398
399 pub fn build(self) -> Chart {
401 let mut layer = Layer::new(MarkType::Bar)
402 .with_x(self.x)
403 .with_y(self.y)
404 .with_categories(self.labels);
405 if let Some(eb) = self.error_bars {
406 layer = layer.with_error_bars(eb);
407 }
408 let mut chart = Chart::new()
409 .layer(layer)
410 .size(self.width, self.height)
411 .theme(self.theme);
412 if let Some((lo, hi)) = self.x_domain {
413 chart = chart.x_domain(lo, hi);
414 }
415 if let Some((lo, hi)) = self.y_domain {
416 chart = chart.y_domain(lo, hi);
417 }
418 for ann in self.annotations {
419 chart = chart.annotate(ann);
420 }
421 apply_chart_labels!(xy: chart, self)
422 }
423}
424
425#[must_use]
429pub fn histogram(values: &[f64]) -> HistogramBuilder {
430 HistogramBuilder {
431 values: values.to_vec(),
432 bins: 0,
433 title: None,
434 x_label: None,
435 y_label: None,
436 theme: NewTheme::default(),
437 width: 800.0,
438 height: 600.0,
439 x_domain: None,
440 y_domain: None,
441 opacity: None,
442 annotations: Vec::new(),
443 }
444}
445
446pub struct HistogramBuilder {
448 values: Vec<f64>,
449 bins: usize,
450 title: Option<String>,
451 x_label: Option<String>,
452 y_label: Option<String>,
453 theme: NewTheme,
454 width: f32,
455 height: f32,
456 x_domain: Option<(f64, f64)>,
457 y_domain: Option<(f64, f64)>,
458 opacity: Option<f32>,
459 annotations: Vec<crate::grammar::annotation::Annotation>,
460}
461
462impl HistogramBuilder {
463 xy_builder_methods!();
464
465 pub fn bins(mut self, bins: usize) -> Self {
467 self.bins = bins;
468 self
469 }
470
471 pub fn build(self) -> Chart {
473 let layer = Layer::new(MarkType::Bar)
474 .with_x(self.values)
475 .stat(Stat::Bin { bins: self.bins });
476 let mut chart = Chart::new()
477 .layer(layer)
478 .size(self.width, self.height)
479 .theme(self.theme);
480 if let Some((lo, hi)) = self.x_domain {
481 chart = chart.x_domain(lo, hi);
482 }
483 if let Some((lo, hi)) = self.y_domain {
484 chart = chart.y_domain(lo, hi);
485 }
486 for ann in self.annotations {
487 chart = chart.annotate(ann);
488 }
489 apply_chart_labels!(xy: chart, self)
490 }
491}
492
493#[must_use]
497pub fn boxplot(categories: &[impl ToString], values: &[f64]) -> BoxPlotBuilder {
498 BoxPlotBuilder {
499 categories: categories.iter().map(|c| c.to_string()).collect(),
500 values: values.to_vec(),
501 title: None,
502 x_label: None,
503 y_label: None,
504 theme: NewTheme::default(),
505 width: 800.0,
506 height: 600.0,
507 x_domain: None,
508 y_domain: None,
509 opacity: None,
510 annotations: Vec::new(),
511 }
512}
513
514pub struct BoxPlotBuilder {
516 categories: Vec<String>,
517 values: Vec<f64>,
518 title: Option<String>,
519 x_label: Option<String>,
520 y_label: Option<String>,
521 theme: NewTheme,
522 width: f32,
523 height: f32,
524 x_domain: Option<(f64, f64)>,
525 y_domain: Option<(f64, f64)>,
526 opacity: Option<f32>,
527 annotations: Vec<crate::grammar::annotation::Annotation>,
528}
529
530impl BoxPlotBuilder {
531 xy_builder_methods!();
532
533 pub fn build(self) -> Chart {
535 let layer = Layer::new(MarkType::Bar)
536 .with_y(self.values)
537 .with_categories(self.categories)
538 .stat(Stat::BoxPlot);
539 let mut chart = Chart::new()
540 .layer(layer)
541 .size(self.width, self.height)
542 .theme(self.theme);
543 if let Some((lo, hi)) = self.x_domain {
544 chart = chart.x_domain(lo, hi);
545 }
546 if let Some((lo, hi)) = self.y_domain {
547 chart = chart.y_domain(lo, hi);
548 }
549 for ann in self.annotations {
550 chart = chart.annotate(ann);
551 }
552 apply_chart_labels!(xy: chart, self)
553 }
554}
555
556#[must_use]
560pub fn area(x: &[f64], y: &[f64]) -> AreaBuilder {
561 AreaBuilder {
562 x: x.to_vec(),
563 y: y.to_vec(),
564 categories: None,
565 title: None,
566 x_label: None,
567 y_label: None,
568 theme: NewTheme::default(),
569 width: 800.0,
570 height: 600.0,
571 x_domain: None,
572 y_domain: None,
573 opacity: None,
574 annotations: Vec::new(),
575 }
576}
577
578pub struct AreaBuilder {
580 x: Vec<f64>,
581 y: Vec<f64>,
582 categories: Option<Vec<String>>,
583 title: Option<String>,
584 x_label: Option<String>,
585 y_label: Option<String>,
586 theme: NewTheme,
587 width: f32,
588 height: f32,
589 x_domain: Option<(f64, f64)>,
590 y_domain: Option<(f64, f64)>,
591 opacity: Option<f32>,
592 annotations: Vec<crate::grammar::annotation::Annotation>,
593}
594
595impl AreaBuilder {
596 xy_builder_methods!();
597
598 pub fn color_by(mut self, categories: &[impl ToString]) -> Self {
600 self.categories = Some(categories.iter().map(|c| c.to_string()).collect());
601 self
602 }
603
604 pub fn build(self) -> Chart {
606 let mut layer = Layer::new(MarkType::Area).with_x(self.x).with_y(self.y);
607 if let Some(cats) = self.categories {
608 layer = layer.with_categories(cats);
609 }
610 let mut chart = Chart::new()
611 .layer(layer)
612 .size(self.width, self.height)
613 .theme(self.theme);
614 if let Some((lo, hi)) = self.x_domain {
615 chart = chart.x_domain(lo, hi);
616 }
617 if let Some((lo, hi)) = self.y_domain {
618 chart = chart.y_domain(lo, hi);
619 }
620 for ann in self.annotations {
621 chart = chart.annotate(ann);
622 }
623 apply_chart_labels!(xy: chart, self)
624 }
625}
626
627#[must_use]
631pub fn pie_labeled(labels: &[impl ToString], values: &[f64]) -> PieBuilder {
632 PieBuilder {
633 values: values.to_vec(),
634 labels: labels.iter().map(|l| l.to_string()).collect(),
635 inner_fraction: 0.0,
636 title: None,
637 theme: NewTheme::default(),
638 width: 600.0,
639 height: 600.0,
640 }
641}
642
643#[deprecated(note = "Use pie_labeled(labels, values) for consistent parameter order")]
648pub fn pie(values: &[f64], labels: &[impl ToString]) -> PieBuilder {
649 PieBuilder {
650 values: values.to_vec(),
651 labels: labels.iter().map(|l| l.to_string()).collect(),
652 inner_fraction: 0.0,
653 title: None,
654 theme: NewTheme::default(),
655 width: 600.0,
656 height: 600.0,
657 }
658}
659
660pub struct PieBuilder {
662 values: Vec<f64>,
663 labels: Vec<String>,
664 inner_fraction: f32,
665 title: Option<String>,
666 theme: NewTheme,
667 width: f32,
668 height: f32,
669}
670
671impl PieBuilder {
672 pie_builder_methods!();
673
674 pub fn donut(mut self, inner_fraction: f32) -> Self {
676 self.inner_fraction = inner_fraction.clamp(0.0, 0.9);
677 self
678 }
679
680 pub fn build(self) -> Chart {
682 let layer = Layer::new(MarkType::Arc)
683 .with_y(self.values)
684 .with_categories(self.labels)
685 .with_inner_radius_fraction(self.inner_fraction);
686 let chart = Chart::new()
687 .layer(layer)
688 .size(self.width, self.height)
689 .theme(self.theme);
690 apply_chart_labels!(pie: chart, self)
691 }
692}
693
694#[must_use]
698pub fn treemap(labels: &[impl ToString], values: &[f64]) -> TreemapBuilder {
699 TreemapBuilder {
700 labels: labels.iter().map(|l| l.to_string()).collect(),
701 values: values.to_vec(),
702 title: None,
703 theme: NewTheme::default(),
704 width: 600.0,
705 height: 600.0,
706 }
707}
708
709pub struct TreemapBuilder {
711 labels: Vec<String>,
712 values: Vec<f64>,
713 title: Option<String>,
714 theme: NewTheme,
715 width: f32,
716 height: f32,
717}
718
719impl TreemapBuilder {
720 pie_builder_methods!();
721
722 pub fn build(self) -> Chart {
724 let layer = Layer::new(MarkType::Treemap)
725 .with_y(self.values)
726 .with_categories(self.labels);
727 let chart = Chart::new()
728 .layer(layer)
729 .size(self.width, self.height)
730 .theme(self.theme);
731 apply_chart_labels!(pie: chart, self)
732 }
733}
734
735pub struct MultiBarBuilder {
739 categories: Vec<String>,
740 groups: Vec<String>,
741 values: Vec<f64>,
742 position: Position,
743 title: Option<String>,
744 x_label: Option<String>,
745 y_label: Option<String>,
746 theme: NewTheme,
747 width: f32,
748 height: f32,
749 x_domain: Option<(f64, f64)>,
750 y_domain: Option<(f64, f64)>,
751 opacity: Option<f32>,
752 annotations: Vec<crate::grammar::annotation::Annotation>,
753}
754
755pub type StackedBarBuilder = MultiBarBuilder;
757pub type GroupedBarBuilder = MultiBarBuilder;
759
760#[must_use]
765pub fn stacked_bar(
766 categories: &[impl ToString],
767 groups: &[impl ToString],
768 values: &[f64],
769) -> MultiBarBuilder {
770 MultiBarBuilder {
771 categories: categories.iter().map(|c| c.to_string()).collect(),
772 groups: groups.iter().map(|g| g.to_string()).collect(),
773 values: values.to_vec(),
774 position: Position::Stack,
775 title: None,
776 x_label: None,
777 y_label: None,
778 theme: NewTheme::default(),
779 width: 800.0,
780 height: 600.0,
781 x_domain: None,
782 y_domain: None,
783 opacity: None,
784 annotations: Vec::new(),
785 }
786}
787
788#[must_use]
790pub fn grouped_bar(
791 categories: &[impl ToString],
792 groups: &[impl ToString],
793 values: &[f64],
794) -> MultiBarBuilder {
795 MultiBarBuilder {
796 categories: categories.iter().map(|c| c.to_string()).collect(),
797 groups: groups.iter().map(|g| g.to_string()).collect(),
798 values: values.to_vec(),
799 position: Position::Dodge,
800 title: None,
801 x_label: None,
802 y_label: None,
803 theme: NewTheme::default(),
804 width: 800.0,
805 height: 600.0,
806 x_domain: None,
807 y_domain: None,
808 opacity: None,
809 annotations: Vec::new(),
810 }
811}
812
813impl MultiBarBuilder {
814 xy_builder_methods!();
815
816 pub fn try_build(self) -> std::result::Result<Chart, String> {
818 let config = ChartConfig {
819 title: self.title,
820 x_label: self.x_label,
821 y_label: self.y_label,
822 theme: self.theme,
823 width: self.width,
824 height: self.height,
825 };
826 let mut chart = try_build_grouped_chart(
827 self.categories,
828 self.groups,
829 self.values,
830 self.position,
831 config,
832 )?;
833 if let Some((lo, hi)) = self.x_domain {
834 chart = chart.x_domain(lo, hi);
835 }
836 if let Some((lo, hi)) = self.y_domain {
837 chart = chart.y_domain(lo, hi);
838 }
839 for ann in self.annotations {
840 chart = chart.annotate(ann);
841 }
842 Ok(chart)
843 }
844
845 #[allow(deprecated)]
849 #[deprecated(note = "Use try_build() instead — build() panics on invalid input")]
850 pub fn build(self) -> Chart {
851 self.try_build().expect("MultiBarBuilder::build() failed")
852 }
853}
854
855struct ChartConfig {
859 title: Option<String>,
860 x_label: Option<String>,
861 y_label: Option<String>,
862 theme: NewTheme,
863 width: f32,
864 height: f32,
865}
866
867fn try_build_grouped_chart(
870 categories: Vec<String>,
871 groups: Vec<String>,
872 values: Vec<f64>,
873 position: Position,
874 config: ChartConfig,
875) -> std::result::Result<Chart, String> {
876 use std::collections::HashSet;
877
878 if categories.len() != groups.len() || categories.len() != values.len() {
880 return Err(format!(
881 "categories ({}), groups ({}), and values ({}) must have the same length",
882 categories.len(),
883 groups.len(),
884 values.len(),
885 ));
886 }
887
888 let mut unique_cats: Vec<String> = Vec::new();
889 let mut seen_cats = HashSet::new();
890 for c in &categories {
891 if seen_cats.insert(c.as_str()) {
892 unique_cats.push(c.clone());
893 }
894 }
895
896 let mut unique_groups: Vec<String> = Vec::new();
897 let mut seen_groups = HashSet::new();
898 for g in &groups {
899 if seen_groups.insert(g.as_str()) {
900 unique_groups.push(g.clone());
901 }
902 }
903
904 let cat_idx: std::collections::HashMap<&str, f64> = unique_cats
905 .iter()
906 .enumerate()
907 .map(|(i, c)| (c.as_str(), i as f64))
908 .collect();
909
910 let mut chart = Chart::new()
911 .size(config.width, config.height)
912 .theme(config.theme);
913
914 for group in &unique_groups {
915 let mut x_data = Vec::new();
916 let mut y_data = Vec::new();
917 for (i, g) in groups.iter().enumerate() {
918 if g == group {
919 if let Some(&x) = cat_idx.get(categories[i].as_str()) {
920 x_data.push(x);
921 y_data.push(values[i]);
922 }
923 }
924 }
925 let mut layer = Layer::new(MarkType::Bar)
926 .with_x(x_data)
927 .with_y(y_data)
928 .with_label(group.clone())
929 .position(position);
930 layer = layer.with_categories(unique_cats.clone());
932 chart = chart.layer(layer);
933 }
934
935 if let Some(t) = config.title {
936 chart = chart.title(t);
937 }
938 if let Some(l) = config.x_label {
939 chart = chart.x_label(l);
940 }
941 if let Some(l) = config.y_label {
942 chart = chart.y_label(l);
943 }
944 Ok(chart)
945}
946
947#[must_use]
953pub fn heatmap(data: Vec<Vec<f64>>) -> HeatmapBuilder {
954 HeatmapBuilder {
955 data,
956 row_labels: None,
957 col_labels: None,
958 annotate: false,
959 title: None,
960 x_label: None,
961 y_label: None,
962 theme: NewTheme::default(),
963 width: 600.0,
964 height: 600.0,
965 x_domain: None,
966 y_domain: None,
967 opacity: None,
968 annotations: Vec::new(),
969 }
970}
971
972#[must_use]
974pub fn heatmap_ref(data: &[Vec<f64>]) -> HeatmapBuilder {
975 HeatmapBuilder {
976 data: data.to_vec(),
977 row_labels: None,
978 col_labels: None,
979 annotate: false,
980 title: None,
981 x_label: None,
982 y_label: None,
983 theme: NewTheme::default(),
984 width: 600.0,
985 height: 600.0,
986 x_domain: None,
987 y_domain: None,
988 opacity: None,
989 annotations: Vec::new(),
990 }
991}
992
993pub struct HeatmapBuilder {
995 data: Vec<Vec<f64>>,
996 row_labels: Option<Vec<String>>,
997 col_labels: Option<Vec<String>>,
998 annotate: bool,
999 title: Option<String>,
1000 x_label: Option<String>,
1001 y_label: Option<String>,
1002 theme: NewTheme,
1003 width: f32,
1004 height: f32,
1005 x_domain: Option<(f64, f64)>,
1006 y_domain: Option<(f64, f64)>,
1007 opacity: Option<f32>,
1008 annotations: Vec<crate::grammar::annotation::Annotation>,
1009}
1010
1011impl HeatmapBuilder {
1012 xy_builder_methods!();
1013
1014 pub fn annotate(mut self) -> Self {
1016 self.annotate = true;
1017 self
1018 }
1019
1020 #[deprecated(note = "Use with_row_labels(&[impl ToString]) instead")]
1022 pub fn row_labels(mut self, labels: Vec<String>) -> Self {
1023 self.row_labels = Some(labels);
1024 self
1025 }
1026
1027 #[deprecated(note = "Use with_col_labels(&[impl ToString]) instead")]
1029 pub fn col_labels(mut self, labels: Vec<String>) -> Self {
1030 self.col_labels = Some(labels);
1031 self
1032 }
1033
1034 pub fn with_row_labels(mut self, labels: &[impl ToString]) -> Self {
1036 self.row_labels = Some(labels.iter().map(|l| l.to_string()).collect());
1037 self
1038 }
1039
1040 pub fn with_col_labels(mut self, labels: &[impl ToString]) -> Self {
1042 self.col_labels = Some(labels.iter().map(|l| l.to_string()).collect());
1043 self
1044 }
1045
1046 pub fn build(self) -> Chart {
1048 let mut layer = Layer::new(MarkType::Heatmap).with_heatmap_data(self.data);
1049 if let Some(rl) = self.row_labels {
1050 layer = layer.with_row_labels(rl);
1051 }
1052 if let Some(cl) = self.col_labels {
1053 layer = layer.with_col_labels(cl);
1054 }
1055 if self.annotate {
1056 layer = layer.annotate_cells();
1057 }
1058 let mut chart = Chart::new()
1059 .layer(layer)
1060 .size(self.width, self.height)
1061 .theme(self.theme);
1062 if let Some((lo, hi)) = self.x_domain {
1063 chart = chart.x_domain(lo, hi);
1064 }
1065 if let Some((lo, hi)) = self.y_domain {
1066 chart = chart.y_domain(lo, hi);
1067 }
1068 for ann in self.annotations {
1069 chart = chart.annotate(ann);
1070 }
1071 apply_chart_labels!(xy: chart, self)
1072 }
1073}
1074
1075#[cfg(test)]
1076mod tests {
1077 use super::*;
1078
1079 #[test]
1080 fn scatter_builds_svg() {
1081 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1082 let y = vec![2.0, 4.0, 1.0, 5.0, 3.0];
1083 let svg = scatter(&x, &y).title("Test Scatter").to_svg().unwrap();
1084 assert!(svg.contains("<svg"));
1085 assert!(svg.contains("</svg>"));
1086 assert!(svg.contains("<circle"));
1087 }
1088
1089 #[test]
1090 fn scatter_with_categories() {
1091 let x = vec![1.0, 2.0, 3.0, 4.0];
1092 let y = vec![2.0, 4.0, 1.0, 5.0];
1093 let cats = vec!["A", "B", "A", "B"];
1094 let svg = scatter(&x, &y)
1095 .color_by(&cats)
1096 .title("Colored Scatter")
1097 .to_svg()
1098 .unwrap();
1099 assert!(svg.contains("<circle"));
1100 }
1101
1102 #[test]
1103 fn line_builds_svg() {
1104 let x = vec![0.0, 1.0, 2.0, 3.0];
1105 let y = vec![0.0, 1.0, 4.0, 9.0];
1106 let svg = line(&x, &y)
1107 .title("Quadratic")
1108 .x_label("x")
1109 .y_label("y")
1110 .to_svg()
1111 .unwrap();
1112 assert!(svg.contains("<polyline"));
1113 }
1114
1115 #[test]
1116 fn bar_builds_svg() {
1117 let cats = vec!["A", "B", "C"];
1118 let vals = vec![10.0, 25.0, 15.0];
1119 let svg = bar(&cats, &vals).title("Bar Chart").to_svg().unwrap();
1120 assert!(svg.contains("<rect"));
1121 }
1122
1123 #[test]
1124 fn histogram_builds_svg() {
1125 let data: Vec<f64> = (0..100).map(|i| f64::from(i) * 0.1).collect();
1126 let svg = histogram(&data).title("Histogram").to_svg().unwrap();
1127 assert!(svg.contains("<svg"));
1128 assert!(svg.contains("<rect"));
1129 }
1130
1131 #[test]
1132 fn histogram_with_bins() {
1133 let data: Vec<f64> = (0..50).map(f64::from).collect();
1134 let svg = histogram(&data).bins(5).title("5 Bins").to_svg().unwrap();
1135 assert!(svg.contains("<rect"));
1136 }
1137
1138 #[test]
1139 fn area_builds_svg() {
1140 let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
1141 let y = vec![1.0, 3.0, 2.0, 5.0, 4.0];
1142 let svg = area(&x, &y).title("Area Chart").to_svg().unwrap();
1143 assert!(svg.contains("<svg"));
1144 assert!(svg.contains("<path"));
1145 }
1146
1147 #[test]
1148 fn pie_builds_svg() {
1149 let values = vec![30.0, 20.0, 50.0];
1150 let labels = vec!["A", "B", "C"];
1151 let svg = pie_labeled(&labels, &values)
1152 .title("Pie Chart")
1153 .to_svg()
1154 .unwrap();
1155 assert!(svg.contains("<svg"));
1156 assert!(!svg.contains("<line") || svg.contains("<path"));
1157 }
1158
1159 #[test]
1160 fn pie_equal_values() {
1161 let values = vec![1.0, 1.0, 1.0];
1162 let labels = vec!["X", "Y", "Z"];
1163 let svg = pie_labeled(&labels, &values).to_svg().unwrap();
1164 assert!(svg.contains("<svg"));
1165 }
1166
1167 #[test]
1168 fn donut_builds_svg() {
1169 let values = vec![40.0, 60.0];
1170 let labels = vec!["Yes", "No"];
1171 let svg = pie_labeled(&labels, &values).donut(0.5).to_svg().unwrap();
1172 assert!(svg.contains("<svg"));
1173 }
1174
1175 #[test]
1176 fn boxplot_builds_svg() {
1177 let cats = vec!["A", "A", "A", "A", "A", "B", "B", "B", "B", "B"];
1178 let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 4.0, 6.0, 8.0, 10.0];
1179 let svg = boxplot(&cats, &vals).title("Box Plot").to_svg().unwrap();
1180 assert!(svg.contains("<svg"));
1181 assert!(svg.contains("<rect"));
1182 assert!(svg.contains("<line"));
1183 }
1184
1185 #[test]
1186 fn annotation_hline() {
1187 use crate::grammar::annotation::Annotation;
1188 let x = vec![1.0, 2.0, 3.0];
1189 let y = vec![10.0, 20.0, 30.0];
1190 let chart = scatter(&x, &y).build().annotate(Annotation::hline(15.0));
1191 let svg = chart.to_svg().unwrap();
1192 assert!(svg.contains("<line"));
1193 }
1194
1195 #[test]
1196 fn subtitle_and_caption() {
1197 use crate::grammar::chart::Chart;
1198 use crate::grammar::layer::{Layer, MarkType};
1199 let chart = Chart::new()
1200 .layer(
1201 Layer::new(MarkType::Point)
1202 .with_x(vec![1.0])
1203 .with_y(vec![1.0]),
1204 )
1205 .title("Title")
1206 .subtitle("Subtitle here")
1207 .caption("Source: data");
1208 let svg = chart.to_svg().unwrap();
1209 assert!(svg.contains("Subtitle here"));
1210 assert!(svg.contains("Source: data"));
1211 }
1212
1213 #[test]
1214 fn legend_with_categories() {
1215 let x = vec![1.0, 2.0, 3.0, 4.0];
1216 let y = vec![10.0, 20.0, 15.0, 25.0];
1217 let cats = vec!["Group A", "Group B", "Group A", "Group B"];
1218 let svg = scatter(&x, &y).color_by(&cats).to_svg().unwrap();
1219 assert!(svg.contains("Group A"));
1220 assert!(svg.contains("Group B"));
1221 }
1222
1223 #[test]
1224 fn flipped_coord_bar() {
1225 use crate::grammar::chart::Chart;
1226 use crate::grammar::coord::CoordSystem;
1227 use crate::grammar::layer::{Layer, MarkType};
1228 let chart = Chart::new()
1229 .layer(
1230 Layer::new(MarkType::Bar)
1231 .with_x(vec![0.0, 1.0, 2.0])
1232 .with_y(vec![10.0, 20.0, 30.0]),
1233 )
1234 .coord(CoordSystem::Flipped)
1235 .title("Horizontal Bars");
1236 let svg = chart.to_svg().unwrap();
1237 assert!(svg.contains("<rect"));
1238 }
1239
1240 #[test]
1241 fn stacked_bar_builds_svg() {
1242 let cats = vec!["Q1", "Q2", "Q3", "Q1", "Q2", "Q3"];
1243 let groups = vec!["A", "A", "A", "B", "B", "B"];
1244 let vals = vec![10.0, 20.0, 30.0, 5.0, 15.0, 25.0];
1245 let svg = stacked_bar(&cats, &groups, &vals)
1246 .title("Stacked")
1247 .to_svg()
1248 .unwrap();
1249 assert!(svg.contains("<rect"));
1250 }
1251
1252 #[test]
1253 fn grouped_bar_builds_svg() {
1254 let cats = vec!["Q1", "Q2", "Q1", "Q2"];
1255 let groups = vec!["A", "A", "B", "B"];
1256 let vals = vec![10.0, 20.0, 15.0, 25.0];
1257 let svg = grouped_bar(&cats, &groups, &vals)
1258 .title("Grouped")
1259 .to_svg()
1260 .unwrap();
1261 assert!(svg.contains("<rect"));
1262 }
1263
1264 #[test]
1265 fn facet_wrap_scatter_builds_svg() {
1266 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
1267 let y = vec![2.0, 4.0, 1.0, 5.0, 3.0, 6.0];
1268 let facets = vec!["A", "A", "A", "B", "B", "B"];
1269 let svg = scatter(&x, &y)
1270 .facet_wrap(&facets, 2)
1271 .title("Faceted Scatter")
1272 .to_svg()
1273 .unwrap();
1274 assert!(svg.contains("<svg"));
1275 assert!(svg.contains("<circle"));
1276 assert!(svg.contains('A'));
1277 assert!(svg.contains('B'));
1278 }
1279
1280 #[test]
1281 fn heatmap_builds_svg() {
1282 let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1283 let svg = heatmap(data).title("Heatmap").to_svg().unwrap();
1284 assert!(svg.contains("<svg"));
1285 assert!(svg.contains("<rect"));
1286 }
1287
1288 #[test]
1289 fn heatmap_annotated_builds_svg() {
1290 let data = vec![vec![10.0, 20.0], vec![30.0, 40.0]];
1291 let svg = heatmap(data)
1292 .annotate()
1293 .title("Annotated")
1294 .to_svg()
1295 .unwrap();
1296 assert!(svg.contains("<svg"));
1297 assert!(svg.contains("<rect"));
1298 assert!(svg.contains("<text"));
1299 }
1300
1301 #[test]
1302 fn facet_single_value_no_split() {
1303 let x = vec![1.0, 2.0, 3.0];
1304 let y = vec![10.0, 20.0, 30.0];
1305 let facets = vec!["All", "All", "All"];
1306 let svg = scatter(&x, &y).facet_wrap(&facets, 2).to_svg().unwrap();
1307 assert!(svg.contains("<svg"));
1308 assert!(svg.contains("<circle"));
1309 }
1310
1311 #[test]
1312 fn grouped_bar_legend_has_group_names() {
1313 let cats = vec!["Q1", "Q2", "Q1", "Q2"];
1314 let groups = vec!["Revenue", "Revenue", "Costs", "Costs"];
1315 let vals = vec![10.0, 20.0, 15.0, 25.0];
1316 let svg = grouped_bar(&cats, &groups, &vals).to_svg().unwrap();
1317 assert!(
1318 svg.contains("Revenue"),
1319 "legend should contain group name 'Revenue'"
1320 );
1321 assert!(
1322 svg.contains("Costs"),
1323 "legend should contain group name 'Costs'"
1324 );
1325 }
1326
1327 #[test]
1328 fn grouped_bar_mismatched_lengths_panics() {
1329 #[allow(deprecated)]
1330 let result = std::panic::catch_unwind(|| {
1331 grouped_bar(&["Q1", "Q2"], &["A", "A", "B"], &[10.0, 20.0]).build();
1332 });
1333 assert!(result.is_err(), "mismatched lengths should panic");
1334 }
1335
1336 #[test]
1337 fn grouped_bar_try_build_returns_err() {
1338 let result = grouped_bar(&["Q1", "Q2"], &["A", "A", "B"], &[10.0, 20.0]).try_build();
1339 assert!(result.is_err());
1340 }
1341
1342 #[test]
1343 fn line_color_by() {
1344 let x = vec![1.0, 2.0, 3.0, 4.0];
1345 let y = vec![10.0, 20.0, 15.0, 25.0];
1346 let cats = vec!["A", "B", "A", "B"];
1347 let svg = line(&x, &y).color_by(&cats).to_svg().unwrap();
1348 assert!(svg.contains("<svg"));
1349 }
1350
1351 #[test]
1352 fn area_color_by() {
1353 let x = vec![1.0, 2.0, 3.0, 4.0];
1354 let y = vec![10.0, 20.0, 15.0, 25.0];
1355 let cats = vec!["A", "B", "A", "B"];
1356 let svg = area(&x, &y).color_by(&cats).to_svg().unwrap();
1357 assert!(svg.contains("<svg"));
1358 }
1359
1360 #[test]
1361 fn svg_contains_title_and_role() {
1362 use crate::grammar::chart::Chart;
1363 use crate::grammar::layer::{Layer, MarkType};
1364 let chart = Chart::new()
1365 .layer(
1366 Layer::new(MarkType::Point)
1367 .with_x(vec![1.0])
1368 .with_y(vec![1.0]),
1369 )
1370 .title("My Chart")
1371 .description("A scatter plot of test data");
1372 let svg = chart.to_svg().unwrap();
1373 assert!(svg.contains(r#"role="img""#), "SVG should have role=img");
1374 assert!(
1375 svg.contains("<title>My Chart</title>"),
1376 "SVG should contain <title>"
1377 );
1378 assert!(
1379 svg.contains("<desc>A scatter plot of test data</desc>"),
1380 "SVG should contain <desc>"
1381 );
1382 }
1383
1384 #[test]
1385 fn heatmap_legend_rendered() {
1386 let data = vec![vec![1.0, 5.0], vec![3.0, 9.0]];
1387 let svg = heatmap(data).title("Heatmap Legend").to_svg().unwrap();
1388 assert!(svg.contains("<svg"));
1390 }
1391
1392 #[test]
1393 fn treemap_builds_svg() {
1394 let labels = vec!["A", "B", "C", "D"];
1395 let values = vec![30.0, 20.0, 15.0, 10.0];
1396 let svg = treemap(&labels, &values).title("Treemap").to_svg().unwrap();
1397 assert!(svg.contains("<svg"));
1398 assert!(svg.contains("<rect"));
1399 }
1400
1401 #[test]
1402 fn treemap_single_item() {
1403 let svg = treemap(&["Only"], &[100.0]).to_svg().unwrap();
1404 assert!(svg.contains("<svg"));
1405 assert!(svg.contains("<rect"));
1406 }
1407
1408 #[test]
1409 fn treemap_with_zeros() {
1410 let labels = vec!["A", "B", "C"];
1411 let values = vec![30.0, 0.0, 20.0];
1412 let svg = treemap(&labels, &values).to_svg().unwrap();
1413 assert!(svg.contains("<svg"));
1414 }
1415
1416 #[test]
1417 fn bar_with_error_bars_builds_svg() {
1418 let cats = vec!["A", "B", "C"];
1419 let vals = vec![10.0, 25.0, 15.0];
1420 let errs = vec![1.5, 2.0, 1.0];
1421 let svg = bar(&cats, &vals)
1422 .error_bars(&errs)
1423 .title("Bar with Error Bars")
1424 .to_svg()
1425 .unwrap();
1426 assert!(svg.contains("<rect"));
1427 assert!(svg.contains("<line"), "should have whisker lines");
1428 }
1429
1430 #[test]
1431 fn stacked_bar_legend_has_group_names() {
1432 let cats = vec!["Q1", "Q2", "Q1", "Q2"];
1433 let groups = vec!["Alpha", "Alpha", "Beta", "Beta"];
1434 let vals = vec![10.0, 20.0, 5.0, 15.0];
1435 let svg = stacked_bar(&cats, &groups, &vals).to_svg().unwrap();
1436 assert!(
1437 svg.contains("Alpha"),
1438 "legend should contain group name 'Alpha'"
1439 );
1440 assert!(
1441 svg.contains("Beta"),
1442 "legend should contain group name 'Beta'"
1443 );
1444 }
1445
1446 #[test]
1447 fn boxplot_shows_category_labels() {
1448 let cats = vec![
1449 "GroupA", "GroupA", "GroupA", "GroupA", "GroupA", "GroupB", "GroupB", "GroupB",
1450 "GroupB", "GroupB",
1451 ];
1452 let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 4.0, 6.0, 8.0, 10.0];
1453 let svg = boxplot(&cats, &vals).title("Box Plot").to_svg().unwrap();
1454 assert!(
1455 svg.contains("GroupA"),
1456 "boxplot SVG should contain category name GroupA"
1457 );
1458 assert!(
1459 svg.contains("GroupB"),
1460 "boxplot SVG should contain category name GroupB"
1461 );
1462 }
1463}