1use crate::artist::*;
9use crate::error::{PlotError, Result};
10use crate::layout::{self, LayoutConfig};
11use crate::legend::{self, LegendEntry, SwatchKind};
12use crate::primitives::*;
13use crate::renderer::Renderer;
14use crate::scale::Scale;
15use crate::series::{IntoCategories, IntoSeries};
16use crate::theme::{Loc, Marker, Theme, TickDirection};
17use crate::ticks;
18
19const DEFAULT_TICK_COUNT: usize = 7;
25
26const AUTOSCALE_PAD: f64 = 0.05;
29
30#[derive(Debug)]
44pub struct Axes {
45 pub(crate) artists: Vec<Artist>,
47 pub(crate) title: Option<String>,
49 pub(crate) xlabel: Option<String>,
51 pub(crate) ylabel: Option<String>,
53 pub(crate) xlim: Option<(f64, f64)>,
55 pub(crate) ylim: Option<(f64, f64)>,
57 pub(crate) xscale: Scale,
59 pub(crate) yscale: Scale,
61 pub(crate) show_grid: Option<bool>,
63 pub(crate) show_legend: bool,
65 pub(crate) legend_loc: Loc,
67 pub(crate) theme_override: Option<Theme>,
69 color_index: usize,
72}
73
74impl Axes {
79 pub(crate) fn new() -> Self {
81 Self {
82 artists: Vec::new(),
83 title: None,
84 xlabel: None,
85 ylabel: None,
86 xlim: None,
87 ylim: None,
88 xscale: Scale::default(),
89 yscale: Scale::default(),
90 show_grid: None,
91 show_legend: false,
92 legend_loc: Loc::Best,
93 theme_override: None,
94 color_index: 0,
95 }
96 }
97
98}
99
100impl Axes {
105 pub fn plot<X, Y>(&mut self, x: X, y: Y) -> Result<&mut LineArtist>
116 where
117 X: IntoSeries,
118 Y: IntoSeries,
119 {
120 let xs = x.into_series();
121 let ys = y.into_series();
122 if xs.len() != ys.len() {
123 return Err(PlotError::SeriesLengthMismatch {
124 expected: xs.len(),
125 got: ys.len(),
126 });
127 }
128 if xs.is_empty() {
129 return Err(PlotError::EmptyData);
130 }
131 let color = Color::TABLEAU_10[self.color_index % 10];
132 self.color_index += 1;
133 let artist = LineArtist {
134 x: xs,
135 y: ys,
136 color,
137 width: 1.5,
138 style: crate::theme::LineStyle::Solid,
139 label: None,
140 alpha: 1.0,
141 };
142 self.artists.push(Artist::Line(artist));
143 match self.artists.last_mut().expect("just pushed") {
144 Artist::Line(a) => Ok(a),
145 _ => unreachable!(),
146 }
147 }
148
149 pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
158 where
159 X: IntoSeries,
160 Y: IntoSeries,
161 {
162 let xs = x.into_series();
163 let ys = y.into_series();
164 if xs.len() != ys.len() {
165 return Err(PlotError::SeriesLengthMismatch {
166 expected: xs.len(),
167 got: ys.len(),
168 });
169 }
170 if xs.is_empty() {
171 return Err(PlotError::EmptyData);
172 }
173 let color = Color::TABLEAU_10[self.color_index % 10];
174 self.color_index += 1;
175 let artist = ScatterArtist {
176 x: xs,
177 y: ys,
178 color,
179 marker: Marker::Circle,
180 size: 6.0,
181 label: None,
182 alpha: 0.8,
183 colors: None,
184 c: None,
185 cmap: None,
186 };
187 self.artists.push(Artist::Scatter(artist));
188 match self.artists.last_mut().expect("just pushed") {
189 Artist::Scatter(a) => Ok(a),
190 _ => unreachable!(),
191 }
192 }
193
194 pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
204 where
205 C: IntoCategories,
206 H: IntoSeries,
207 {
208 let cats = categories.into_categories();
209 let vals = heights.into_series();
210 if cats.len() != vals.len() {
211 return Err(PlotError::SeriesLengthMismatch {
212 expected: cats.len(),
213 got: vals.len(),
214 });
215 }
216 if cats.is_empty() {
217 return Err(PlotError::EmptyData);
218 }
219 let color = Color::TABLEAU_10[self.color_index % 10];
220 self.color_index += 1;
221 let artist = BarArtist {
222 categories: cats,
223 heights: vals,
224 color,
225 horizontal: false,
226 bar_width: 0.8,
227 label: None,
228 alpha: 1.0,
229 };
230 self.artists.push(Artist::Bar(artist));
231 match self.artists.last_mut().expect("just pushed") {
232 Artist::Bar(a) => Ok(a),
233 _ => unreachable!(),
234 }
235 }
236
237 pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
246 where
247 C: IntoCategories,
248 W: IntoSeries,
249 {
250 let cats = categories.into_categories();
251 let vals = widths.into_series();
252 if cats.len() != vals.len() {
253 return Err(PlotError::SeriesLengthMismatch {
254 expected: cats.len(),
255 got: vals.len(),
256 });
257 }
258 if cats.is_empty() {
259 return Err(PlotError::EmptyData);
260 }
261 let color = Color::TABLEAU_10[self.color_index % 10];
262 self.color_index += 1;
263 let artist = BarArtist {
264 categories: cats,
265 heights: vals,
266 color,
267 horizontal: true,
268 bar_width: 0.8,
269 label: None,
270 alpha: 1.0,
271 };
272 self.artists.push(Artist::Bar(artist));
273 match self.artists.last_mut().expect("just pushed") {
274 Artist::Bar(a) => Ok(a),
275 _ => unreachable!(),
276 }
277 }
278
279 pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
289 where
290 D: IntoSeries,
291 {
292 let series = data.into_series();
293 if series.is_empty() {
294 return Err(PlotError::EmptyData);
295 }
296 let bins = bins.max(1);
297
298 let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
300
301 let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
303 (data_min - 0.5, data_max + 0.5)
304 } else {
305 (data_min, data_max)
306 };
307
308 let bin_width = (hi - lo) / bins as f64;
309
310 let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
312 *edges.last_mut().expect("edges is non-empty") = hi;
314
315 let mut counts = vec![0.0f64; bins];
317 for &v in &series.data {
318 if !v.is_finite() {
319 continue;
320 }
321 let idx = if v >= hi {
323 bins - 1
325 } else {
326 let raw = ((v - lo) / bin_width) as usize;
327 raw.min(bins - 1)
328 };
329 counts[idx] += 1.0;
330 }
331
332 let color = Color::TABLEAU_10[self.color_index % 10];
333 self.color_index += 1;
334 let artist = HistArtist {
335 data: series,
336 bins,
337 bin_edges: edges,
338 counts,
339 color,
340 label: None,
341 alpha: 0.85,
342 density: false,
343 };
344
345
346
347
348
349
350
351 self.artists.push(Artist::Histogram(artist));
352 match self.artists.last_mut().expect("just pushed") {
353 Artist::Histogram(a) => Ok(a),
354 _ => unreachable!(),
355 }
356 }
357
358 pub fn fill_between<X, Y1, Y2>(
367 &mut self,
368 x: X,
369 y1: Y1,
370 y2: Y2,
371 ) -> Result<&mut FillBetweenArtist>
372 where
373 X: IntoSeries,
374 Y1: IntoSeries,
375 Y2: IntoSeries,
376 {
377 let xs = x.into_series();
378 let y1s = y1.into_series();
379 let y2s = y2.into_series();
380 if xs.len() != y1s.len() {
381 return Err(PlotError::SeriesLengthMismatch {
382 expected: xs.len(),
383 got: y1s.len(),
384 });
385 }
386 if xs.len() != y2s.len() {
387 return Err(PlotError::SeriesLengthMismatch {
388 expected: xs.len(),
389 got: y2s.len(),
390 });
391 }
392 if xs.is_empty() {
393 return Err(PlotError::EmptyData);
394 }
395 let color = Color::TABLEAU_10[self.color_index % 10];
396 self.color_index += 1;
397 let artist = FillBetweenArtist {
398 x: xs,
399 y1: y1s,
400 y2: y2s,
401 color,
402 label: None,
403 alpha: 0.3,
404 };
405 self.artists.push(Artist::FillBetween(artist));
406 match self.artists.last_mut().expect("just pushed") {
407 Artist::FillBetween(a) => Ok(a),
408 _ => unreachable!(),
409 }
410 }
411
412 pub fn step<X: IntoSeries, Y: IntoSeries>(
421 &mut self,
422 x: X,
423 y: Y,
424 ) -> Result<&mut StepArtist> {
425 let xs = x.into_series();
426 let ys = y.into_series();
427 if xs.len() != ys.len() {
428 return Err(PlotError::SeriesLengthMismatch {
429 expected: xs.len(),
430 got: ys.len(),
431 });
432 }
433 if xs.is_empty() {
434 return Err(PlotError::EmptyData);
435 }
436 let color = Color::TABLEAU_10[self.color_index % 10];
437 self.color_index += 1;
438 let artist = StepArtist {
439 x: xs,
440 y: ys,
441 color,
442 width: 1.5,
443 where_step: StepWhere::Pre,
444 label: None,
445 alpha: 1.0,
446 };
447 self.artists.push(Artist::Step(artist));
448 match self.artists.last_mut().expect("just pushed") {
449 Artist::Step(a) => Ok(a),
450 _ => unreachable!(),
451 }
452 }
453
454 pub fn stem<X: IntoSeries, Y: IntoSeries>(
463 &mut self,
464 x: X,
465 y: Y,
466 ) -> Result<&mut StemArtist> {
467 let xs = x.into_series();
468 let ys = y.into_series();
469 if xs.len() != ys.len() {
470 return Err(PlotError::SeriesLengthMismatch {
471 expected: xs.len(),
472 got: ys.len(),
473 });
474 }
475 if xs.is_empty() {
476 return Err(PlotError::EmptyData);
477 }
478 let color = Color::TABLEAU_10[self.color_index % 10];
479 self.color_index += 1;
480 let artist = StemArtist {
481 x: xs,
482 y: ys,
483 color,
484 line_width: 1.5,
485 marker_size: 6.0,
486 baseline: 0.0,
487 label: None,
488 alpha: 1.0,
489 };
490 self.artists.push(Artist::Stem(artist));
491 match self.artists.last_mut().expect("just pushed") {
492 Artist::Stem(a) => Ok(a),
493 _ => unreachable!(),
494 }
495 }
496
497 pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
506 use crate::charts::boxplot::compute_stats;
507 if datasets.is_empty() {
508 return Err(PlotError::EmptyData);
509 }
510 let color = Color::TABLEAU_10[self.color_index % 10];
511 self.color_index += 1;
512 let factor = 1.5;
513 let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
514 let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
515 let artist = BoxPlotArtist {
516 stats,
517 labels,
518 color,
519 label: None,
520 alpha: 1.0,
521 box_width: 0.5,
522 show_outliers: true,
523 whisker_iq_factor: factor,
524 raw_data: datasets,
525 };
526 self.artists.push(Artist::BoxPlot(artist));
527 match self.artists.last_mut().expect("just pushed") {
528 Artist::BoxPlot(a) => Ok(a),
529 _ => unreachable!(),
530 }
531 }
532
533 pub fn errorbar<X: IntoSeries, Y: IntoSeries>(
544 &mut self,
545 x: X,
546 y: Y,
547 ) -> Result<ErrorBarArtist> {
548 let xs = x.into_series();
549 let ys = y.into_series();
550 if xs.len() != ys.len() {
551 return Err(PlotError::SeriesLengthMismatch {
552 expected: xs.len(),
553 got: ys.len(),
554 });
555 }
556 if xs.is_empty() {
557 return Err(PlotError::EmptyData);
558 }
559 let color = Color::TABLEAU_10[self.color_index % 10];
560 self.color_index += 1;
561 Ok(ErrorBarArtist {
562 x: xs,
563 y: ys,
564 xerr: None,
565 yerr: None,
566 color,
567 label: None,
568 cap_size: 4.0,
569 line_width: 1.0,
570 })
571 }
572
573 pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
578 self.artists.push(Artist::ErrorBar(artist));
579 }
580
581 pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
590 if data.is_empty() {
591 return Err(PlotError::EmptyData);
592 }
593 let color = Color::TABLEAU_10[self.color_index % 10];
594 self.color_index += 1;
595 let artist = HeatmapArtist {
596 data,
597 x_labels: None,
598 y_labels: None,
599 cmap: crate::colormap::Colormap::Viridis,
600 vmin: None,
601 vmax: None,
602 show_values: false,
603 color,
604 label: None,
605 };
606 self.artists.push(Artist::Heatmap(artist));
607 match self.artists.last_mut().expect("just pushed") {
608 Artist::Heatmap(a) => Ok(a),
609 _ => unreachable!(),
610 }
611 }
612}
613
614impl Axes {
619 pub fn set_title(&mut self, title: &str) -> &mut Self {
621 self.title = Some(title.to_string());
622 self
623 }
624
625 pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
627 self.xlabel = Some(label.to_string());
628 self
629 }
630
631 pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
633 self.ylabel = Some(label.to_string());
634 self
635 }
636
637 pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
639 self.xlim = Some((min, max));
640 self
641 }
642
643 pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
645 self.ylim = Some((min, max));
646 self
647 }
648
649 pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
651 self.xscale = scale;
652 self
653 }
654
655 pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
657 self.yscale = scale;
658 self
659 }
660
661 pub fn grid(&mut self, show: bool) -> &mut Self {
663 self.show_grid = Some(show);
664 self
665 }
666
667 pub fn legend(&mut self) -> &mut Self {
669 self.show_legend = true;
670 self
671 }
672
673 pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
675 self.legend_loc = loc;
676 self
677 }
678
679 pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
681 self.theme_override = Some(theme);
682 self
683 }
684}
685
686#[allow(clippy::too_many_arguments)]
691impl Axes {
692 pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
708 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
709
710 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
712
713 let xticks = ticks::generate_ticks(xmin, xmax, DEFAULT_TICK_COUNT, &self.xscale);
715 let yticks = ticks::generate_ticks(ymin, ymax, DEFAULT_TICK_COUNT, &self.yscale);
716
717 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
719 layout_config.has_title = self.title.is_some();
720 layout_config.has_xlabel = self.xlabel.is_some();
721 layout_config.has_ylabel = self.ylabel.is_some();
722 layout_config.has_legend = self.show_legend;
723
724
725
726
727
728 let layout_result = layout::compute_layout(&layout_config);
729
730 let plot_area = Rect::new(
732 bounds.x + layout_result.plot_area.x,
733 bounds.y + layout_result.plot_area.y,
734 layout_result.plot_area.width,
735 layout_result.plot_area.height,
736 );
737
738 let bg_path = Path::rect(plot_area);
740 renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
741
742 if self.show_grid.unwrap_or(theme.show_grid) {
744 self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
745 }
746
747 let clip_path = Path::rect(plot_area);
749 renderer.push_clip(&clip_path, Affine::IDENTITY);
750 for artist in &self.artists {
751 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
752 }
753 renderer.pop_clip();
754
755 self.draw_spines(renderer, &plot_area, theme);
757
758 self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
760
761 self.draw_labels(renderer, &plot_area, &bounds, theme);
763
764 if self.show_legend {
766 self.draw_legend(renderer, &plot_area, theme);
767 }
768 }
769
770 fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
777 let mut x_lo = f64::INFINITY;
778 let mut x_hi = f64::NEG_INFINITY;
779 let mut y_lo = f64::INFINITY;
780 let mut y_hi = f64::NEG_INFINITY;
781
782 for artist in &self.artists {
783 match artist {
784 Artist::Line(a) => {
785 if let Some((lo, hi)) = a.x.bounds() {
786 x_lo = x_lo.min(lo);
787 x_hi = x_hi.max(hi);
788 }
789 if let Some((lo, hi)) = a.y.bounds() {
790 y_lo = y_lo.min(lo);
791 y_hi = y_hi.max(hi);
792 }
793 }
794 Artist::Scatter(a) => {
795 if let Some((lo, hi)) = a.x.bounds() {
796 x_lo = x_lo.min(lo);
797 x_hi = x_hi.max(hi);
798 }
799 if let Some((lo, hi)) = a.y.bounds() {
800 y_lo = y_lo.min(lo);
801 y_hi = y_hi.max(hi);
802 }
803 }
804 Artist::Bar(a) => {
805 let n = a.categories.len() as f64;
806 if a.horizontal {
807 y_lo = 0.0_f64.min(y_lo);
809 y_hi = n.max(y_hi);
810 x_lo = 0.0_f64.min(x_lo);
811 if let Some((lo, hi)) = a.heights.bounds() {
812 x_lo = x_lo.min(lo.min(0.0));
813 x_hi = x_hi.max(hi);
814 }
815 } else {
816 x_lo = 0.0_f64.min(x_lo);
818 x_hi = n.max(x_hi);
819 y_lo = 0.0_f64.min(y_lo);
820 if let Some((lo, hi)) = a.heights.bounds() {
821 y_lo = y_lo.min(lo.min(0.0));
822 y_hi = y_hi.max(hi);
823 }
824 }
825 }
826 Artist::Histogram(a) => {
827 if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
828 x_lo = x_lo.min(first);
829 x_hi = x_hi.max(last);
830 }
831 y_lo = 0.0_f64.min(y_lo);
832 let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
833 y_hi = y_hi.max(max_count);
834 }
835 Artist::FillBetween(a) => {
836 if let Some((lo, hi)) = a.x.bounds() {
837 x_lo = x_lo.min(lo);
838 x_hi = x_hi.max(hi);
839 }
840 if let Some((lo, hi)) = a.y1.bounds() {
841 y_lo = y_lo.min(lo);
842 y_hi = y_hi.max(hi);
843 }
844 if let Some((lo, hi)) = a.y2.bounds() {
845 y_lo = y_lo.min(lo);
846 y_hi = y_hi.max(hi);
847 }
848 }
849 Artist::Step(a) => {
850 if let Some((lo, hi)) = a.x.bounds() {
851 x_lo = x_lo.min(lo);
852 x_hi = x_hi.max(hi);
853 }
854 if let Some((lo, hi)) = a.y.bounds() {
855 y_lo = y_lo.min(lo);
856 y_hi = y_hi.max(hi);
857 }
858 }
859 Artist::Stem(a) => {
860 if let Some((lo, hi)) = a.x.bounds() {
861 x_lo = x_lo.min(lo);
862 x_hi = x_hi.max(hi);
863 }
864 if let Some((lo, hi)) = a.y.bounds() {
865 y_lo = y_lo.min(lo.min(a.baseline));
866 y_hi = y_hi.max(hi.max(a.baseline));
867 }
868 }
869 Artist::BoxPlot(a) => {
870 let n = a.stats.len() as f64;
871 x_lo = 0.0_f64.min(x_lo);
872 x_hi = n.max(x_hi);
873 for s in &a.stats {
874 y_lo = y_lo.min(s.whisker_low);
875 y_hi = y_hi.max(s.whisker_high);
876 for &o in &s.outliers {
877 y_lo = y_lo.min(o);
878 y_hi = y_hi.max(o);
879 }
880 }
881 }
882 Artist::ErrorBar(a) => {
883 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
884 x_lo = x_lo.min(bxlo);
885 x_hi = x_hi.max(bxhi);
886 y_lo = y_lo.min(bylo);
887 y_hi = y_hi.max(byhi);
888 }
889 Artist::Heatmap(a) => {
890 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
891 x_lo = x_lo.min(bxlo);
892 x_hi = x_hi.max(bxhi);
893 y_lo = y_lo.min(bylo);
894 y_hi = y_hi.max(byhi);
895 }
896 }
897 }
898
899 if !x_lo.is_finite() || !x_hi.is_finite() {
901 x_lo = 0.0;
902 x_hi = 1.0;
903 }
904 if !y_lo.is_finite() || !y_hi.is_finite() {
905 y_lo = 0.0;
906 y_hi = 1.0;
907 }
908
909 if (x_hi - x_lo).abs() < f64::EPSILON {
911 x_lo -= 0.5;
912 x_hi += 0.5;
913 }
914 if (y_hi - y_lo).abs() < f64::EPSILON {
915 y_lo -= 0.5;
916 y_hi += 0.5;
917 }
918
919 let x_pad = (x_hi - x_lo) * AUTOSCALE_PAD;
921 let y_pad = (y_hi - y_lo) * AUTOSCALE_PAD;
922 x_lo -= x_pad;
923 x_hi += x_pad;
924 y_lo -= y_pad;
925 y_hi += y_pad;
926
927 if let Some((lo, hi)) = self.xlim {
929 x_lo = lo;
930 x_hi = hi;
931 }
932 if let Some((lo, hi)) = self.ylim {
933 y_lo = lo;
934 y_hi = hi;
935 }
936
937 (x_lo, x_hi, y_lo, y_hi)
938 }
939
940 fn draw_grid(
946 &self,
947 renderer: &mut impl Renderer,
948 plot_area: &Rect,
949 xticks: &[ticks::Tick],
950 yticks: &[ticks::Tick],
951 xmin: f64,
952 xmax: f64,
953 ymin: f64,
954 ymax: f64,
955 theme: &Theme,
956 ) {
957 let paint = Paint::new(theme.grid_color);
958 let stroke = Stroke::new(theme.grid_width);
959
960 for tick in xticks {
962 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
963 let mut path = Path::new();
964 path.move_to(pt.x, plot_area.y);
965 path.line_to(pt.x, plot_area.bottom());
966 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
967 }
968
969 for tick in yticks {
971 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
972 let mut path = Path::new();
973 path.move_to(plot_area.x, pt.y);
974 path.line_to(plot_area.right(), pt.y);
975 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
976 }
977 }
978
979 fn draw_artist(
985 &self,
986 renderer: &mut impl Renderer,
987 artist: &Artist,
988 plot_area: &Rect,
989 xmin: f64,
990 xmax: f64,
991 ymin: f64,
992 ymax: f64,
993 theme: &Theme,
994 ) {
995 match artist {
996 Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
997 Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
998 Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
999 Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1000 Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1001 Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1002 Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1003 Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1004 Artist::ErrorBar(a) => self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1005 Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1006 }
1007 }
1008
1009 fn draw_line(
1011 &self,
1012 renderer: &mut impl Renderer,
1013 artist: &LineArtist,
1014 plot_area: &Rect,
1015 xmin: f64,
1016 xmax: f64,
1017 ymin: f64,
1018 ymax: f64,
1019 ) {
1020 if artist.x.is_empty() {
1021 return;
1022 }
1023
1024 let mut path = Path::new();
1025 let first = self.data_to_pixel(
1026 artist.x.data[0],
1027 artist.y.data[0],
1028 plot_area,
1029 xmin, xmax, ymin, ymax,
1030 );
1031 path.move_to(first.x, first.y);
1032
1033 for i in 1..artist.x.len() {
1034 let pt = self.data_to_pixel(
1035 artist.x.data[i],
1036 artist.y.data[i],
1037 plot_area,
1038 xmin, xmax, ymin, ymax,
1039 );
1040 path.line_to(pt.x, pt.y);
1041 }
1042
1043 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1044 let paint = Paint::new(color);
1045 let mut stroke = Stroke::new(artist.width);
1046
1047 match artist.style {
1049 crate::theme::LineStyle::Solid => {}
1050 crate::theme::LineStyle::Dashed => {
1051 stroke = stroke.with_dash(DashPattern {
1052 dashes: vec![6.0, 4.0],
1053 offset: 0.0,
1054 });
1055 }
1056 crate::theme::LineStyle::Dotted => {
1057 stroke = stroke.with_dash(DashPattern {
1058 dashes: vec![2.0, 2.0],
1059 offset: 0.0,
1060 });
1061 }
1062 crate::theme::LineStyle::DashDot => {
1063 stroke = stroke.with_dash(DashPattern {
1064 dashes: vec![6.0, 3.0, 2.0, 3.0],
1065 offset: 0.0,
1066 });
1067 }
1068 }
1069
1070 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1071 }
1072
1073 fn draw_scatter(
1075 &self,
1076 renderer: &mut impl Renderer,
1077 artist: &ScatterArtist,
1078 plot_area: &Rect,
1079 xmin: f64,
1080 xmax: f64,
1081 ymin: f64,
1082 ymax: f64,
1083 theme: &Theme,
1084 ) {
1085 let alpha_byte = (artist.alpha * 255.0) as u8;
1086
1087 let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
1089 (Some(c_vals), Some(cmap)) => Some(cmap.map_values(c_vals)),
1090 _ => None,
1091 };
1092
1093 let default_color = artist.color.with_alpha(alpha_byte);
1094 let default_paint = Paint::new(default_color);
1095 let radius = artist.size / 2.0;
1096
1097 for i in 0..artist.x.len() {
1098 let pt = self.data_to_pixel(
1099 artist.x.data[i],
1100 artist.y.data[i],
1101 plot_area,
1102 xmin, xmax, ymin, ymax,
1103 );
1104
1105 let paint = if let Some(ref cc) = cmap_colors {
1107 Paint::new(cc[i].with_alpha(alpha_byte))
1108 } else if let Some(ref cs) = artist.colors {
1109 Paint::new(cs[i].with_alpha(alpha_byte))
1110 } else {
1111 default_paint
1112 };
1113
1114 let marker_path = match artist.marker {
1115 Marker::Circle | Marker::Point => Path::circle(pt, radius),
1116 Marker::Square => {
1117 Path::rect(Rect::new(pt.x - radius, pt.y - radius, radius * 2.0, radius * 2.0))
1118 }
1119 Marker::Diamond => {
1120 let mut p = Path::new();
1121 p.move_to(pt.x, pt.y - radius);
1122 p.line_to(pt.x + radius, pt.y);
1123 p.line_to(pt.x, pt.y + radius);
1124 p.line_to(pt.x - radius, pt.y);
1125 p.close();
1126 p
1127 }
1128 Marker::Triangle => {
1129 let mut p = Path::new();
1130 let h = radius * 1.1547; p.move_to(pt.x, pt.y - radius);
1132 p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
1133 p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
1134 p.close();
1135 p
1136 }
1137 Marker::Plus => {
1138 let mut p = Path::new();
1140 p.move_to(pt.x - radius, pt.y);
1141 p.line_to(pt.x + radius, pt.y);
1142 p.move_to(pt.x, pt.y - radius);
1143 p.line_to(pt.x, pt.y + radius);
1144 let stroke = Stroke::new(theme.line_width.max(1.0));
1145 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1146 continue;
1147 }
1148 Marker::Cross => {
1149 let mut p = Path::new();
1150 let d = radius * 0.707; p.move_to(pt.x - d, pt.y - d);
1152 p.line_to(pt.x + d, pt.y + d);
1153 p.move_to(pt.x + d, pt.y - d);
1154 p.line_to(pt.x - d, pt.y + d);
1155 let stroke = Stroke::new(theme.line_width.max(1.0));
1156 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1157 continue;
1158 }
1159 Marker::Star => {
1160 let mut p = Path::new();
1162 let inner = radius * 0.382;
1163 for j in 0..10 {
1164 let angle = std::f64::consts::FRAC_PI_2
1165 + j as f64 * std::f64::consts::PI / 5.0;
1166 let r = if j % 2 == 0 { radius } else { inner };
1167 let sx = pt.x + r * angle.cos();
1168 let sy = pt.y - r * angle.sin();
1169 if j == 0 {
1170 p.move_to(sx, sy);
1171 } else {
1172 p.line_to(sx, sy);
1173 }
1174 }
1175 p.close();
1176 p
1177 }
1178 };
1179
1180 renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
1181 }
1182 }
1183
1184 fn draw_bar(
1186 &self,
1187 renderer: &mut impl Renderer,
1188 artist: &BarArtist,
1189 plot_area: &Rect,
1190 xmin: f64,
1191 xmax: f64,
1192 ymin: f64,
1193 ymax: f64,
1194 ) {
1195 let n = artist.categories.len();
1196 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1197 let paint = Paint::new(color);
1198
1199 if artist.horizontal {
1200 let cat_range = ymax - ymin;
1202 let cat_step = cat_range / n as f64;
1203 let bar_half = cat_step * artist.bar_width * 0.5;
1204
1205 for i in 0..n {
1206 let cat_center = ymin + (i as f64 + 0.5) * cat_step;
1207 let value = artist.heights.data[i];
1208
1209 let left_val = 0.0_f64.min(value);
1210 let right_val = 0.0_f64.max(value);
1211
1212 let p_left = self.data_to_pixel(left_val, cat_center - bar_half, plot_area, xmin, xmax, ymin, ymax);
1213 let p_right = self.data_to_pixel(right_val, cat_center + bar_half, plot_area, xmin, xmax, ymin, ymax);
1214
1215 let rect = Rect::from_points(p_left, p_right);
1216 let bar_path = Path::rect(rect);
1217 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1218 }
1219 } else {
1220 let cat_range = xmax - xmin;
1222 let cat_step = cat_range / n as f64;
1223 let bar_half = cat_step * artist.bar_width * 0.5;
1224
1225 for i in 0..n {
1226 let cat_center = xmin + (i as f64 + 0.5) * cat_step;
1227 let value = artist.heights.data[i];
1228
1229 let bottom_val = 0.0_f64.min(value);
1230 let top_val = 0.0_f64.max(value);
1231
1232 let p_bl = self.data_to_pixel(cat_center - bar_half, bottom_val, plot_area, xmin, xmax, ymin, ymax);
1233 let p_tr = self.data_to_pixel(cat_center + bar_half, top_val, plot_area, xmin, xmax, ymin, ymax);
1234
1235 let rect = Rect::from_points(p_bl, p_tr);
1236 let bar_path = Path::rect(rect);
1237 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1238 }
1239 }
1240 }
1241
1242 fn draw_hist(
1244 &self,
1245 renderer: &mut impl Renderer,
1246 artist: &HistArtist,
1247 plot_area: &Rect,
1248 xmin: f64,
1249 xmax: f64,
1250 ymin: f64,
1251 ymax: f64,
1252 ) {
1253 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1254 let paint = Paint::new(color);
1255 let stroke_paint = Paint::new(Color::WHITE);
1256 let stroke = Stroke::new(0.5);
1257
1258 for i in 0..artist.counts.len() {
1259 let left = artist.bin_edges[i];
1260 let right = artist.bin_edges[i + 1];
1261 let height = artist.counts[i];
1262
1263 if height <= 0.0 {
1264 continue;
1265 }
1266
1267 let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
1268 let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
1269
1270 let rect = Rect::from_points(p_bl, p_tr);
1271 let bar_path = Path::rect(rect);
1272 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1273 renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
1275 }
1276 }
1277
1278 fn draw_fill_between(
1281 &self,
1282 renderer: &mut impl Renderer,
1283 artist: &FillBetweenArtist,
1284 plot_area: &Rect,
1285 xmin: f64,
1286 xmax: f64,
1287 ymin: f64,
1288 ymax: f64,
1289 ) {
1290 if artist.x.is_empty() {
1291 return;
1292 }
1293
1294 let n = artist.x.len();
1295 let mut path = Path::new();
1296
1297 let first = self.data_to_pixel(
1299 artist.x.data[0],
1300 artist.y1.data[0],
1301 plot_area,
1302 xmin, xmax, ymin, ymax,
1303 );
1304 path.move_to(first.x, first.y);
1305 for i in 1..n {
1306 let pt = self.data_to_pixel(
1307 artist.x.data[i],
1308 artist.y1.data[i],
1309 plot_area,
1310 xmin, xmax, ymin, ymax,
1311 );
1312 path.line_to(pt.x, pt.y);
1313 }
1314
1315 for i in (0..n).rev() {
1317 let pt = self.data_to_pixel(
1318 artist.x.data[i],
1319 artist.y2.data[i],
1320 plot_area,
1321 xmin, xmax, ymin, ymax,
1322 );
1323 path.line_to(pt.x, pt.y);
1324 }
1325 path.close();
1326
1327 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1328 let paint = Paint::new(color);
1329 renderer.fill_path(&path, &paint, Affine::IDENTITY);
1330 }
1331
1332
1333 fn draw_spines(
1339 &self,
1340 renderer: &mut impl Renderer,
1341 plot_area: &Rect,
1342 theme: &Theme,
1343 ) {
1344 let paint = Paint::new(theme.spine_color);
1345 let stroke = Stroke::new(theme.spine_width);
1346
1347 if theme.show_bottom_spine {
1349 let mut p = Path::new();
1350 p.move_to(plot_area.x, plot_area.bottom());
1351 p.line_to(plot_area.right(), plot_area.bottom());
1352 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1353 }
1354 if theme.show_left_spine {
1356 let mut p = Path::new();
1357 p.move_to(plot_area.x, plot_area.y);
1358 p.line_to(plot_area.x, plot_area.bottom());
1359 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1360 }
1361 if theme.show_top_spine {
1363 let mut p = Path::new();
1364 p.move_to(plot_area.x, plot_area.y);
1365 p.line_to(plot_area.right(), plot_area.y);
1366 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1367 }
1368 if theme.show_right_spine {
1370 let mut p = Path::new();
1371 p.move_to(plot_area.right(), plot_area.y);
1372 p.line_to(plot_area.right(), plot_area.bottom());
1373 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1374 }
1375 }
1376
1377 fn draw_ticks(
1383 &self,
1384 renderer: &mut impl Renderer,
1385 plot_area: &Rect,
1386 xticks: &[ticks::Tick],
1387 yticks: &[ticks::Tick],
1388 xmin: f64,
1389 xmax: f64,
1390 ymin: f64,
1391 ymax: f64,
1392 theme: &Theme,
1393 ) {
1394 let tick_paint = Paint::new(theme.tick_color);
1395 let tick_stroke = Stroke::new(1.0);
1396 let tick_len = theme.tick_length;
1397
1398 let label_style = TextStyle {
1399 size: theme.tick_label_size,
1400 color: theme.text_color,
1401 weight: FontWeight::Normal,
1402 family: theme.font_family.clone(),
1403 halign: HAlign::Center,
1404 valign: VAlign::Top,
1405 };
1406
1407 let outward = matches!(theme.tick_direction, TickDirection::Outward);
1409
1410 for tick in xticks {
1412 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1413 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
1415 continue;
1416 }
1417 let x = pt.x;
1418 let y_base = plot_area.bottom();
1419
1420 let (y_start, y_end) = if outward {
1422 (y_base, y_base + tick_len)
1423 } else {
1424 (y_base - tick_len, y_base)
1425 };
1426 let mut tp = Path::new();
1427 tp.move_to(x, y_start);
1428 tp.line_to(x, y_end);
1429 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1430
1431 let label_y = if outward {
1433 y_base + tick_len + 2.0
1434 } else {
1435 y_base + 2.0
1436 };
1437 renderer.draw_text(
1438 &tick.label,
1439 Point::new(x, label_y),
1440 &label_style,
1441 Affine::IDENTITY,
1442 );
1443 }
1444
1445 let y_label_style = TextStyle {
1447 halign: HAlign::Right,
1448 valign: VAlign::Middle,
1449 ..label_style.clone()
1450 };
1451
1452 for tick in yticks {
1453 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1454 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
1456 continue;
1457 }
1458 let y = pt.y;
1459 let x_base = plot_area.x;
1460
1461 let (x_start, x_end) = if outward {
1463 (x_base - tick_len, x_base)
1464 } else {
1465 (x_base, x_base + tick_len)
1466 };
1467 let mut tp = Path::new();
1468 tp.move_to(x_start, y);
1469 tp.line_to(x_end, y);
1470 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1471
1472 let label_x = if outward {
1474 x_base - tick_len - 3.0
1475 } else {
1476 x_base - 3.0
1477 };
1478 renderer.draw_text(
1479 &tick.label,
1480 Point::new(label_x, y),
1481 &y_label_style,
1482 Affine::IDENTITY,
1483 );
1484 }
1485 }
1486
1487 fn draw_labels(
1493 &self,
1494 renderer: &mut impl Renderer,
1495 plot_area: &Rect,
1496 bounds: &Rect,
1497 theme: &Theme,
1498 ) {
1499 if let Some(title) = &self.title {
1501 let style = TextStyle {
1502 size: theme.title_size,
1503 color: theme.text_color,
1504 weight: theme.title_weight,
1505 family: theme.font_family.clone(),
1506 halign: HAlign::Center,
1507 valign: VAlign::Bottom,
1508 };
1509 let x = plot_area.x + plot_area.width / 2.0;
1510 let y = plot_area.y - 10.0;
1511 renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
1512 }
1513
1514 if let Some(xlabel) = &self.xlabel {
1516 let style = TextStyle {
1517 size: theme.axis_label_size,
1518 color: theme.text_color,
1519 weight: FontWeight::Normal,
1520 family: theme.font_family.clone(),
1521 halign: HAlign::Center,
1522 valign: VAlign::Top,
1523 };
1524 let x = plot_area.x + plot_area.width / 2.0;
1525 let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
1527 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
1528 }
1529
1530 if let Some(ylabel) = &self.ylabel {
1532 let style = TextStyle {
1533 size: theme.axis_label_size,
1534 color: theme.text_color,
1535 weight: FontWeight::Normal,
1536 family: theme.font_family.clone(),
1537 halign: HAlign::Center,
1538 valign: VAlign::Bottom,
1539 };
1540 let x = bounds.x + 4.0;
1541 let y = plot_area.y + plot_area.height / 2.0;
1542 let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
1544 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
1545 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
1546 let transform = translate_to * rotate * translate_back;
1547 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
1548 }
1549 }
1550
1551 fn draw_boxplot(
1559 &self,
1560 renderer: &mut impl Renderer,
1561 artist: &BoxPlotArtist,
1562 plot_area: &Rect,
1563 xmin: f64,
1564 xmax: f64,
1565 ymin: f64,
1566 ymax: f64,
1567 ) {
1568 let n = artist.stats.len();
1569 if n == 0 {
1570 return;
1571 }
1572
1573 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1574 let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
1575 let paint = Paint::new(stroke_color);
1576 let thin = Stroke::new(1.0);
1577 let thick = Stroke::new(2.0);
1578 let hair = Stroke::new(0.5);
1579
1580 for (i, stats) in artist.stats.iter().enumerate() {
1581 let cx = i as f64 + 0.5;
1582 let half = artist.box_width / 2.0;
1583 let left = cx - half;
1584 let right = cx + half;
1585
1586 let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
1588 let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
1589 let box_rect_path = {
1590 let mut p = Path::new();
1591 p.move_to(tl.x, tl.y);
1592 p.line_to(br.x, tl.y);
1593 p.line_to(br.x, br.y);
1594 p.line_to(tl.x, br.y);
1595 p.close();
1596 p
1597 };
1598 renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
1599 renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
1600
1601 let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
1603 let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
1604 let mut median_path = Path::new();
1605 median_path.move_to(ml.x, ml.y);
1606 median_path.line_to(mr.x, mr.y);
1607 renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
1608
1609 let wl_bottom = self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1611 let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
1612 let mut wl_path = Path::new();
1613 wl_path.move_to(wl_top.x, wl_top.y);
1614 wl_path.line_to(wl_bottom.x, wl_bottom.y);
1615 renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
1616
1617 let cap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1619 let cap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
1620 let mut cap_path = Path::new();
1621 cap_path.move_to(cap_left.x, cap_left.y);
1622 cap_path.line_to(cap_right.x, cap_right.y);
1623 renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
1624
1625 let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
1627 let wu_top = self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1628 let mut wu_path = Path::new();
1629 wu_path.move_to(wu_bottom.x, wu_bottom.y);
1630 wu_path.line_to(wu_top.x, wu_top.y);
1631 renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
1632
1633 let ucap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1635 let ucap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
1636 let mut ucap_path = Path::new();
1637 ucap_path.move_to(ucap_left.x, ucap_left.y);
1638 ucap_path.line_to(ucap_right.x, ucap_right.y);
1639 renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
1640
1641 if artist.show_outliers {
1643 let r = 3.0;
1644 for &val in &stats.outliers {
1645 let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
1646 let mut dot = Path::new();
1647 for seg in 0..8 {
1648 let angle = std::f64::consts::TAU * seg as f64 / 8.0;
1649 let dx = r * angle.cos();
1650 let dy = r * angle.sin();
1651 if seg == 0 {
1652 dot.move_to(pt.x + dx, pt.y + dy);
1653 } else {
1654 dot.line_to(pt.x + dx, pt.y + dy);
1655 }
1656 }
1657 dot.close();
1658 renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
1659 renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
1660 }
1661 }
1662 }
1663 }
1664
1665 fn draw_step(
1672 &self,
1673 renderer: &mut impl Renderer,
1674 artist: &StepArtist,
1675 plot_area: &Rect,
1676 xmin: f64,
1677 xmax: f64,
1678 ymin: f64,
1679 ymax: f64,
1680 ) {
1681 if artist.x.len() < 2 {
1682 return;
1683 }
1684 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1685 let paint = Paint::new(color);
1686 let stroke = Stroke::new(artist.width);
1687
1688 let mut path = Path::new();
1689 let first = self.data_to_pixel(
1690 artist.x.data[0], artist.y.data[0],
1691 plot_area, xmin, xmax, ymin, ymax,
1692 );
1693 path.move_to(first.x, first.y);
1694
1695 for i in 1..artist.x.len() {
1696 let prev = self.data_to_pixel(
1697 artist.x.data[i - 1], artist.y.data[i - 1],
1698 plot_area, xmin, xmax, ymin, ymax,
1699 );
1700 let cur = self.data_to_pixel(
1701 artist.x.data[i], artist.y.data[i],
1702 plot_area, xmin, xmax, ymin, ymax,
1703 );
1704 match artist.where_step {
1705 StepWhere::Pre => {
1706 path.line_to(prev.x, cur.y);
1707 path.line_to(cur.x, cur.y);
1708 }
1709 StepWhere::Post => {
1710 path.line_to(cur.x, prev.y);
1711 path.line_to(cur.x, cur.y);
1712 }
1713 StepWhere::Mid => {
1714 let mid_x = (prev.x + cur.x) / 2.0;
1715 path.line_to(mid_x, prev.y);
1716 path.line_to(mid_x, cur.y);
1717 path.line_to(cur.x, cur.y);
1718 }
1719 }
1720 }
1721 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1722 }
1723
1724 fn draw_stem(
1726 &self,
1727 renderer: &mut impl Renderer,
1728 artist: &StemArtist,
1729 plot_area: &Rect,
1730 xmin: f64,
1731 xmax: f64,
1732 ymin: f64,
1733 ymax: f64,
1734 ) {
1735 if artist.x.is_empty() {
1736 return;
1737 }
1738 let alpha_byte = (artist.alpha * 255.0) as u8;
1739 let color = artist.color.with_alpha(alpha_byte);
1740 let paint = Paint::new(color);
1741 let stroke = Stroke::new(artist.line_width);
1742 let radius = artist.marker_size / 2.0;
1743
1744 let bl_left = self.data_to_pixel(
1746 artist.x.data[0], artist.baseline,
1747 plot_area, xmin, xmax, ymin, ymax,
1748 );
1749 let bl_right = self.data_to_pixel(
1750 *artist.x.data.last().unwrap(), artist.baseline,
1751 plot_area, xmin, xmax, ymin, ymax,
1752 );
1753 let mut bl_path = Path::new();
1754 bl_path.move_to(bl_left.x, bl_left.y);
1755 bl_path.line_to(bl_right.x, bl_right.y);
1756 let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
1757 let bl_stroke = Stroke::new(0.8);
1758 renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
1759
1760 for i in 0..artist.x.len() {
1762 let base = self.data_to_pixel(
1763 artist.x.data[i], artist.baseline,
1764 plot_area, xmin, xmax, ymin, ymax,
1765 );
1766 let tip = self.data_to_pixel(
1767 artist.x.data[i], artist.y.data[i],
1768 plot_area, xmin, xmax, ymin, ymax,
1769 );
1770 let mut stem_path = Path::new();
1771 stem_path.move_to(base.x, base.y);
1772 stem_path.line_to(tip.x, tip.y);
1773 renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
1774 let marker = Path::circle(tip, radius);
1775 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
1776 }
1777 }
1778
1779 fn draw_errorbar(
1782 &self,
1783 renderer: &mut impl Renderer,
1784 artist: &ErrorBarArtist,
1785 plot_area: &Rect,
1786 xmin: f64,
1787 xmax: f64,
1788 ymin: f64,
1789 ymax: f64,
1790 ) {
1791 if artist.x.is_empty() {
1792 return;
1793 }
1794 let paint = Paint::new(artist.color);
1795 let stroke = Stroke::new(artist.line_width);
1796 let marker_radius = 3.0;
1797
1798 let mut line_path = Path::new();
1800 let first = self.data_to_pixel(
1801 artist.x.data[0], artist.y.data[0],
1802 plot_area, xmin, xmax, ymin, ymax,
1803 );
1804 line_path.move_to(first.x, first.y);
1805 for i in 1..artist.x.len() {
1806 let pt = self.data_to_pixel(
1807 artist.x.data[i], artist.y.data[i],
1808 plot_area, xmin, xmax, ymin, ymax,
1809 );
1810 line_path.line_to(pt.x, pt.y);
1811 }
1812 renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
1813
1814 for i in 0..artist.x.len() {
1816 let xv = artist.x.data[i];
1817 let yv = artist.y.data[i];
1818 let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
1819
1820 let marker = Path::circle(center, marker_radius);
1822 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
1823
1824 if let Some(ref yerr) = artist.yerr {
1826 let (lo, hi) = match yerr {
1827 ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
1828 ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
1829 };
1830 let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
1831 let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
1832
1833 let mut bar = Path::new();
1835 bar.move_to(pt_lo.x, pt_lo.y);
1836 bar.line_to(pt_hi.x, pt_hi.y);
1837 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
1838
1839 if artist.cap_size > 0.0 {
1841 let half_cap = artist.cap_size / 2.0;
1842 let mut cap_lo = Path::new();
1843 cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
1844 cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
1845 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
1846
1847 let mut cap_hi = Path::new();
1848 cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
1849 cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
1850 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
1851 }
1852 }
1853
1854 if let Some(ref xerr) = artist.xerr {
1856 let (lo, hi) = match xerr {
1857 ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
1858 ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
1859 };
1860 let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
1861 let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
1862
1863 let mut bar = Path::new();
1865 bar.move_to(pt_lo.x, pt_lo.y);
1866 bar.line_to(pt_hi.x, pt_hi.y);
1867 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
1868
1869 if artist.cap_size > 0.0 {
1871 let half_cap = artist.cap_size / 2.0;
1872 let mut cap_lo = Path::new();
1873 cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
1874 cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
1875 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
1876
1877 let mut cap_hi = Path::new();
1878 cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
1879 cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
1880 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
1881 }
1882 }
1883 }
1884 }
1885
1886 fn draw_heatmap(
1889 &self,
1890 renderer: &mut impl Renderer,
1891 artist: &HeatmapArtist,
1892 plot_area: &Rect,
1893 xmin: f64,
1894 xmax: f64,
1895 ymin: f64,
1896 ymax: f64,
1897 ) {
1898 let nrows = artist.data.len();
1899 if nrows == 0 {
1900 return;
1901 }
1902 let ncols = artist.data[0].len();
1903 if ncols == 0 {
1904 return;
1905 }
1906
1907 let vmin = artist.effective_vmin();
1908 let vmax = artist.effective_vmax();
1909
1910 let text_style = TextStyle {
1911 size: 10.0,
1912 color: Color::BLACK,
1913 weight: FontWeight::Normal,
1914 family: None,
1915 halign: HAlign::Center,
1916 valign: VAlign::Middle,
1917 };
1918
1919 for row in 0..nrows {
1920 for col in 0..ncols {
1921 let val = artist.data[row][col];
1922 let cell_color = artist.cmap.map_value(val, vmin, vmax);
1923
1924 let p_bl = self.data_to_pixel(
1926 col as f64, row as f64,
1927 plot_area, xmin, xmax, ymin, ymax,
1928 );
1929 let p_tr = self.data_to_pixel(
1930 (col + 1) as f64, (row + 1) as f64,
1931 plot_area, xmin, xmax, ymin, ymax,
1932 );
1933 let rect = Rect::from_points(p_bl, p_tr);
1934 let cell_path = Path::rect(rect);
1935 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
1936
1937 if artist.show_values {
1939 let cx = (p_bl.x + p_tr.x) / 2.0;
1940 let cy = (p_bl.y + p_tr.y) / 2.0;
1941 let label = format!("{val:.1}");
1942 renderer.draw_text(
1943 &label,
1944 Point::new(cx, cy),
1945 &text_style,
1946 Affine::IDENTITY,
1947 );
1948 }
1949 }
1950 }
1951 }
1952
1953 fn draw_legend(
1954 &self,
1955 renderer: &mut impl Renderer,
1956 plot_area: &Rect,
1957 theme: &Theme,
1958 ) {
1959 let entries: Vec<LegendEntry> = self
1962 .artists
1963 .iter()
1964 .filter_map(|a| {
1965 let (label, color, swatch) = match a {
1966 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1967 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1968 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1969 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1970 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1971 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1972 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1973 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1974 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1975 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1976 };
1977 label.map(|l| LegendEntry { label: l.to_string(), color, swatch })
1978 })
1979 .collect();
1980
1981 legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
1982 }
1983
1984 fn data_to_pixel(
1994 &self,
1995 x: f64,
1996 y: f64,
1997 plot_area: &Rect,
1998 xmin: f64,
1999 xmax: f64,
2000 ymin: f64,
2001 ymax: f64,
2002 ) -> Point {
2003 let tx = self.xscale.transform(x, xmin, xmax);
2004 let ty = self.yscale.transform(y, ymin, ymax);
2005 Point::new(
2006 plot_area.x + tx * plot_area.width,
2007 plot_area.y + (1.0 - ty) * plot_area.height, )
2009 }
2010}
2011
2012#[cfg(test)]
2017mod tests {
2018 use super::*;
2019
2020 #[test]
2021 fn new_axes_has_defaults() {
2022 let ax = Axes::new();
2023 assert!(ax.artists.is_empty());
2024 assert!(ax.title.is_none());
2025 assert!(ax.xlabel.is_none());
2026 assert!(ax.ylabel.is_none());
2027 assert!(ax.xlim.is_none());
2028 assert!(ax.ylim.is_none());
2029 assert!(!ax.show_legend);
2030 assert_eq!(ax.color_index, 0);
2031 }
2032
2033 #[test]
2034 fn plot_creates_line_artist() {
2035 let mut ax = Axes::new();
2036 let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
2037 assert!(result.is_ok());
2038 assert_eq!(ax.artists.len(), 1);
2039 assert!(matches!(&ax.artists[0], Artist::Line(_)));
2040 assert_eq!(ax.color_index, 1);
2041 }
2042
2043 #[test]
2044 fn plot_length_mismatch() {
2045 let mut ax = Axes::new();
2046 let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
2047 assert!(matches!(
2048 result,
2049 Err(PlotError::SeriesLengthMismatch { expected: 2, got: 1 })
2050 ));
2051 }
2052
2053 #[test]
2054 fn plot_empty_data() {
2055 let mut ax = Axes::new();
2056 let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
2057 assert!(matches!(result, Err(PlotError::EmptyData)));
2058 }
2059
2060 #[test]
2061 fn scatter_creates_artist() {
2062 let mut ax = Axes::new();
2063 let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
2064 assert!(result.is_ok());
2065 assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
2066 }
2067
2068 #[test]
2069 fn bar_creates_artist() {
2070 let mut ax = Axes::new();
2071 let cats: &[&str] = &["a", "b", "c"];
2072 let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
2073 assert!(result.is_ok());
2074 match &ax.artists[0] {
2075 Artist::Bar(a) => {
2076 assert!(!a.horizontal);
2077 assert_eq!(a.categories.len(), 3);
2078 }
2079 _ => panic!("expected Bar artist"),
2080 }
2081 }
2082
2083 #[test]
2084 fn barh_creates_horizontal_artist() {
2085 let mut ax = Axes::new();
2086 let cats: &[&str] = &["x", "y"];
2087 let result = ax.barh(cats, vec![5.0, 10.0]);
2088 assert!(result.is_ok());
2089 match &ax.artists[0] {
2090 Artist::Bar(a) => assert!(a.horizontal),
2091 _ => panic!("expected Bar artist"),
2092 }
2093 }
2094
2095 #[test]
2096 fn hist_computes_bins() {
2097 let mut ax = Axes::new();
2098 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
2099 let result = ax.hist(data, 5);
2100 assert!(result.is_ok());
2101 match &ax.artists[0] {
2102 Artist::Histogram(a) => {
2103 assert_eq!(a.bin_edges.len(), 6); assert_eq!(a.counts.len(), 5);
2105 let total: f64 = a.counts.iter().sum();
2107 assert_eq!(total, 10.0);
2108 }
2109 _ => panic!("expected Hist artist"),
2110 }
2111 }
2112
2113 #[test]
2114 fn hist_single_value() {
2115 let mut ax = Axes::new();
2116 let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
2117 assert!(result.is_ok());
2118 match &ax.artists[0] {
2119 Artist::Histogram(a) => {
2120 let total: f64 = a.counts.iter().sum();
2121 assert_eq!(total, 3.0);
2122 }
2123 _ => panic!("expected Hist artist"),
2124 }
2125 }
2126
2127 #[test]
2128 fn hist_empty_data() {
2129 let mut ax = Axes::new();
2130 let result = ax.hist(Vec::<f64>::new(), 10);
2131 assert!(matches!(result, Err(PlotError::EmptyData)));
2132 }
2133
2134 #[test]
2135 fn fill_between_creates_artist() {
2136 let mut ax = Axes::new();
2137 let result = ax.fill_between(
2138 vec![1.0, 2.0, 3.0],
2139 vec![1.0, 2.0, 1.0],
2140 vec![0.0, 0.0, 0.0],
2141 );
2142 assert!(result.is_ok());
2143 assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
2144 }
2145
2146 #[test]
2147 fn fill_between_length_mismatch() {
2148 let mut ax = Axes::new();
2149 let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
2150 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2151 }
2152
2153 #[test]
2154 fn configuration_methods_return_self() {
2155 let mut ax = Axes::new();
2156 ax.set_title("Test")
2157 .set_xlabel("X")
2158 .set_ylabel("Y")
2159 .set_xlim(0.0, 10.0)
2160 .set_ylim(-1.0, 1.0)
2161 .set_xscale(Scale::Linear)
2162 .set_yscale(Scale::Log10)
2163 .grid(true)
2164 .legend();
2165
2166 assert_eq!(ax.title.as_deref(), Some("Test"));
2167 assert_eq!(ax.xlabel.as_deref(), Some("X"));
2168 assert_eq!(ax.ylabel.as_deref(), Some("Y"));
2169 assert_eq!(ax.xlim, Some((0.0, 10.0)));
2170 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
2171 assert_eq!(ax.show_grid, Some(true));
2172 assert!(ax.show_legend);
2173 }
2174
2175 #[test]
2176 fn color_cycle_advances() {
2177 let mut ax = Axes::new();
2178 for _ in 0..12 {
2179 ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2180 }
2181 assert_eq!(ax.color_index, 12);
2182 match (&ax.artists[0], &ax.artists[10]) {
2185 (Artist::Line(a), Artist::Line(b)) => {
2186 assert_eq!(a.color, b.color);
2187 }
2188 _ => panic!("expected Line artists"),
2189 }
2190 }
2191
2192 #[test]
2193 fn data_to_pixel_linear() {
2194 let ax = Axes::new();
2195 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
2196
2197 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2199 assert!((p.x - 100.0).abs() < 1e-10);
2200 assert!((p.y - 350.0).abs() < 1e-10); let p = ax.data_to_pixel(10.0, 10.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2204 assert!((p.x - 500.0).abs() < 1e-10);
2205 assert!((p.y - 50.0).abs() < 1e-10); let p = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
2209 assert!((p.x - 300.0).abs() < 1e-10);
2210 assert!((p.y - 200.0).abs() < 1e-10);
2211 }
2212
2213 #[test]
2214 fn compute_data_limits_no_artists() {
2215 let ax = Axes::new();
2216 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2217 assert!(xmin < xmax);
2219 assert!(ymin < ymax);
2220 }
2221
2222 #[test]
2223 fn compute_data_limits_with_user_override() {
2224 let mut ax = Axes::new();
2225 ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
2226 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2227 assert!((xmin - (-5.0)).abs() < f64::EPSILON);
2228 assert!((xmax - 5.0).abs() < f64::EPSILON);
2229 assert!((ymin - 0.0).abs() < f64::EPSILON);
2230 assert!((ymax - 100.0).abs() < f64::EPSILON);
2231 }
2232
2233 #[test]
2234 fn compute_data_limits_from_line_data() {
2235 let mut ax = Axes::new();
2236 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
2237 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2238 assert!(xmin < 1.0);
2240 assert!(xmax > 10.0);
2241 assert!(ymin < 2.0);
2242 assert!(ymax > 8.0);
2243 }
2244 #[test]
2247 fn step_creates_artist() {
2248 let mut ax = Axes::new();
2249 let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
2250 assert!(result.is_ok());
2251 assert!(matches!(&ax.artists[0], Artist::Step(_)));
2252 }
2253
2254 #[test]
2255 fn step_length_mismatch() {
2256 let mut ax = Axes::new();
2257 let result = ax.step(vec![1.0, 2.0], vec![1.0]);
2258 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2259 }
2260
2261 #[test]
2262 fn step_empty_data() {
2263 let mut ax = Axes::new();
2264 let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
2265 assert!(matches!(result, Err(PlotError::EmptyData)));
2266 }
2267
2268 #[test]
2269 fn step_default_where() {
2270 let mut ax = Axes::new();
2271 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2272 match &ax.artists[0] {
2273 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
2274 _ => panic!("expected Step"),
2275 }
2276 }
2277
2278 #[test]
2279 fn step_color_cycle() {
2280 let mut ax = Axes::new();
2281 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2282 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2283 let c0 = ax.artists[0].color();
2284 let c1 = ax.artists[1].color();
2285 assert_ne!(c0, c1);
2286 }
2287
2288 #[test]
2289 fn step_builder_chaining() {
2290 let mut ax = Axes::new();
2291 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2292 .unwrap()
2293 .color(Color::TAB_RED)
2294 .width(3.0)
2295 .where_step(StepWhere::Post)
2296 .label("steps")
2297 .alpha(0.5);
2298 match &ax.artists[0] {
2299 Artist::Step(a) => {
2300 assert_eq!(a.color, Color::TAB_RED);
2301 assert!((a.width - 3.0).abs() < 1e-12);
2302 assert!(matches!(a.where_step, StepWhere::Post));
2303 assert_eq!(a.label.as_deref(), Some("steps"));
2304 assert!((a.alpha - 0.5).abs() < 1e-12);
2305 }
2306 _ => panic!("expected Step"),
2307 }
2308 }
2309
2310 #[test]
2311 fn step_data_bounds() {
2312 let mut ax = Axes::new();
2313 ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
2314 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2315 assert!(xmin < 1.0);
2316 assert!(xmax > 10.0);
2317 assert!(ymin < 2.0);
2318 assert!(ymax > 8.0);
2319 }
2320
2321 #[test]
2322 fn step_legend_label() {
2323 let mut ax = Axes::new();
2324 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
2325 assert_eq!(ax.artists[0].label(), Some("S"));
2326 }
2327
2328 #[test]
2329 fn step_default_alpha() {
2330 let mut ax = Axes::new();
2331 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2332 match &ax.artists[0] {
2333 Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
2334 _ => panic!("expected Step"),
2335 }
2336 }
2337
2338 #[test]
2339 fn step_default_width() {
2340 let mut ax = Axes::new();
2341 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2342 match &ax.artists[0] {
2343 Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
2344 _ => panic!("expected Step"),
2345 }
2346 }
2347
2348 #[test]
2349 fn step_mid_mode() {
2350 let mut ax = Axes::new();
2351 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2352 .unwrap()
2353 .where_step(StepWhere::Mid);
2354 match &ax.artists[0] {
2355 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
2356 _ => panic!("expected Step"),
2357 }
2358 }
2359
2360 #[test]
2363 fn stem_creates_artist() {
2364 let mut ax = Axes::new();
2365 let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
2366 assert!(result.is_ok());
2367 assert!(matches!(&ax.artists[0], Artist::Stem(_)));
2368 }
2369
2370 #[test]
2371 fn stem_length_mismatch() {
2372 let mut ax = Axes::new();
2373 let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
2374 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
2375 }
2376
2377 #[test]
2378 fn stem_empty_data() {
2379 let mut ax = Axes::new();
2380 let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
2381 assert!(matches!(result, Err(PlotError::EmptyData)));
2382 }
2383
2384 #[test]
2385 fn stem_default_baseline() {
2386 let mut ax = Axes::new();
2387 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2388 match &ax.artists[0] {
2389 Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
2390 _ => panic!("expected Stem"),
2391 }
2392 }
2393
2394 #[test]
2395 fn stem_default_marker_size() {
2396 let mut ax = Axes::new();
2397 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2398 match &ax.artists[0] {
2399 Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
2400 _ => panic!("expected Stem"),
2401 }
2402 }
2403
2404 #[test]
2405 fn stem_builder_chaining() {
2406 let mut ax = Axes::new();
2407 ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
2408 .unwrap()
2409 .color(Color::TAB_GREEN)
2410 .baseline(1.0)
2411 .marker_size(8.0)
2412 .width(2.0)
2413 .label("stems")
2414 .alpha(0.7);
2415 match &ax.artists[0] {
2416 Artist::Stem(a) => {
2417 assert_eq!(a.color, Color::TAB_GREEN);
2418 assert!((a.baseline - 1.0).abs() < 1e-12);
2419 assert!((a.marker_size - 8.0).abs() < 1e-12);
2420 assert!((a.line_width - 2.0).abs() < 1e-12);
2421 assert_eq!(a.label.as_deref(), Some("stems"));
2422 assert!((a.alpha - 0.7).abs() < 1e-12);
2423 }
2424 _ => panic!("expected Stem"),
2425 }
2426 }
2427
2428 #[test]
2429 fn stem_data_bounds_include_baseline() {
2430 let mut ax = Axes::new();
2431 ax.stem(vec![1.0, 5.0], vec![2.0, 8.0]).unwrap().baseline(-5.0);
2432 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
2433 assert!(ymin < -5.0);
2434 }
2435
2436 #[test]
2437 fn stem_legend_label() {
2438 let mut ax = Axes::new();
2439 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
2440 assert_eq!(ax.artists[0].label(), Some("L"));
2441 }
2442
2443 #[test]
2444 fn stem_color_cycle() {
2445 let mut ax = Axes::new();
2446 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2447 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2448 let c0 = ax.artists[0].color();
2449 let c1 = ax.artists[1].color();
2450 assert_ne!(c0, c1);
2451 }
2452
2453 #[test]
2454 fn stem_alpha_default() {
2455 let mut ax = Axes::new();
2456 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
2457 match &ax.artists[0] {
2458 Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
2459 _ => panic!("expected Stem"),
2460 }
2461 }
2462
2463 #[test]
2464 fn stem_negative_baseline() {
2465 let mut ax = Axes::new();
2466 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().baseline(-3.0);
2467 match &ax.artists[0] {
2468 Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
2469 _ => panic!("expected Stem"),
2470 }
2471 }
2472
2473}