1use crate::annotations::{Annotation, ArrowStyle, TextAnnotation};
9use crate::artist::*;
10use crate::colorbar::{self, Colorbar};
11use crate::error::{PlotError, Result};
12use crate::layout::{self, LayoutConfig};
13use crate::legend::{self, LegendEntry, SwatchKind};
14use crate::primitives::*;
15use crate::renderer::Renderer;
16use crate::scale::Scale;
17use crate::series::{IntoCategories, IntoSeries};
18use crate::theme::{GridAxis, LineStyle, Loc, Marker, Theme, TickDirection};
19use crate::ticks;
20
21const DEFAULT_TICK_COUNT: usize = 7;
27
28const AUTOSCALE_PAD: f64 = 0.05;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TwinSide {
39 Right,
41 Top,
43}
44
45#[derive(Debug)]
59pub struct Axes {
60 pub(crate) artists: Vec<Artist>,
62 pub(crate) title: Option<String>,
64 pub(crate) xlabel: Option<String>,
66 pub(crate) ylabel: Option<String>,
68 pub(crate) xlim: Option<(f64, f64)>,
70 pub(crate) ylim: Option<(f64, f64)>,
72 pub(crate) xscale: Scale,
74 pub(crate) yscale: Scale,
76 pub(crate) show_grid: Option<bool>,
78 pub(crate) grid_axis: GridAxis,
80 pub(crate) grid_alpha: Option<f64>,
83 pub(crate) grid_style: Option<LineStyle>,
85 pub(crate) x_inverted: bool,
87 pub(crate) y_inverted: bool,
89 pub(crate) custom_xticks: Option<Vec<f64>>,
91 pub(crate) custom_yticks: Option<Vec<f64>>,
93 pub(crate) custom_xticklabels: Option<Vec<String>>,
95 pub(crate) custom_yticklabels: Option<Vec<String>>,
97 pub(crate) xtick_rotation: f64,
99 pub(crate) ytick_rotation: f64,
101 pub(crate) show_legend: bool,
103 pub(crate) legend_loc: Loc,
105 pub(crate) theme_override: Option<Theme>,
107 pub(crate) texts: Vec<TextAnnotation>,
109 pub(crate) annotations: Vec<Annotation>,
111 pub(crate) color_index: usize,
114 pub(crate) is_twin: bool,
116 pub(crate) twin_side: Option<TwinSide>,
118 pub(crate) colorbar: Option<Colorbar>,
120}
121
122impl Axes {
127 pub(crate) fn new() -> Self {
129 Self {
130 artists: Vec::new(),
131 title: None,
132 xlabel: None,
133 ylabel: None,
134 xlim: None,
135 ylim: None,
136 xscale: Scale::default(),
137 yscale: Scale::default(),
138 show_grid: None,
139 grid_axis: GridAxis::default(),
140 grid_alpha: None,
141 grid_style: None,
142 x_inverted: false,
143 y_inverted: false,
144 custom_xticks: None,
145 custom_yticks: None,
146 custom_xticklabels: None,
147 custom_yticklabels: None,
148 xtick_rotation: 0.0,
149 ytick_rotation: 0.0,
150 show_legend: false,
151 legend_loc: Loc::Best,
152 theme_override: None,
153 texts: Vec::new(),
154 annotations: Vec::new(),
155 color_index: 0,
156 is_twin: false,
157 twin_side: None,
158 colorbar: None,
159 }
160 }
161
162 pub(crate) fn new_twin(side: TwinSide, color_index: usize) -> Self {
164 Self {
165 is_twin: true,
166 twin_side: Some(side),
167 color_index,
168 ..Self::new()
169 }
170 }
171
172 pub fn is_twin(&self) -> bool {
174 self.is_twin
175 }
176
177 pub fn twin_side(&self) -> Option<TwinSide> {
179 self.twin_side
180 }
181}
182
183impl Axes {
188 pub fn plot<X, Y>(&mut self, x: X, y: Y) -> Result<&mut LineArtist>
199 where
200 X: IntoSeries,
201 Y: IntoSeries,
202 {
203 let xs = x.into_series();
204 let ys = y.into_series();
205 if xs.len() != ys.len() {
206 return Err(PlotError::SeriesLengthMismatch {
207 expected: xs.len(),
208 got: ys.len(),
209 });
210 }
211 if xs.is_empty() {
212 return Err(PlotError::EmptyData);
213 }
214 let color = Color::TABLEAU_10[self.color_index % 10];
215 self.color_index += 1;
216 let artist = LineArtist {
217 x: xs,
218 y: ys,
219 color,
220 width: 1.5,
221 style: crate::theme::LineStyle::Solid,
222 label: None,
223 alpha: 1.0,
224 decimate: crate::decimate::DecimateMode::Auto,
225 };
226 self.artists.push(Artist::Line(artist));
227 match self.artists.last_mut().expect("just pushed") {
228 Artist::Line(a) => Ok(a),
229 _ => unreachable!(),
230 }
231 }
232
233 pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
242 where
243 X: IntoSeries,
244 Y: IntoSeries,
245 {
246 let xs = x.into_series();
247 let ys = y.into_series();
248 if xs.len() != ys.len() {
249 return Err(PlotError::SeriesLengthMismatch {
250 expected: xs.len(),
251 got: ys.len(),
252 });
253 }
254 if xs.is_empty() {
255 return Err(PlotError::EmptyData);
256 }
257 let color = Color::TABLEAU_10[self.color_index % 10];
258 self.color_index += 1;
259 let artist = ScatterArtist {
260 x: xs,
261 y: ys,
262 color,
263 marker: Marker::Circle,
264 size: 6.0,
265 label: None,
266 alpha: 0.8,
267 colors: None,
268 c: None,
269 cmap: None,
270 decimate: crate::decimate::DecimateMode::Auto,
271 };
272 self.artists.push(Artist::Scatter(artist));
273 match self.artists.last_mut().expect("just pushed") {
274 Artist::Scatter(a) => Ok(a),
275 _ => unreachable!(),
276 }
277 }
278
279 pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
289 where
290 C: IntoCategories,
291 H: IntoSeries,
292 {
293 let cats = categories.into_categories();
294 let vals = heights.into_series();
295 if cats.len() != vals.len() {
296 return Err(PlotError::SeriesLengthMismatch {
297 expected: cats.len(),
298 got: vals.len(),
299 });
300 }
301 if cats.is_empty() {
302 return Err(PlotError::EmptyData);
303 }
304 let color = Color::TABLEAU_10[self.color_index % 10];
305 self.color_index += 1;
306 let artist = BarArtist {
307 categories: cats,
308 heights: vals,
309 color,
310 horizontal: false,
311 bar_width: 0.8,
312 label: None,
313 alpha: 1.0,
314 bottom: None,
315 offset: None,
316 };
317 self.artists.push(Artist::Bar(artist));
318 match self.artists.last_mut().expect("just pushed") {
319 Artist::Bar(a) => Ok(a),
320 _ => unreachable!(),
321 }
322 }
323
324 pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
333 where
334 C: IntoCategories,
335 W: IntoSeries,
336 {
337 let cats = categories.into_categories();
338 let vals = widths.into_series();
339 if cats.len() != vals.len() {
340 return Err(PlotError::SeriesLengthMismatch {
341 expected: cats.len(),
342 got: vals.len(),
343 });
344 }
345 if cats.is_empty() {
346 return Err(PlotError::EmptyData);
347 }
348 let color = Color::TABLEAU_10[self.color_index % 10];
349 self.color_index += 1;
350 let artist = BarArtist {
351 categories: cats,
352 heights: vals,
353 color,
354 horizontal: true,
355 bar_width: 0.8,
356 label: None,
357 alpha: 1.0,
358 bottom: None,
359 offset: None,
360 };
361 self.artists.push(Artist::Bar(artist));
362 match self.artists.last_mut().expect("just pushed") {
363 Artist::Bar(a) => Ok(a),
364 _ => unreachable!(),
365 }
366 }
367
368 pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
378 where
379 D: IntoSeries,
380 {
381 let series = data.into_series();
382 if series.is_empty() {
383 return Err(PlotError::EmptyData);
384 }
385 let bins = bins.max(1);
386
387 let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
389
390 let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
392 (data_min - 0.5, data_max + 0.5)
393 } else {
394 (data_min, data_max)
395 };
396
397 let bin_width = (hi - lo) / bins as f64;
398
399 let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
401 *edges.last_mut().expect("edges is non-empty") = hi;
403
404 let mut counts = vec![0.0f64; bins];
406 for &v in &series.data {
407 if !v.is_finite() {
408 continue;
409 }
410 let idx = if v >= hi {
412 bins - 1
414 } else {
415 let raw = ((v - lo) / bin_width) as usize;
416 raw.min(bins - 1)
417 };
418 counts[idx] += 1.0;
419 }
420
421 let color = Color::TABLEAU_10[self.color_index % 10];
422 self.color_index += 1;
423 let artist = HistArtist {
424 data: series,
425 bins,
426 bin_edges: edges,
427 counts,
428 color,
429 label: None,
430 alpha: 0.85,
431 density: false,
432 };
433
434 self.artists.push(Artist::Histogram(artist));
435 match self.artists.last_mut().expect("just pushed") {
436 Artist::Histogram(a) => Ok(a),
437 _ => unreachable!(),
438 }
439 }
440
441 pub fn fill_between<X, Y1, Y2>(
450 &mut self,
451 x: X,
452 y1: Y1,
453 y2: Y2,
454 ) -> Result<&mut FillBetweenArtist>
455 where
456 X: IntoSeries,
457 Y1: IntoSeries,
458 Y2: IntoSeries,
459 {
460 let xs = x.into_series();
461 let y1s = y1.into_series();
462 let y2s = y2.into_series();
463 if xs.len() != y1s.len() {
464 return Err(PlotError::SeriesLengthMismatch {
465 expected: xs.len(),
466 got: y1s.len(),
467 });
468 }
469 if xs.len() != y2s.len() {
470 return Err(PlotError::SeriesLengthMismatch {
471 expected: xs.len(),
472 got: y2s.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 = FillBetweenArtist {
481 x: xs,
482 y1: y1s,
483 y2: y2s,
484 color,
485 label: None,
486 alpha: 0.3,
487 };
488 self.artists.push(Artist::FillBetween(artist));
489 match self.artists.last_mut().expect("just pushed") {
490 Artist::FillBetween(a) => Ok(a),
491 _ => unreachable!(),
492 }
493 }
494
495 pub fn step<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<&mut StepArtist> {
504 let xs = x.into_series();
505 let ys = y.into_series();
506 if xs.len() != ys.len() {
507 return Err(PlotError::SeriesLengthMismatch {
508 expected: xs.len(),
509 got: ys.len(),
510 });
511 }
512 if xs.is_empty() {
513 return Err(PlotError::EmptyData);
514 }
515 let color = Color::TABLEAU_10[self.color_index % 10];
516 self.color_index += 1;
517 let artist = StepArtist {
518 x: xs,
519 y: ys,
520 color,
521 width: 1.5,
522 where_step: StepWhere::Pre,
523 label: None,
524 alpha: 1.0,
525 };
526 self.artists.push(Artist::Step(artist));
527 match self.artists.last_mut().expect("just pushed") {
528 Artist::Step(a) => Ok(a),
529 _ => unreachable!(),
530 }
531 }
532
533 pub fn stem<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<&mut StemArtist> {
542 let xs = x.into_series();
543 let ys = y.into_series();
544 if xs.len() != ys.len() {
545 return Err(PlotError::SeriesLengthMismatch {
546 expected: xs.len(),
547 got: ys.len(),
548 });
549 }
550 if xs.is_empty() {
551 return Err(PlotError::EmptyData);
552 }
553 let color = Color::TABLEAU_10[self.color_index % 10];
554 self.color_index += 1;
555 let artist = StemArtist {
556 x: xs,
557 y: ys,
558 color,
559 line_width: 1.5,
560 marker_size: 6.0,
561 baseline: 0.0,
562 label: None,
563 alpha: 1.0,
564 };
565 self.artists.push(Artist::Stem(artist));
566 match self.artists.last_mut().expect("just pushed") {
567 Artist::Stem(a) => Ok(a),
568 _ => unreachable!(),
569 }
570 }
571
572 pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
581 use crate::charts::boxplot::compute_stats;
582 if datasets.is_empty() {
583 return Err(PlotError::EmptyData);
584 }
585 let color = Color::TABLEAU_10[self.color_index % 10];
586 self.color_index += 1;
587 let factor = 1.5;
588 let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
589 let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
590 let artist = BoxPlotArtist {
591 stats,
592 labels,
593 color,
594 label: None,
595 alpha: 1.0,
596 box_width: 0.5,
597 show_outliers: true,
598 whisker_iq_factor: factor,
599 raw_data: datasets,
600 };
601 self.artists.push(Artist::BoxPlot(artist));
602 match self.artists.last_mut().expect("just pushed") {
603 Artist::BoxPlot(a) => Ok(a),
604 _ => unreachable!(),
605 }
606 }
607
608 pub fn errorbar<X: IntoSeries, Y: IntoSeries>(&mut self, x: X, y: Y) -> Result<ErrorBarArtist> {
619 let xs = x.into_series();
620 let ys = y.into_series();
621 if xs.len() != ys.len() {
622 return Err(PlotError::SeriesLengthMismatch {
623 expected: xs.len(),
624 got: ys.len(),
625 });
626 }
627 if xs.is_empty() {
628 return Err(PlotError::EmptyData);
629 }
630 let color = Color::TABLEAU_10[self.color_index % 10];
631 self.color_index += 1;
632 Ok(ErrorBarArtist {
633 x: xs,
634 y: ys,
635 xerr: None,
636 yerr: None,
637 color,
638 label: None,
639 cap_size: 4.0,
640 line_width: 1.0,
641 })
642 }
643
644 pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
649 self.artists.push(Artist::ErrorBar(artist));
650 }
651
652 pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
661 if data.is_empty() {
662 return Err(PlotError::EmptyData);
663 }
664 let color = Color::TABLEAU_10[self.color_index % 10];
665 self.color_index += 1;
666 let artist = HeatmapArtist {
667 data,
668 x_labels: None,
669 y_labels: None,
670 cmap: crate::colormap::Colormap::Viridis,
671 vmin: None,
672 vmax: None,
673 show_values: false,
674 color,
675 label: None,
676 show_colorbar: false,
677 };
678 self.artists.push(Artist::Heatmap(artist));
679 match self.artists.last_mut().expect("just pushed") {
680 Artist::Heatmap(a) => Ok(a),
681 _ => unreachable!(),
682 }
683 }
684
685 pub fn pie<S: IntoSeries>(&mut self, sizes: S) -> Result<&mut PieArtist> {
696 let series = sizes.into_series();
697 if series.is_empty() {
698 return Err(PlotError::EmptyData);
699 }
700 let color = Color::TABLEAU_10[self.color_index % 10];
701 self.color_index += 1;
702 let artist = PieArtist {
703 sizes: series.data,
704 labels: None,
705 colors: None,
706 explode: None,
707 autopct: false,
708 start_angle: 90.0,
709 radius: 1.0,
710 label: None,
711 color,
712 };
713 self.artists.push(Artist::Pie(artist));
714 match self.artists.last_mut().expect("just pushed") {
715 Artist::Pie(a) => Ok(a),
716 _ => unreachable!(),
717 }
718 }
719}
720
721impl Axes {
726 pub fn set_title(&mut self, title: &str) -> &mut Self {
730 self.title = Some(crate::text::format_markup(title));
731 self
732 }
733
734 pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
736 self.xlabel = Some(crate::text::format_markup(label));
737 self
738 }
739
740 pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
742 self.ylabel = Some(crate::text::format_markup(label));
743 self
744 }
745
746 pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
748 self.xlim = Some((min, max));
749 self
750 }
751
752 pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
754 self.ylim = Some((min, max));
755 self
756 }
757
758 pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
760 self.xscale = scale;
761 self
762 }
763
764 pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
766 self.yscale = scale;
767 self
768 }
769
770 pub fn grid(&mut self, show: bool) -> &mut Self {
772 self.show_grid = Some(show);
773 self
774 }
775
776 pub fn legend(&mut self) -> &mut Self {
778 self.show_legend = true;
779 self
780 }
781
782 pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
784 self.legend_loc = loc;
785 self
786 }
787
788 pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
790 self.theme_override = Some(theme);
791 self
792 }
793
794 pub fn grid_axis(&mut self, axis: &str) -> &mut Self {
799 self.grid_axis = match axis {
800 "x" => GridAxis::X,
801 "y" => GridAxis::Y,
802 _ => GridAxis::Both,
803 };
804 self
805 }
806
807 pub fn grid_alpha(&mut self, alpha: f64) -> &mut Self {
809 self.grid_alpha = Some(alpha.clamp(0.0, 1.0));
810 self
811 }
812
813 pub fn grid_style(&mut self, style: LineStyle) -> &mut Self {
815 self.grid_style = Some(style);
816 self
817 }
818
819 pub fn invert_xaxis(&mut self) -> &mut Self {
821 self.x_inverted = true;
822 self
823 }
824
825 pub fn invert_yaxis(&mut self) -> &mut Self {
827 self.y_inverted = true;
828 self
829 }
830
831 pub fn set_xticks(&mut self, ticks: &[f64]) -> &mut Self {
835 self.custom_xticks = Some(ticks.to_vec());
836 self
837 }
838
839 pub fn set_yticks(&mut self, ticks: &[f64]) -> &mut Self {
843 self.custom_yticks = Some(ticks.to_vec());
844 self
845 }
846
847 pub fn set_xticklabels(&mut self, labels: &[&str]) -> &mut Self {
853 self.custom_xticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
854 self
855 }
856
857 pub fn set_yticklabels(&mut self, labels: &[&str]) -> &mut Self {
863 self.custom_yticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
864 self
865 }
866
867 pub fn tick_params_x_rotation(&mut self, degrees: f64) -> &mut Self {
869 self.xtick_rotation = degrees;
870 self
871 }
872
873 pub fn tick_params_y_rotation(&mut self, degrees: f64) -> &mut Self {
875 self.ytick_rotation = degrees;
876 self
877 }
878
879 pub fn text(&mut self, x: f64, y: f64, text: &str) -> &mut TextAnnotation {
887 self.texts.push(TextAnnotation {
888 text: crate::text::format_markup(text),
889 x,
890 y,
891 fontsize: None,
892 color: None,
893 ha: HAlign::Left,
894 va: VAlign::Baseline,
895 rotation: 0.0,
896 });
897 self.texts.last_mut().expect("just pushed")
898 }
899
900 pub fn annotate(&mut self, text: &str, xy: (f64, f64), xytext: (f64, f64)) -> &mut Annotation {
909 self.annotations.push(Annotation {
910 text: crate::text::format_markup(text),
911 xy,
912 xytext,
913 fontsize: None,
914 color: None,
915 ha: HAlign::Center,
916 va: VAlign::Bottom,
917 arrowstyle: ArrowStyle::None,
918 arrow_color: None,
919 });
920 self.annotations.last_mut().expect("just pushed")
921 }
922}
923
924impl Axes {
929 pub fn colorbar(
931 &mut self,
932 cmap: crate::colormap::Colormap,
933 vmin: f64,
934 vmax: f64,
935 ) -> &mut Colorbar {
936 self.colorbar = Some(Colorbar::new(cmap, vmin, vmax));
937 self.colorbar.as_mut().expect("just set")
938 }
939}
940
941impl Axes {
946 pub fn contour(
948 &mut self,
949 x: &[f64],
950 y: &[f64],
951 z: Vec<Vec<f64>>,
952 ) -> Result<&mut ContourArtist> {
953 if x.is_empty() || y.is_empty() || z.is_empty() {
954 return Err(PlotError::EmptyData);
955 }
956 let color = Color::TABLEAU_10[self.color_index % 10];
957 self.color_index += 1;
958 let artist = ContourArtist {
959 x: x.to_vec(),
960 y: y.to_vec(),
961 z,
962 levels: None,
963 filled: false,
964 cmap: crate::colormap::Colormap::Viridis,
965 colors: None,
966 linewidths: 1.0,
967 label: None,
968 color,
969 num_levels: 10,
970 };
971 self.artists.push(Artist::Contour(artist));
972 match self.artists.last_mut().expect("just pushed") {
973 Artist::Contour(a) => Ok(a),
974 _ => unreachable!(),
975 }
976 }
977
978 pub fn contourf(
980 &mut self,
981 x: &[f64],
982 y: &[f64],
983 z: Vec<Vec<f64>>,
984 ) -> Result<&mut ContourArtist> {
985 if x.is_empty() || y.is_empty() || z.is_empty() {
986 return Err(PlotError::EmptyData);
987 }
988 let color = Color::TABLEAU_10[self.color_index % 10];
989 self.color_index += 1;
990 let artist = ContourArtist {
991 x: x.to_vec(),
992 y: y.to_vec(),
993 z,
994 levels: None,
995 filled: true,
996 cmap: crate::colormap::Colormap::Viridis,
997 colors: None,
998 linewidths: 1.0,
999 label: None,
1000 color,
1001 num_levels: 10,
1002 };
1003 self.artists.push(Artist::Contour(artist));
1004 match self.artists.last_mut().expect("just pushed") {
1005 Artist::Contour(a) => Ok(a),
1006 _ => unreachable!(),
1007 }
1008 }
1009
1010 pub fn violin(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut ViolinArtist> {
1012 if datasets.is_empty() {
1013 return Err(PlotError::EmptyData);
1014 }
1015 let color = Color::TABLEAU_10[self.color_index % 10];
1016 self.color_index += 1;
1017 let artist = ViolinArtist {
1018 datasets,
1019 positions: None,
1020 widths: 0.7,
1021 show_median: true,
1022 show_quartiles: true,
1023 color,
1024 alpha: 0.7,
1025 label: None,
1026 bw_method: 0.0,
1027 };
1028 self.artists.push(Artist::Violin(artist));
1029 match self.artists.last_mut().expect("just pushed") {
1030 Artist::Violin(a) => Ok(a),
1031 _ => unreachable!(),
1032 }
1033 }
1034
1035 pub fn bar_group<C: IntoCategories>(
1040 &mut self,
1041 categories: C,
1042 series: &[(&str, Vec<f64>)],
1043 ) -> Result<()> {
1044 let cat_labels: Vec<String> = categories
1045 .into_categories()
1046 .labels
1047 .iter()
1048 .map(|s| s.to_string())
1049 .collect();
1050 let n_series = series.len();
1051 if n_series == 0 {
1052 return Err(PlotError::EmptyData);
1053 }
1054 let total_width = 0.8;
1055 let bar_width = total_width / n_series as f64;
1056
1057 for (si, (label, heights)) in series.iter().enumerate() {
1058 let offset_val = (si as f64 - (n_series as f64 - 1.0) / 2.0) * bar_width;
1059 let offsets: Vec<f64> = vec![offset_val; heights.len()];
1060 let artist_ref = self.bar(cat_labels.clone(), heights.as_slice())?;
1061 artist_ref.bar_width(bar_width);
1062 artist_ref.offset(offsets);
1063 artist_ref.label(label);
1064 }
1065 Ok(())
1066 }
1067
1068 pub fn hexbin<X, Y>(&mut self, x: X, y: Y) -> Result<&mut HexbinArtist>
1078 where
1079 X: IntoSeries,
1080 Y: IntoSeries,
1081 {
1082 let xs = x.into_series();
1083 let ys = y.into_series();
1084 if xs.is_empty() || ys.is_empty() {
1085 return Err(PlotError::EmptyData);
1086 }
1087 if xs.len() != ys.len() {
1088 return Err(PlotError::SeriesLengthMismatch {
1089 expected: xs.len(),
1090 got: ys.len(),
1091 });
1092 }
1093 let color = Color::TABLEAU_10[self.color_index % 10];
1094 self.color_index += 1;
1095 let artist = HexbinArtist {
1096 x: xs.data,
1097 y: ys.data,
1098 gridsize: 20,
1099 cmap: crate::colormap::Colormap::Viridis,
1100 mincnt: 1,
1101 alpha: 1.0,
1102 color,
1103 label: None,
1104 edgecolor: None,
1105 show_colorbar: false,
1106 };
1107 self.artists.push(Artist::Hexbin(artist));
1108 match self.artists.last_mut().expect("just pushed") {
1109 Artist::Hexbin(a) => Ok(a),
1110 _ => unreachable!(),
1111 }
1112 }
1113
1114 pub fn polar_plot<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1127 where
1128 T: IntoSeries,
1129 R: IntoSeries,
1130 {
1131 let ts = theta.into_series();
1132 let rs = r.into_series();
1133 if ts.len() != rs.len() {
1134 return Err(PlotError::SeriesLengthMismatch {
1135 expected: ts.len(),
1136 got: rs.len(),
1137 });
1138 }
1139 if ts.is_empty() {
1140 return Err(PlotError::EmptyData);
1141 }
1142 let color = Color::TABLEAU_10[self.color_index % 10];
1143 self.color_index += 1;
1144 let artist = PolarArtist {
1145 theta: ts.data,
1146 r: rs.data,
1147 color,
1148 label: None,
1149 alpha: 1.0,
1150 linewidth: 1.5,
1151 filled: false,
1152 marker: None,
1153 };
1154 self.artists.push(Artist::Polar(artist));
1155 match self.artists.last_mut().expect("just pushed") {
1156 Artist::Polar(a) => Ok(a),
1157 _ => unreachable!(),
1158 }
1159 }
1160
1161 pub fn polar_fill<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1171 where
1172 T: IntoSeries,
1173 R: IntoSeries,
1174 {
1175 let artist_ref = self.polar_plot(theta, r)?;
1176 artist_ref.filled(true);
1177 artist_ref.alpha(0.3);
1178 Ok(artist_ref)
1179 }
1180
1181 pub fn waterfall<C, V>(&mut self, categories: C, values: V) -> Result<&mut WaterfallArtist>
1195 where
1196 C: IntoCategories,
1197 V: IntoSeries,
1198 {
1199 let cats = categories.into_categories();
1200 let vals = values.into_series();
1201 if cats.len() != vals.len() {
1202 return Err(PlotError::SeriesLengthMismatch {
1203 expected: cats.len(),
1204 got: vals.len(),
1205 });
1206 }
1207 if cats.is_empty() {
1208 return Err(PlotError::EmptyData);
1209 }
1210
1211 let n = cats.len();
1213 let tick_positions: Vec<f64> = (0..n).map(|i| i as f64).collect();
1214 self.custom_xticks = Some(tick_positions);
1215 self.custom_xticklabels = Some(cats.labels.iter().map(|s| s.to_string()).collect());
1216
1217 let color = Color::TABLEAU_10[self.color_index % 10];
1218 self.color_index += 1;
1219 let artist = WaterfallArtist {
1220 categories: cats,
1221 values: vals,
1222 total_indices: Vec::new(),
1223 increase_color: Color::rgb(0x2C, 0xA0, 0x2C), decrease_color: Color::rgb(0xD6, 0x27, 0x28), total_color: Color::rgb(0x4E, 0x79, 0xA7), connector_lines: true,
1227 show_values: false,
1228 bar_width: 0.6,
1229 label: None,
1230 color,
1231 alpha: 1.0,
1232 };
1233 self.artists.push(Artist::Waterfall(artist));
1234 match self.artists.last_mut().expect("just pushed") {
1235 Artist::Waterfall(a) => Ok(a),
1236 _ => unreachable!(),
1237 }
1238 }
1239}
1240
1241#[allow(clippy::too_many_arguments)]
1246impl Axes {
1247 pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
1263 self.render_primary(renderer, bounds, fig_theme, false);
1264 }
1265
1266 pub(crate) fn render_primary(
1267 &self,
1268 renderer: &mut impl Renderer,
1269 bounds: Rect,
1270 fig_theme: &Theme,
1271 suppress_legend: bool,
1272 ) {
1273 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1274
1275 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1277
1278 let xticks = self.resolve_xticks(xmin, xmax);
1280 let yticks = self.resolve_yticks(ymin, ymax);
1281
1282 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1284 layout_config.has_title = self.title.is_some();
1285 layout_config.has_xlabel = self.xlabel.is_some();
1286 layout_config.has_ylabel = self.ylabel.is_some();
1287 layout_config.has_legend = self.show_legend;
1288
1289 let layout_result = layout::compute_layout(&layout_config);
1290
1291 let full_plot_area = Rect::new(
1292 bounds.x + layout_result.plot_area.x,
1293 bounds.y + layout_result.plot_area.y,
1294 layout_result.plot_area.width,
1295 layout_result.plot_area.height,
1296 );
1297
1298 let effective_colorbar = if self.colorbar.is_some() {
1300 self.colorbar.clone()
1301 } else {
1302 self.auto_colorbar_from_artists()
1303 };
1304
1305 let (plot_area, colorbar_rect) = if effective_colorbar.is_some() {
1307 let cb_width = (full_plot_area.width * colorbar::COLORBAR_WIDTH_FRACTION).max(30.0);
1308 let shrunk = Rect::new(
1309 full_plot_area.x,
1310 full_plot_area.y,
1311 full_plot_area.width - cb_width - colorbar::COLORBAR_GAP,
1312 full_plot_area.height,
1313 );
1314 let cb_rect = Rect::new(
1315 full_plot_area.x + full_plot_area.width - cb_width,
1316 full_plot_area.y,
1317 cb_width,
1318 full_plot_area.height,
1319 );
1320 (shrunk, Some(cb_rect))
1321 } else {
1322 (full_plot_area, None)
1323 };
1324
1325 let bg_path = Path::rect(plot_area);
1327 renderer.fill_path(
1328 &bg_path,
1329 &Paint::new(theme.axes_background),
1330 Affine::IDENTITY,
1331 );
1332
1333 if self.show_grid.unwrap_or(theme.show_grid) {
1335 self.draw_grid(
1336 renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme,
1337 );
1338 }
1339
1340 let clip_path = Path::rect(plot_area);
1342 renderer.push_clip(&clip_path, Affine::IDENTITY);
1343 for artist in &self.artists {
1344 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1345 }
1346 renderer.pop_clip();
1347
1348 let x_minor = if matches!(self.xscale, Scale::Log10) {
1350 ticks::generate_log_minor_ticks(xmin, xmax)
1351 } else {
1352 Vec::new()
1353 };
1354 let y_minor = if matches!(self.yscale, Scale::Log10) {
1355 ticks::generate_log_minor_ticks(ymin, ymax)
1356 } else {
1357 Vec::new()
1358 };
1359
1360 self.draw_spines(renderer, &plot_area, theme);
1362
1363 self.draw_ticks(
1365 renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme,
1366 );
1367 if !x_minor.is_empty() || !y_minor.is_empty() {
1368 self.draw_minor_ticks(
1369 renderer, &plot_area, &x_minor, &y_minor, xmin, xmax, ymin, ymax, theme,
1370 );
1371 }
1372
1373 self.draw_labels(renderer, &plot_area, &bounds, theme);
1375
1376 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1378
1379 if self.show_legend && !suppress_legend {
1381 self.draw_legend(renderer, &plot_area, theme);
1382 }
1383
1384 if let (Some(ref cb), Some(ref cb_rect)) = (&effective_colorbar, &colorbar_rect) {
1386 colorbar::draw_colorbar(renderer, cb, cb_rect, theme);
1387 }
1388 }
1389
1390 fn auto_colorbar_from_artists(&self) -> Option<Colorbar> {
1391 for artist in &self.artists {
1392 match artist {
1393 Artist::Heatmap(a) if a.show_colorbar => {
1394 return Some(Colorbar::new(
1395 a.cmap,
1396 a.effective_vmin(),
1397 a.effective_vmax(),
1398 ));
1399 }
1400 Artist::Hexbin(a) if a.show_colorbar => {
1401 let result =
1402 crate::charts::hexbin::bin_hexagonal(&a.x, &a.y, a.gridsize, a.mincnt);
1403 let vmin = result.min_count as f64;
1404 let vmax = (result.max_count as f64).max(vmin + 1.0);
1405 return Some(Colorbar::new(a.cmap, vmin, vmax));
1406 }
1407 _ => {}
1408 }
1409 }
1410 None
1411 }
1412
1413 pub(crate) fn render_twin(
1415 &self,
1416 renderer: &mut impl Renderer,
1417 plot_area: Rect,
1418 bounds: Rect,
1419 fig_theme: &Theme,
1420 ) {
1421 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1422 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1423 let yticks = self.resolve_yticks(ymin, ymax);
1424 let xticks = self.resolve_xticks(xmin, xmax);
1425
1426 let clip_path = Path::rect(plot_area);
1427 renderer.push_clip(&clip_path, Affine::IDENTITY);
1428 for artist in &self.artists {
1429 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1430 }
1431 renderer.pop_clip();
1432
1433 let side = self.twin_side.unwrap_or(TwinSide::Right);
1434 match side {
1435 TwinSide::Right => {
1436 let paint = Paint::new(theme.spine_color);
1437 let stroke = Stroke::new(theme.spine_width);
1438 let mut p = Path::new();
1439 p.move_to(plot_area.right(), plot_area.y);
1440 p.line_to(plot_area.right(), plot_area.bottom());
1441 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1442 self.draw_ticks_right(renderer, &plot_area, &yticks, ymin, ymax, theme);
1443 self.draw_ylabel_right(renderer, &plot_area, &bounds, theme);
1444 }
1445 TwinSide::Top => {
1446 let paint = Paint::new(theme.spine_color);
1447 let stroke = Stroke::new(theme.spine_width);
1448 let mut p = Path::new();
1449 p.move_to(plot_area.x, plot_area.y);
1450 p.line_to(plot_area.right(), plot_area.y);
1451 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1452 self.draw_ticks_top(renderer, &plot_area, &xticks, xmin, xmax, theme);
1453 self.draw_xlabel_top(renderer, &plot_area, theme);
1454 }
1455 }
1456
1457 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1458 }
1459
1460 pub(crate) fn compute_plot_area(&self, bounds: &Rect) -> Rect {
1462 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1463 layout_config.has_title = self.title.is_some();
1464 layout_config.has_xlabel = self.xlabel.is_some();
1465 layout_config.has_ylabel = self.ylabel.is_some();
1466 layout_config.has_legend = self.show_legend;
1467 let layout_result = layout::compute_layout(&layout_config);
1468 Rect::new(
1469 bounds.x + layout_result.plot_area.x,
1470 bounds.y + layout_result.plot_area.y,
1471 layout_result.plot_area.width,
1472 layout_result.plot_area.height,
1473 )
1474 }
1475
1476 pub fn collect_legend_entries(&self) -> Vec<LegendEntry> {
1478 self.artists
1479 .iter()
1480 .filter_map(|a| {
1481 let (label, color, swatch) = match a {
1482 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1483 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1484 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1485 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1486 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1487 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1488 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1489 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1490 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1491 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1492 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1493 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1494 Artist::Contour(a) => (
1495 a.label.as_deref(),
1496 a.color,
1497 if a.filled {
1498 SwatchKind::Filled
1499 } else {
1500 SwatchKind::Line
1501 },
1502 ),
1503 Artist::Polar(a) => (
1504 a.label.as_deref(),
1505 a.color,
1506 if a.filled {
1507 SwatchKind::Filled
1508 } else {
1509 SwatchKind::Line
1510 },
1511 ),
1512 Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1513 Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1514 };
1515 label.map(|l| LegendEntry {
1516 label: l.to_string(),
1517 color,
1518 swatch,
1519 })
1520 })
1521 .collect()
1522 }
1523
1524 fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
1531 let mut x_lo = f64::INFINITY;
1532 let mut x_hi = f64::NEG_INFINITY;
1533 let mut y_lo = f64::INFINITY;
1534 let mut y_hi = f64::NEG_INFINITY;
1535
1536 for artist in &self.artists {
1537 match artist {
1538 Artist::Line(a) => {
1539 if let Some((lo, hi)) = a.x.bounds() {
1540 x_lo = x_lo.min(lo);
1541 x_hi = x_hi.max(hi);
1542 }
1543 if let Some((lo, hi)) = a.y.bounds() {
1544 y_lo = y_lo.min(lo);
1545 y_hi = y_hi.max(hi);
1546 }
1547 }
1548 Artist::Scatter(a) => {
1549 if let Some((lo, hi)) = a.x.bounds() {
1550 x_lo = x_lo.min(lo);
1551 x_hi = x_hi.max(hi);
1552 }
1553 if let Some((lo, hi)) = a.y.bounds() {
1554 y_lo = y_lo.min(lo);
1555 y_hi = y_hi.max(hi);
1556 }
1557 }
1558 Artist::Bar(a) => {
1559 let n = a.categories.len() as f64;
1560 if a.horizontal {
1561 y_lo = 0.0_f64.min(y_lo);
1563 y_hi = n.max(y_hi);
1564 x_lo = 0.0_f64.min(x_lo);
1565 if let Some((lo, hi)) = a.heights.bounds() {
1566 x_lo = x_lo.min(lo.min(0.0));
1567 x_hi = x_hi.max(hi);
1568 }
1569 } else {
1570 x_lo = 0.0_f64.min(x_lo);
1572 x_hi = n.max(x_hi);
1573 y_lo = 0.0_f64.min(y_lo);
1574 if let Some((lo, hi)) = a.heights.bounds() {
1575 y_lo = y_lo.min(lo.min(0.0));
1576 y_hi = y_hi.max(hi);
1577 }
1578 }
1579 }
1580 Artist::Histogram(a) => {
1581 if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
1582 x_lo = x_lo.min(first);
1583 x_hi = x_hi.max(last);
1584 }
1585 y_lo = 0.0_f64.min(y_lo);
1586 let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
1587 y_hi = y_hi.max(max_count);
1588 }
1589 Artist::FillBetween(a) => {
1590 if let Some((lo, hi)) = a.x.bounds() {
1591 x_lo = x_lo.min(lo);
1592 x_hi = x_hi.max(hi);
1593 }
1594 if let Some((lo, hi)) = a.y1.bounds() {
1595 y_lo = y_lo.min(lo);
1596 y_hi = y_hi.max(hi);
1597 }
1598 if let Some((lo, hi)) = a.y2.bounds() {
1599 y_lo = y_lo.min(lo);
1600 y_hi = y_hi.max(hi);
1601 }
1602 }
1603 Artist::Step(a) => {
1604 if let Some((lo, hi)) = a.x.bounds() {
1605 x_lo = x_lo.min(lo);
1606 x_hi = x_hi.max(hi);
1607 }
1608 if let Some((lo, hi)) = a.y.bounds() {
1609 y_lo = y_lo.min(lo);
1610 y_hi = y_hi.max(hi);
1611 }
1612 }
1613 Artist::Stem(a) => {
1614 if let Some((lo, hi)) = a.x.bounds() {
1615 x_lo = x_lo.min(lo);
1616 x_hi = x_hi.max(hi);
1617 }
1618 if let Some((lo, hi)) = a.y.bounds() {
1619 y_lo = y_lo.min(lo.min(a.baseline));
1620 y_hi = y_hi.max(hi.max(a.baseline));
1621 }
1622 }
1623 Artist::BoxPlot(a) => {
1624 let n = a.stats.len() as f64;
1625 x_lo = 0.0_f64.min(x_lo);
1626 x_hi = n.max(x_hi);
1627 for s in &a.stats {
1628 y_lo = y_lo.min(s.whisker_low);
1629 y_hi = y_hi.max(s.whisker_high);
1630 for &o in &s.outliers {
1631 y_lo = y_lo.min(o);
1632 y_hi = y_hi.max(o);
1633 }
1634 }
1635 }
1636 Artist::ErrorBar(a) => {
1637 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1638 x_lo = x_lo.min(bxlo);
1639 x_hi = x_hi.max(bxhi);
1640 y_lo = y_lo.min(bylo);
1641 y_hi = y_hi.max(byhi);
1642 }
1643 Artist::Heatmap(a) => {
1644 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1645 x_lo = x_lo.min(bxlo);
1646 x_hi = x_hi.max(bxhi);
1647 y_lo = y_lo.min(bylo);
1648 y_hi = y_hi.max(byhi);
1649 }
1650 Artist::Pie(a) => {
1651 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1654 x_lo = x_lo.min(bxlo);
1655 x_hi = x_hi.max(bxhi);
1656 y_lo = y_lo.min(bylo);
1657 y_hi = y_hi.max(byhi);
1658 }
1659 Artist::Violin(a) => {
1660 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1661 x_lo = x_lo.min(bxlo);
1662 x_hi = x_hi.max(bxhi);
1663 y_lo = y_lo.min(bylo);
1664 y_hi = y_hi.max(byhi);
1665 }
1666 Artist::Contour(a) => {
1667 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1668 x_lo = x_lo.min(bxlo);
1669 x_hi = x_hi.max(bxhi);
1670 y_lo = y_lo.min(bylo);
1671 y_hi = y_hi.max(byhi);
1672 }
1673 Artist::Polar(a) => {
1674 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1675 x_lo = x_lo.min(bxlo);
1676 x_hi = x_hi.max(bxhi);
1677 y_lo = y_lo.min(bylo);
1678 y_hi = y_hi.max(byhi);
1679 }
1680 Artist::Hexbin(a) => {
1681 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1682 x_lo = x_lo.min(bxlo);
1683 x_hi = x_hi.max(bxhi);
1684 y_lo = y_lo.min(bylo);
1685 y_hi = y_hi.max(byhi);
1686 }
1687 Artist::Waterfall(a) => {
1688 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1689 x_lo = x_lo.min(bxlo);
1690 x_hi = x_hi.max(bxhi);
1691 y_lo = y_lo.min(bylo);
1692 y_hi = y_hi.max(byhi);
1693 }
1694 }
1695 }
1696
1697 if !x_lo.is_finite() || !x_hi.is_finite() {
1699 x_lo = if self.xscale.requires_positive() {
1700 1.0
1701 } else {
1702 0.0
1703 };
1704 x_hi = if self.xscale.requires_positive() {
1705 10.0
1706 } else {
1707 1.0
1708 };
1709 }
1710 if !y_lo.is_finite() || !y_hi.is_finite() {
1711 y_lo = if self.yscale.requires_positive() {
1712 1.0
1713 } else {
1714 0.0
1715 };
1716 y_hi = if self.yscale.requires_positive() {
1717 10.0
1718 } else {
1719 1.0
1720 };
1721 }
1722
1723 if self.xscale.requires_positive() {
1725 if x_lo <= 0.0 {
1726 x_lo = if x_hi > 0.0 { x_hi * 1e-4 } else { 1.0 };
1728 }
1729 if x_hi <= x_lo {
1730 x_hi = x_lo * 10.0;
1731 }
1732 }
1733 if self.yscale.requires_positive() {
1734 if y_lo <= 0.0 {
1735 y_lo = if y_hi > 0.0 { y_hi * 1e-4 } else { 1.0 };
1736 }
1737 if y_hi <= y_lo {
1738 y_hi = y_lo * 10.0;
1739 }
1740 }
1741
1742 if (x_hi - x_lo).abs() < f64::EPSILON {
1744 x_lo -= 0.5;
1745 x_hi += 0.5;
1746 }
1747 if (y_hi - y_lo).abs() < f64::EPSILON {
1748 y_lo -= 0.5;
1749 y_hi += 0.5;
1750 }
1751
1752 let (x_pad_lo, x_pad_hi) = if self.xscale.requires_positive() {
1755 let factor = 1.0 + AUTOSCALE_PAD;
1757 (x_lo / factor, x_hi * factor)
1758 } else {
1759 let pad = (x_hi - x_lo) * AUTOSCALE_PAD;
1760 (x_lo - pad, x_hi + pad)
1761 };
1762 let (y_pad_lo, y_pad_hi) = if self.yscale.requires_positive() {
1763 let factor = 1.0 + AUTOSCALE_PAD;
1764 (y_lo / factor, y_hi * factor)
1765 } else {
1766 let pad = (y_hi - y_lo) * AUTOSCALE_PAD;
1767 (y_lo - pad, y_hi + pad)
1768 };
1769 x_lo = x_pad_lo;
1770 x_hi = x_pad_hi;
1771 y_lo = y_pad_lo;
1772 y_hi = y_pad_hi;
1773
1774 if let Some((lo, hi)) = self.xlim {
1776 x_lo = lo;
1777 x_hi = hi;
1778 }
1779 if let Some((lo, hi)) = self.ylim {
1780 y_lo = lo;
1781 y_hi = hi;
1782 }
1783
1784 if self.x_inverted {
1786 std::mem::swap(&mut x_lo, &mut x_hi);
1787 }
1788 if self.y_inverted {
1789 std::mem::swap(&mut y_lo, &mut y_hi);
1790 }
1791
1792 (x_lo, x_hi, y_lo, y_hi)
1793 }
1794
1795 fn resolve_xticks(&self, xmin: f64, xmax: f64) -> Vec<ticks::Tick> {
1802 if let Some(ref positions) = self.custom_xticks {
1803 let labels = self.custom_xticklabels.as_ref();
1804 positions
1805 .iter()
1806 .enumerate()
1807 .map(|(i, &v)| ticks::Tick {
1808 value: v,
1809 label: labels
1810 .and_then(|l| l.get(i))
1811 .cloned()
1812 .unwrap_or_else(|| ticks::format_tick_value(v)),
1813 })
1814 .collect()
1815 } else {
1816 let (lo, hi) = if xmin <= xmax {
1817 (xmin, xmax)
1818 } else {
1819 (xmax, xmin)
1820 };
1821 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.xscale)
1822 }
1823 }
1824
1825 fn resolve_yticks(&self, ymin: f64, ymax: f64) -> Vec<ticks::Tick> {
1828 if let Some(ref positions) = self.custom_yticks {
1829 let labels = self.custom_yticklabels.as_ref();
1830 positions
1831 .iter()
1832 .enumerate()
1833 .map(|(i, &v)| ticks::Tick {
1834 value: v,
1835 label: labels
1836 .and_then(|l| l.get(i))
1837 .cloned()
1838 .unwrap_or_else(|| ticks::format_tick_value(v)),
1839 })
1840 .collect()
1841 } else {
1842 let (lo, hi) = if ymin <= ymax {
1843 (ymin, ymax)
1844 } else {
1845 (ymax, ymin)
1846 };
1847 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.yscale)
1848 }
1849 }
1850
1851 fn draw_grid(
1857 &self,
1858 renderer: &mut impl Renderer,
1859 plot_area: &Rect,
1860 xticks: &[ticks::Tick],
1861 yticks: &[ticks::Tick],
1862 xmin: f64,
1863 xmax: f64,
1864 ymin: f64,
1865 ymax: f64,
1866 theme: &Theme,
1867 ) {
1868 let grid_color = if let Some(alpha) = self.grid_alpha {
1870 theme.grid_color.with_alpha((alpha * 255.0) as u8)
1871 } else {
1872 theme.grid_color
1873 };
1874 let paint = Paint::new(grid_color);
1875
1876 let mut stroke = Stroke::new(theme.grid_width);
1878 if let Some(style) = self.grid_style {
1879 stroke = match style {
1880 LineStyle::Solid => stroke,
1881 LineStyle::Dashed => stroke.with_dash(DashPattern {
1882 dashes: vec![6.0, 4.0],
1883 offset: 0.0,
1884 }),
1885 LineStyle::Dotted => stroke.with_dash(DashPattern {
1886 dashes: vec![2.0, 2.0],
1887 offset: 0.0,
1888 }),
1889 LineStyle::DashDot => stroke.with_dash(DashPattern {
1890 dashes: vec![6.0, 3.0, 2.0, 3.0],
1891 offset: 0.0,
1892 }),
1893 };
1894 }
1895
1896 let draw_x = matches!(self.grid_axis, GridAxis::X | GridAxis::Both);
1897 let draw_y = matches!(self.grid_axis, GridAxis::Y | GridAxis::Both);
1898
1899 if draw_x {
1901 for tick in xticks {
1902 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1903 let mut path = Path::new();
1904 path.move_to(pt.x, plot_area.y);
1905 path.line_to(pt.x, plot_area.bottom());
1906 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1907 }
1908 }
1909
1910 if draw_y {
1912 for tick in yticks {
1913 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1914 let mut path = Path::new();
1915 path.move_to(plot_area.x, pt.y);
1916 path.line_to(plot_area.right(), pt.y);
1917 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1918 }
1919 }
1920 }
1921
1922 fn draw_artist(
1928 &self,
1929 renderer: &mut impl Renderer,
1930 artist: &Artist,
1931 plot_area: &Rect,
1932 xmin: f64,
1933 xmax: f64,
1934 ymin: f64,
1935 ymax: f64,
1936 theme: &Theme,
1937 ) {
1938 match artist {
1939 Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1940 Artist::Scatter(a) => {
1941 self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1942 }
1943 Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1944 Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1945 Artist::FillBetween(a) => {
1946 self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1947 }
1948 Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1949 Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1950 Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1951 Artist::ErrorBar(a) => {
1952 self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1953 }
1954 Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1955 Artist::Pie(a) => self.draw_pie(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1956 Artist::Violin(a) => {
1957 self.draw_violin(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1958 }
1959 Artist::Contour(a) => self.draw_contour(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1960 Artist::Polar(a) => {
1961 self.draw_polar(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme)
1962 }
1963 Artist::Hexbin(a) => self.draw_hexbin(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1964 Artist::Waterfall(a) => {
1965 self.draw_waterfall(renderer, a, plot_area, xmin, xmax, ymin, ymax)
1966 }
1967 }
1968 }
1969
1970 fn draw_line(
1975 &self,
1976 renderer: &mut impl Renderer,
1977 artist: &LineArtist,
1978 plot_area: &Rect,
1979 xmin: f64,
1980 xmax: f64,
1981 ymin: f64,
1982 ymax: f64,
1983 ) {
1984 if artist.x.is_empty() {
1985 return;
1986 }
1987
1988 let indices: Vec<usize> = artist
1992 .decimate
1993 .resolve_indices(&artist.x.data, &artist.y.data);
1994
1995 if indices.is_empty() {
1996 return;
1997 }
1998
1999 let mut path = Path::new();
2000 let first = self.data_to_pixel(
2001 artist.x.data[indices[0]],
2002 artist.y.data[indices[0]],
2003 plot_area,
2004 xmin,
2005 xmax,
2006 ymin,
2007 ymax,
2008 );
2009 path.move_to(first.x, first.y);
2010
2011 for &i in &indices[1..] {
2012 let pt = self.data_to_pixel(
2013 artist.x.data[i],
2014 artist.y.data[i],
2015 plot_area,
2016 xmin,
2017 xmax,
2018 ymin,
2019 ymax,
2020 );
2021 path.line_to(pt.x, pt.y);
2022 }
2023
2024 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2025 let paint = Paint::new(color);
2026 let mut stroke = Stroke::new(artist.width);
2027
2028 match artist.style {
2030 crate::theme::LineStyle::Solid => {}
2031 crate::theme::LineStyle::Dashed => {
2032 stroke = stroke.with_dash(DashPattern {
2033 dashes: vec![6.0, 4.0],
2034 offset: 0.0,
2035 });
2036 }
2037 crate::theme::LineStyle::Dotted => {
2038 stroke = stroke.with_dash(DashPattern {
2039 dashes: vec![2.0, 2.0],
2040 offset: 0.0,
2041 });
2042 }
2043 crate::theme::LineStyle::DashDot => {
2044 stroke = stroke.with_dash(DashPattern {
2045 dashes: vec![6.0, 3.0, 2.0, 3.0],
2046 offset: 0.0,
2047 });
2048 }
2049 }
2050
2051 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
2052 }
2053
2054 fn draw_scatter(
2056 &self,
2057 renderer: &mut impl Renderer,
2058 artist: &ScatterArtist,
2059 plot_area: &Rect,
2060 xmin: f64,
2061 xmax: f64,
2062 ymin: f64,
2063 ymax: f64,
2064 theme: &Theme,
2065 ) {
2066 let alpha_byte = (artist.alpha * 255.0) as u8;
2067
2068 let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
2070 (Some(c_vals), Some(cmap)) if !c_vals.is_empty() => Some(cmap.map_values(c_vals)),
2071 _ => None,
2072 };
2073
2074 let default_color = artist.color.with_alpha(alpha_byte);
2075 let default_paint = Paint::new(default_color);
2076 let radius = artist.size / 2.0;
2077
2078 let indices = artist
2083 .decimate
2084 .resolve_indices(&artist.x.data, &artist.y.data);
2085
2086 for &i in &indices {
2087 let pt = self.data_to_pixel(
2088 artist.x.data[i],
2089 artist.y.data[i],
2090 plot_area,
2091 xmin,
2092 xmax,
2093 ymin,
2094 ymax,
2095 );
2096
2097 let paint = if let Some(ref cc) = cmap_colors {
2099 Paint::new(cc[i].with_alpha(alpha_byte))
2100 } else if let Some(ref cs) = artist.colors {
2101 Paint::new(cs[i].with_alpha(alpha_byte))
2102 } else {
2103 default_paint
2104 };
2105
2106 let marker_path = match artist.marker {
2107 Marker::Circle | Marker::Point => Path::circle(pt, radius),
2108 Marker::Square => Path::rect(Rect::new(
2109 pt.x - radius,
2110 pt.y - radius,
2111 radius * 2.0,
2112 radius * 2.0,
2113 )),
2114 Marker::Diamond => {
2115 let mut p = Path::new();
2116 p.move_to(pt.x, pt.y - radius);
2117 p.line_to(pt.x + radius, pt.y);
2118 p.line_to(pt.x, pt.y + radius);
2119 p.line_to(pt.x - radius, pt.y);
2120 p.close();
2121 p
2122 }
2123 Marker::Triangle => {
2124 let mut p = Path::new();
2125 let h = radius * 1.1547; p.move_to(pt.x, pt.y - radius);
2127 p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
2128 p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
2129 p.close();
2130 p
2131 }
2132 Marker::Plus => {
2133 let mut p = Path::new();
2135 p.move_to(pt.x - radius, pt.y);
2136 p.line_to(pt.x + radius, pt.y);
2137 p.move_to(pt.x, pt.y - radius);
2138 p.line_to(pt.x, pt.y + radius);
2139 let stroke = Stroke::new(theme.line_width.max(1.0));
2140 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2141 continue;
2142 }
2143 Marker::Cross => {
2144 let mut p = Path::new();
2145 let d = radius * 0.707; p.move_to(pt.x - d, pt.y - d);
2147 p.line_to(pt.x + d, pt.y + d);
2148 p.move_to(pt.x + d, pt.y - d);
2149 p.line_to(pt.x - d, pt.y + d);
2150 let stroke = Stroke::new(theme.line_width.max(1.0));
2151 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2152 continue;
2153 }
2154 Marker::Star => {
2155 let mut p = Path::new();
2157 let inner = radius * 0.382;
2158 for j in 0..10 {
2159 let angle =
2160 std::f64::consts::FRAC_PI_2 + j as f64 * std::f64::consts::PI / 5.0;
2161 let r = if j % 2 == 0 { radius } else { inner };
2162 let sx = pt.x + r * angle.cos();
2163 let sy = pt.y - r * angle.sin();
2164 if j == 0 {
2165 p.move_to(sx, sy);
2166 } else {
2167 p.line_to(sx, sy);
2168 }
2169 }
2170 p.close();
2171 p
2172 }
2173 };
2174
2175 renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
2176 }
2177 }
2178
2179 fn draw_bar(
2181 &self,
2182 renderer: &mut impl Renderer,
2183 artist: &BarArtist,
2184 plot_area: &Rect,
2185 xmin: f64,
2186 xmax: f64,
2187 ymin: f64,
2188 ymax: f64,
2189 ) {
2190 let n = artist.categories.len();
2191 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2192 let paint = Paint::new(color);
2193
2194 if artist.horizontal {
2195 let cat_range = ymax - ymin;
2197 let cat_step = cat_range / n as f64;
2198 let bar_half = cat_step * artist.bar_width * 0.5;
2199
2200 for i in 0..n {
2201 let base_center = ymin + (i as f64 + 0.5) * cat_step;
2202 let cat_center = if let Some(ref off) = artist.offset {
2203 base_center + if i < off.len() { off[i] } else { 0.0 }
2204 } else {
2205 base_center
2206 };
2207 let value = artist.heights.data[i];
2208 let base = if let Some(ref bot) = artist.bottom {
2209 if i < bot.len() {
2210 bot[i]
2211 } else {
2212 0.0
2213 }
2214 } else {
2215 0.0
2216 };
2217
2218 let left_val = base.min(base + value);
2219 let right_val = base.max(base + value);
2220
2221 let p_left = self.data_to_pixel(
2222 left_val,
2223 cat_center - bar_half,
2224 plot_area,
2225 xmin,
2226 xmax,
2227 ymin,
2228 ymax,
2229 );
2230 let p_right = self.data_to_pixel(
2231 right_val,
2232 cat_center + bar_half,
2233 plot_area,
2234 xmin,
2235 xmax,
2236 ymin,
2237 ymax,
2238 );
2239
2240 let rect = Rect::from_points(p_left, p_right);
2241 let bar_path = Path::rect(rect);
2242 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2243 }
2244 } else {
2245 let cat_range = xmax - xmin;
2247 let cat_step = cat_range / n as f64;
2248 let bar_half = cat_step * artist.bar_width * 0.5;
2249
2250 for i in 0..n {
2251 let base_center = xmin + (i as f64 + 0.5) * cat_step;
2252 let cat_center = if let Some(ref off) = artist.offset {
2253 base_center + if i < off.len() { off[i] } else { 0.0 }
2254 } else {
2255 base_center
2256 };
2257 let value = artist.heights.data[i];
2258 let base = if let Some(ref bot) = artist.bottom {
2259 if i < bot.len() {
2260 bot[i]
2261 } else {
2262 0.0
2263 }
2264 } else {
2265 0.0
2266 };
2267
2268 let bottom_val = base.min(base + value);
2269 let top_val = base.max(base + value);
2270
2271 let p_bl = self.data_to_pixel(
2272 cat_center - bar_half,
2273 bottom_val,
2274 plot_area,
2275 xmin,
2276 xmax,
2277 ymin,
2278 ymax,
2279 );
2280 let p_tr = self.data_to_pixel(
2281 cat_center + bar_half,
2282 top_val,
2283 plot_area,
2284 xmin,
2285 xmax,
2286 ymin,
2287 ymax,
2288 );
2289
2290 let rect = Rect::from_points(p_bl, p_tr);
2291 let bar_path = Path::rect(rect);
2292 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2293 }
2294 }
2295 }
2296
2297 fn draw_hist(
2299 &self,
2300 renderer: &mut impl Renderer,
2301 artist: &HistArtist,
2302 plot_area: &Rect,
2303 xmin: f64,
2304 xmax: f64,
2305 ymin: f64,
2306 ymax: f64,
2307 ) {
2308 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2309 let paint = Paint::new(color);
2310 let stroke_paint = Paint::new(Color::WHITE);
2311 let stroke = Stroke::new(0.5);
2312
2313 for i in 0..artist.counts.len() {
2314 let left = artist.bin_edges[i];
2315 let right = artist.bin_edges[i + 1];
2316 let height = artist.counts[i];
2317
2318 if height <= 0.0 {
2319 continue;
2320 }
2321
2322 let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
2323 let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
2324
2325 let rect = Rect::from_points(p_bl, p_tr);
2326 let bar_path = Path::rect(rect);
2327 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2328 renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
2330 }
2331 }
2332
2333 fn draw_fill_between(
2336 &self,
2337 renderer: &mut impl Renderer,
2338 artist: &FillBetweenArtist,
2339 plot_area: &Rect,
2340 xmin: f64,
2341 xmax: f64,
2342 ymin: f64,
2343 ymax: f64,
2344 ) {
2345 if artist.x.is_empty() {
2346 return;
2347 }
2348
2349 let n = artist.x.len();
2350 let mut path = Path::new();
2351
2352 let first = self.data_to_pixel(
2354 artist.x.data[0],
2355 artist.y1.data[0],
2356 plot_area,
2357 xmin,
2358 xmax,
2359 ymin,
2360 ymax,
2361 );
2362 path.move_to(first.x, first.y);
2363 for i in 1..n {
2364 let pt = self.data_to_pixel(
2365 artist.x.data[i],
2366 artist.y1.data[i],
2367 plot_area,
2368 xmin,
2369 xmax,
2370 ymin,
2371 ymax,
2372 );
2373 path.line_to(pt.x, pt.y);
2374 }
2375
2376 for i in (0..n).rev() {
2378 let pt = self.data_to_pixel(
2379 artist.x.data[i],
2380 artist.y2.data[i],
2381 plot_area,
2382 xmin,
2383 xmax,
2384 ymin,
2385 ymax,
2386 );
2387 path.line_to(pt.x, pt.y);
2388 }
2389 path.close();
2390
2391 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2392 let paint = Paint::new(color);
2393 renderer.fill_path(&path, &paint, Affine::IDENTITY);
2394 }
2395
2396 fn draw_spines(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
2402 let paint = Paint::new(theme.spine_color);
2403 let stroke = Stroke::new(theme.spine_width);
2404
2405 if theme.show_bottom_spine {
2407 let mut p = Path::new();
2408 p.move_to(plot_area.x, plot_area.bottom());
2409 p.line_to(plot_area.right(), plot_area.bottom());
2410 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2411 }
2412 if theme.show_left_spine {
2414 let mut p = Path::new();
2415 p.move_to(plot_area.x, plot_area.y);
2416 p.line_to(plot_area.x, plot_area.bottom());
2417 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2418 }
2419 if theme.show_top_spine {
2421 let mut p = Path::new();
2422 p.move_to(plot_area.x, plot_area.y);
2423 p.line_to(plot_area.right(), plot_area.y);
2424 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2425 }
2426 if theme.show_right_spine {
2428 let mut p = Path::new();
2429 p.move_to(plot_area.right(), plot_area.y);
2430 p.line_to(plot_area.right(), plot_area.bottom());
2431 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2432 }
2433 }
2434
2435 fn draw_ticks(
2441 &self,
2442 renderer: &mut impl Renderer,
2443 plot_area: &Rect,
2444 xticks: &[ticks::Tick],
2445 yticks: &[ticks::Tick],
2446 xmin: f64,
2447 xmax: f64,
2448 ymin: f64,
2449 ymax: f64,
2450 theme: &Theme,
2451 ) {
2452 let tick_paint = Paint::new(theme.tick_color);
2453 let tick_stroke = Stroke::new(1.0);
2454 let tick_len = theme.tick_length;
2455
2456 let x_label_style = TextStyle {
2457 size: theme.tick_label_size,
2458 color: theme.text_color,
2459 weight: FontWeight::Normal,
2460 family: theme.font_family.clone(),
2461 halign: if self.xtick_rotation.abs() > 1.0 {
2462 HAlign::Right
2463 } else {
2464 HAlign::Center
2465 },
2466 valign: VAlign::Top,
2467 };
2468
2469 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2471
2472 let x_rot_rad = -self.xtick_rotation.to_radians();
2474
2475 for tick in xticks {
2477 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
2478 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2480 continue;
2481 }
2482 let x = pt.x;
2483 let y_base = plot_area.bottom();
2484
2485 let (y_start, y_end) = if outward {
2487 (y_base, y_base + tick_len)
2488 } else {
2489 (y_base - tick_len, y_base)
2490 };
2491 let mut tp = Path::new();
2492 tp.move_to(x, y_start);
2493 tp.line_to(x, y_end);
2494 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2495
2496 let label_y = if outward {
2498 y_base + tick_len + 2.0
2499 } else {
2500 y_base + 2.0
2501 };
2502 let label_pos = Point::new(x, label_y);
2503 let transform = if self.xtick_rotation.abs() > 0.01 {
2504 let rotate = Affine::rotate(x_rot_rad);
2505 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2506 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2507 from_origin * rotate * to_origin
2508 } else {
2509 Affine::IDENTITY
2510 };
2511 renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2512 }
2513
2514 let y_label_style = TextStyle {
2516 halign: HAlign::Right,
2517 valign: VAlign::Middle,
2518 ..x_label_style.clone()
2519 };
2520
2521 let y_rot_rad = -self.ytick_rotation.to_radians();
2523
2524 for tick in yticks {
2525 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
2526 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2528 continue;
2529 }
2530 let y = pt.y;
2531 let x_base = plot_area.x;
2532
2533 let (x_start, x_end) = if outward {
2535 (x_base - tick_len, x_base)
2536 } else {
2537 (x_base, x_base + tick_len)
2538 };
2539 let mut tp = Path::new();
2540 tp.move_to(x_start, y);
2541 tp.line_to(x_end, y);
2542 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2543
2544 let label_x = if outward {
2546 x_base - tick_len - 3.0
2547 } else {
2548 x_base - 3.0
2549 };
2550 let label_pos = Point::new(label_x, y);
2551 let transform = if self.ytick_rotation.abs() > 0.01 {
2552 let rotate = Affine::rotate(y_rot_rad);
2553 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2554 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2555 from_origin * rotate * to_origin
2556 } else {
2557 Affine::IDENTITY
2558 };
2559 renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2560 }
2561 }
2562
2563 fn draw_minor_ticks(
2569 &self,
2570 renderer: &mut impl Renderer,
2571 plot_area: &Rect,
2572 x_minor: &[f64],
2573 y_minor: &[f64],
2574 xmin: f64,
2575 xmax: f64,
2576 ymin: f64,
2577 ymax: f64,
2578 theme: &Theme,
2579 ) {
2580 let tick_paint = Paint::new(theme.tick_color);
2581 let tick_stroke = Stroke::new(0.5);
2582 let tick_len = theme.tick_length * 0.5;
2584 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2585
2586 for &val in x_minor {
2588 let pt = self.data_to_pixel(val, ymin, plot_area, xmin, xmax, ymin, ymax);
2589 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2590 continue;
2591 }
2592 let x = pt.x;
2593 let y_base = plot_area.bottom();
2594 let (y_start, y_end) = if outward {
2595 (y_base, y_base + tick_len)
2596 } else {
2597 (y_base - tick_len, y_base)
2598 };
2599 let mut tp = Path::new();
2600 tp.move_to(x, y_start);
2601 tp.line_to(x, y_end);
2602 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2603 }
2604
2605 for &val in y_minor {
2607 let pt = self.data_to_pixel(xmin, val, plot_area, xmin, xmax, ymin, ymax);
2608 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2609 continue;
2610 }
2611 let y = pt.y;
2612 let x_base = plot_area.x;
2613 let (x_start, x_end) = if outward {
2614 (x_base - tick_len, x_base)
2615 } else {
2616 (x_base, x_base + tick_len)
2617 };
2618 let mut tp = Path::new();
2619 tp.move_to(x_start, y);
2620 tp.line_to(x_end, y);
2621 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2622 }
2623 }
2624
2625 fn draw_ticks_right(
2627 &self,
2628 renderer: &mut impl Renderer,
2629 plot_area: &Rect,
2630 yticks: &[ticks::Tick],
2631 ymin: f64,
2632 ymax: f64,
2633 theme: &Theme,
2634 ) {
2635 let tick_paint = Paint::new(theme.tick_color);
2636 let tick_stroke = Stroke::new(1.0);
2637 let tick_len = theme.tick_length;
2638 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2639
2640 let y_label_style = TextStyle {
2641 size: theme.tick_label_size,
2642 color: theme.text_color,
2643 weight: FontWeight::Normal,
2644 family: theme.font_family.clone(),
2645 halign: HAlign::Left,
2646 valign: VAlign::Middle,
2647 };
2648
2649 let y_rot_rad = -self.ytick_rotation.to_radians();
2650
2651 for tick in yticks {
2652 let pt = self.data_to_pixel(0.0, tick.value, plot_area, 0.0, 1.0, ymin, ymax);
2653 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2654 continue;
2655 }
2656 let y = pt.y;
2657 let x_base = plot_area.right();
2658 let (x_start, x_end) = if outward {
2659 (x_base, x_base + tick_len)
2660 } else {
2661 (x_base - tick_len, x_base)
2662 };
2663 let mut tp = Path::new();
2664 tp.move_to(x_start, y);
2665 tp.line_to(x_end, y);
2666 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2667
2668 let label_x = if outward {
2669 x_base + tick_len + 3.0
2670 } else {
2671 x_base + 3.0
2672 };
2673 let label_pos = Point::new(label_x, y);
2674 let transform = if self.ytick_rotation.abs() > 0.01 {
2675 let rotate = Affine::rotate(y_rot_rad);
2676 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2677 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2678 from_origin * rotate * to_origin
2679 } else {
2680 Affine::IDENTITY
2681 };
2682 renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2683 }
2684 }
2685
2686 fn draw_ylabel_right(
2688 &self,
2689 renderer: &mut impl Renderer,
2690 plot_area: &Rect,
2691 bounds: &Rect,
2692 theme: &Theme,
2693 ) {
2694 if let Some(ylabel) = &self.ylabel {
2695 let style = TextStyle {
2696 size: theme.axis_label_size,
2697 color: theme.text_color,
2698 weight: FontWeight::Normal,
2699 family: theme.font_family.clone(),
2700 halign: HAlign::Center,
2701 valign: VAlign::Top,
2702 };
2703 let x = bounds.right() - 4.0;
2704 let y = plot_area.y + plot_area.height / 2.0;
2705 let rotate = Affine::rotate(std::f64::consts::FRAC_PI_2);
2706 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2707 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2708 let transform = translate_to * rotate * translate_back;
2709 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2710 }
2711 }
2712
2713 fn draw_ticks_top(
2715 &self,
2716 renderer: &mut impl Renderer,
2717 plot_area: &Rect,
2718 xticks: &[ticks::Tick],
2719 xmin: f64,
2720 xmax: f64,
2721 theme: &Theme,
2722 ) {
2723 let tick_paint = Paint::new(theme.tick_color);
2724 let tick_stroke = Stroke::new(1.0);
2725 let tick_len = theme.tick_length;
2726 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2727
2728 let x_label_style = TextStyle {
2729 size: theme.tick_label_size,
2730 color: theme.text_color,
2731 weight: FontWeight::Normal,
2732 family: theme.font_family.clone(),
2733 halign: if self.xtick_rotation.abs() > 1.0 {
2734 HAlign::Left
2735 } else {
2736 HAlign::Center
2737 },
2738 valign: VAlign::Bottom,
2739 };
2740
2741 let x_rot_rad = -self.xtick_rotation.to_radians();
2742
2743 for tick in xticks {
2744 let pt = self.data_to_pixel(tick.value, 0.0, plot_area, xmin, xmax, 0.0, 1.0);
2745 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2746 continue;
2747 }
2748 let x = pt.x;
2749 let y_base = plot_area.y;
2750 let (y_start, y_end) = if outward {
2751 (y_base - tick_len, y_base)
2752 } else {
2753 (y_base, y_base + tick_len)
2754 };
2755 let mut tp = Path::new();
2756 tp.move_to(x, y_start);
2757 tp.line_to(x, y_end);
2758 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2759
2760 let label_y = if outward {
2761 y_base - tick_len - 2.0
2762 } else {
2763 y_base - 2.0
2764 };
2765 let label_pos = Point::new(x, label_y);
2766 let transform = if self.xtick_rotation.abs() > 0.01 {
2767 let rotate = Affine::rotate(x_rot_rad);
2768 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2769 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2770 from_origin * rotate * to_origin
2771 } else {
2772 Affine::IDENTITY
2773 };
2774 renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2775 }
2776 }
2777
2778 fn draw_xlabel_top(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
2780 if let Some(xlabel) = &self.xlabel {
2781 let style = TextStyle {
2782 size: theme.axis_label_size,
2783 color: theme.text_color,
2784 weight: FontWeight::Normal,
2785 family: theme.font_family.clone(),
2786 halign: HAlign::Center,
2787 valign: VAlign::Bottom,
2788 };
2789 let x = plot_area.x + plot_area.width / 2.0;
2790 let y = plot_area.y - theme.tick_length - theme.tick_label_size - 8.0;
2791 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2792 }
2793 }
2794
2795 fn draw_labels(
2801 &self,
2802 renderer: &mut impl Renderer,
2803 plot_area: &Rect,
2804 bounds: &Rect,
2805 theme: &Theme,
2806 ) {
2807 if let Some(title) = &self.title {
2809 let style = TextStyle {
2810 size: theme.title_size,
2811 color: theme.text_color,
2812 weight: theme.title_weight,
2813 family: theme.font_family.clone(),
2814 halign: HAlign::Center,
2815 valign: VAlign::Bottom,
2816 };
2817 let x = plot_area.x + plot_area.width / 2.0;
2818 let y = plot_area.y - 10.0;
2819 renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
2820 }
2821
2822 if let Some(xlabel) = &self.xlabel {
2824 let style = TextStyle {
2825 size: theme.axis_label_size,
2826 color: theme.text_color,
2827 weight: FontWeight::Normal,
2828 family: theme.font_family.clone(),
2829 halign: HAlign::Center,
2830 valign: VAlign::Top,
2831 };
2832 let x = plot_area.x + plot_area.width / 2.0;
2833 let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
2835 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2836 }
2837
2838 if let Some(ylabel) = &self.ylabel {
2840 let style = TextStyle {
2841 size: theme.axis_label_size,
2842 color: theme.text_color,
2843 weight: FontWeight::Normal,
2844 family: theme.font_family.clone(),
2845 halign: HAlign::Center,
2846 valign: VAlign::Bottom,
2847 };
2848 let x = bounds.x + 4.0;
2849 let y = plot_area.y + plot_area.height / 2.0;
2850 let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
2852 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2853 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2854 let transform = translate_to * rotate * translate_back;
2855 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2856 }
2857 }
2858
2859 fn draw_boxplot(
2866 &self,
2867 renderer: &mut impl Renderer,
2868 artist: &BoxPlotArtist,
2869 plot_area: &Rect,
2870 xmin: f64,
2871 xmax: f64,
2872 ymin: f64,
2873 ymax: f64,
2874 ) {
2875 let n = artist.stats.len();
2876 if n == 0 {
2877 return;
2878 }
2879
2880 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2881 let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
2882 let paint = Paint::new(stroke_color);
2883 let thin = Stroke::new(1.0);
2884 let thick = Stroke::new(2.0);
2885 let hair = Stroke::new(0.5);
2886
2887 for (i, stats) in artist.stats.iter().enumerate() {
2888 let cx = i as f64 + 0.5;
2889 let half = artist.box_width / 2.0;
2890 let left = cx - half;
2891 let right = cx + half;
2892
2893 let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2895 let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2896 let box_rect_path = {
2897 let mut p = Path::new();
2898 p.move_to(tl.x, tl.y);
2899 p.line_to(br.x, tl.y);
2900 p.line_to(br.x, br.y);
2901 p.line_to(tl.x, br.y);
2902 p.close();
2903 p
2904 };
2905 renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
2906 renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
2907
2908 let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
2910 let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
2911 let mut median_path = Path::new();
2912 median_path.move_to(ml.x, ml.y);
2913 median_path.line_to(mr.x, mr.y);
2914 renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
2915
2916 let wl_bottom =
2918 self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2919 let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2920 let mut wl_path = Path::new();
2921 wl_path.move_to(wl_top.x, wl_top.y);
2922 wl_path.line_to(wl_bottom.x, wl_bottom.y);
2923 renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
2924
2925 let cap_left = self.data_to_pixel(
2927 cx - half * 0.5,
2928 stats.whisker_low,
2929 plot_area,
2930 xmin,
2931 xmax,
2932 ymin,
2933 ymax,
2934 );
2935 let cap_right = self.data_to_pixel(
2936 cx + half * 0.5,
2937 stats.whisker_low,
2938 plot_area,
2939 xmin,
2940 xmax,
2941 ymin,
2942 ymax,
2943 );
2944 let mut cap_path = Path::new();
2945 cap_path.move_to(cap_left.x, cap_left.y);
2946 cap_path.line_to(cap_right.x, cap_right.y);
2947 renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
2948
2949 let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2951 let wu_top =
2952 self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2953 let mut wu_path = Path::new();
2954 wu_path.move_to(wu_bottom.x, wu_bottom.y);
2955 wu_path.line_to(wu_top.x, wu_top.y);
2956 renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
2957
2958 let ucap_left = self.data_to_pixel(
2960 cx - half * 0.5,
2961 stats.whisker_high,
2962 plot_area,
2963 xmin,
2964 xmax,
2965 ymin,
2966 ymax,
2967 );
2968 let ucap_right = self.data_to_pixel(
2969 cx + half * 0.5,
2970 stats.whisker_high,
2971 plot_area,
2972 xmin,
2973 xmax,
2974 ymin,
2975 ymax,
2976 );
2977 let mut ucap_path = Path::new();
2978 ucap_path.move_to(ucap_left.x, ucap_left.y);
2979 ucap_path.line_to(ucap_right.x, ucap_right.y);
2980 renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
2981
2982 if artist.show_outliers {
2984 let r = 3.0;
2985 for &val in &stats.outliers {
2986 let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
2987 let mut dot = Path::new();
2988 for seg in 0..8 {
2989 let angle = std::f64::consts::TAU * seg as f64 / 8.0;
2990 let dx = r * angle.cos();
2991 let dy = r * angle.sin();
2992 if seg == 0 {
2993 dot.move_to(pt.x + dx, pt.y + dy);
2994 } else {
2995 dot.line_to(pt.x + dx, pt.y + dy);
2996 }
2997 }
2998 dot.close();
2999 renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
3000 renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
3001 }
3002 }
3003 }
3004 }
3005
3006 fn draw_step(
3008 &self,
3009 renderer: &mut impl Renderer,
3010 artist: &StepArtist,
3011 plot_area: &Rect,
3012 xmin: f64,
3013 xmax: f64,
3014 ymin: f64,
3015 ymax: f64,
3016 ) {
3017 if artist.x.len() < 2 {
3018 return;
3019 }
3020 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
3021 let paint = Paint::new(color);
3022 let stroke = Stroke::new(artist.width);
3023
3024 let mut path = Path::new();
3025 let first = self.data_to_pixel(
3026 artist.x.data[0],
3027 artist.y.data[0],
3028 plot_area,
3029 xmin,
3030 xmax,
3031 ymin,
3032 ymax,
3033 );
3034 path.move_to(first.x, first.y);
3035
3036 for i in 1..artist.x.len() {
3037 let prev = self.data_to_pixel(
3038 artist.x.data[i - 1],
3039 artist.y.data[i - 1],
3040 plot_area,
3041 xmin,
3042 xmax,
3043 ymin,
3044 ymax,
3045 );
3046 let cur = self.data_to_pixel(
3047 artist.x.data[i],
3048 artist.y.data[i],
3049 plot_area,
3050 xmin,
3051 xmax,
3052 ymin,
3053 ymax,
3054 );
3055 match artist.where_step {
3056 StepWhere::Pre => {
3057 path.line_to(prev.x, cur.y);
3058 path.line_to(cur.x, cur.y);
3059 }
3060 StepWhere::Post => {
3061 path.line_to(cur.x, prev.y);
3062 path.line_to(cur.x, cur.y);
3063 }
3064 StepWhere::Mid => {
3065 let mid_x = (prev.x + cur.x) / 2.0;
3066 path.line_to(mid_x, prev.y);
3067 path.line_to(mid_x, cur.y);
3068 path.line_to(cur.x, cur.y);
3069 }
3070 }
3071 }
3072 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3073 }
3074
3075 fn draw_stem(
3077 &self,
3078 renderer: &mut impl Renderer,
3079 artist: &StemArtist,
3080 plot_area: &Rect,
3081 xmin: f64,
3082 xmax: f64,
3083 ymin: f64,
3084 ymax: f64,
3085 ) {
3086 if artist.x.is_empty() {
3087 return;
3088 }
3089 let alpha_byte = (artist.alpha * 255.0) as u8;
3090 let color = artist.color.with_alpha(alpha_byte);
3091 let paint = Paint::new(color);
3092 let stroke = Stroke::new(artist.line_width);
3093 let radius = artist.marker_size / 2.0;
3094
3095 let bl_left = self.data_to_pixel(
3097 artist.x.data[0],
3098 artist.baseline,
3099 plot_area,
3100 xmin,
3101 xmax,
3102 ymin,
3103 ymax,
3104 );
3105 let bl_right = self.data_to_pixel(
3106 *artist.x.data.last().unwrap(),
3107 artist.baseline,
3108 plot_area,
3109 xmin,
3110 xmax,
3111 ymin,
3112 ymax,
3113 );
3114 let mut bl_path = Path::new();
3115 bl_path.move_to(bl_left.x, bl_left.y);
3116 bl_path.line_to(bl_right.x, bl_right.y);
3117 let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
3118 let bl_stroke = Stroke::new(0.8);
3119 renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
3120
3121 for i in 0..artist.x.len() {
3123 let base = self.data_to_pixel(
3124 artist.x.data[i],
3125 artist.baseline,
3126 plot_area,
3127 xmin,
3128 xmax,
3129 ymin,
3130 ymax,
3131 );
3132 let tip = self.data_to_pixel(
3133 artist.x.data[i],
3134 artist.y.data[i],
3135 plot_area,
3136 xmin,
3137 xmax,
3138 ymin,
3139 ymax,
3140 );
3141 let mut stem_path = Path::new();
3142 stem_path.move_to(base.x, base.y);
3143 stem_path.line_to(tip.x, tip.y);
3144 renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
3145 let marker = Path::circle(tip, radius);
3146 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
3147 }
3148 }
3149
3150 fn draw_errorbar(
3153 &self,
3154 renderer: &mut impl Renderer,
3155 artist: &ErrorBarArtist,
3156 plot_area: &Rect,
3157 xmin: f64,
3158 xmax: f64,
3159 ymin: f64,
3160 ymax: f64,
3161 ) {
3162 if artist.x.is_empty() {
3163 return;
3164 }
3165 let paint = Paint::new(artist.color);
3166 let stroke = Stroke::new(artist.line_width);
3167 let marker_radius = 3.0;
3168
3169 let mut line_path = Path::new();
3171 let first = self.data_to_pixel(
3172 artist.x.data[0],
3173 artist.y.data[0],
3174 plot_area,
3175 xmin,
3176 xmax,
3177 ymin,
3178 ymax,
3179 );
3180 line_path.move_to(first.x, first.y);
3181 for i in 1..artist.x.len() {
3182 let pt = self.data_to_pixel(
3183 artist.x.data[i],
3184 artist.y.data[i],
3185 plot_area,
3186 xmin,
3187 xmax,
3188 ymin,
3189 ymax,
3190 );
3191 line_path.line_to(pt.x, pt.y);
3192 }
3193 renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
3194
3195 for i in 0..artist.x.len() {
3197 let xv = artist.x.data[i];
3198 let yv = artist.y.data[i];
3199 let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
3200
3201 let marker = Path::circle(center, marker_radius);
3203 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
3204
3205 if let Some(ref yerr) = artist.yerr {
3207 let (lo, hi) = match yerr {
3208 ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
3209 ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
3210 };
3211 let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
3212 let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
3213
3214 let mut bar = Path::new();
3216 bar.move_to(pt_lo.x, pt_lo.y);
3217 bar.line_to(pt_hi.x, pt_hi.y);
3218 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3219
3220 if artist.cap_size > 0.0 {
3222 let half_cap = artist.cap_size / 2.0;
3223 let mut cap_lo = Path::new();
3224 cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
3225 cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
3226 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3227
3228 let mut cap_hi = Path::new();
3229 cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
3230 cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
3231 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3232 }
3233 }
3234
3235 if let Some(ref xerr) = artist.xerr {
3237 let (lo, hi) = match xerr {
3238 ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
3239 ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
3240 };
3241 let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
3242 let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
3243
3244 let mut bar = Path::new();
3246 bar.move_to(pt_lo.x, pt_lo.y);
3247 bar.line_to(pt_hi.x, pt_hi.y);
3248 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3249
3250 if artist.cap_size > 0.0 {
3252 let half_cap = artist.cap_size / 2.0;
3253 let mut cap_lo = Path::new();
3254 cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
3255 cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
3256 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3257
3258 let mut cap_hi = Path::new();
3259 cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
3260 cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
3261 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3262 }
3263 }
3264 }
3265 }
3266
3267 fn draw_heatmap(
3270 &self,
3271 renderer: &mut impl Renderer,
3272 artist: &HeatmapArtist,
3273 plot_area: &Rect,
3274 xmin: f64,
3275 xmax: f64,
3276 ymin: f64,
3277 ymax: f64,
3278 ) {
3279 let nrows = artist.data.len();
3280 if nrows == 0 {
3281 return;
3282 }
3283 let ncols = artist.data[0].len();
3284 if ncols == 0 {
3285 return;
3286 }
3287
3288 let vmin = artist.effective_vmin();
3289 let vmax = artist.effective_vmax();
3290
3291 let text_style = TextStyle {
3292 size: 10.0,
3293 color: Color::BLACK,
3294 weight: FontWeight::Normal,
3295 family: None,
3296 halign: HAlign::Center,
3297 valign: VAlign::Middle,
3298 };
3299
3300 for row in 0..nrows {
3301 for col in 0..ncols {
3302 let val = artist.data[row][col];
3303 let cell_color = artist.cmap.map_value(val, vmin, vmax);
3304
3305 let p_bl =
3307 self.data_to_pixel(col as f64, row as f64, plot_area, xmin, xmax, ymin, ymax);
3308 let p_tr = self.data_to_pixel(
3309 (col + 1) as f64,
3310 (row + 1) as f64,
3311 plot_area,
3312 xmin,
3313 xmax,
3314 ymin,
3315 ymax,
3316 );
3317 let rect = Rect::from_points(p_bl, p_tr);
3318 let cell_path = Path::rect(rect);
3319 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
3320
3321 if artist.show_values {
3323 let cx = (p_bl.x + p_tr.x) / 2.0;
3324 let cy = (p_bl.y + p_tr.y) / 2.0;
3325 let label = format!("{val:.1}");
3326 renderer.draw_text(&label, Point::new(cx, cy), &text_style, Affine::IDENTITY);
3327 }
3328 }
3329 }
3330 }
3331
3332 fn draw_polar(
3335 &self,
3336 renderer: &mut impl Renderer,
3337 artist: &PolarArtist,
3338 plot_area: &Rect,
3339 xmin: f64,
3340 xmax: f64,
3341 ymin: f64,
3342 ymax: f64,
3343 theme: &Theme,
3344 ) {
3345 let n = artist.r.len().min(artist.theta.len());
3346 if n == 0 {
3347 return;
3348 }
3349
3350 let r_max = artist.max_finite_r();
3351 if r_max <= 0.0 || !r_max.is_finite() {
3352 return;
3353 }
3354
3355 let center = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3358
3359 let max_radius_px = (plot_area.width / 2.0).min(plot_area.height / 2.0) * 0.85;
3361
3362 let num_r_rings = 5;
3364 let r_step = r_max / num_r_rings as f64;
3365 let grid_color = theme.grid_color;
3366 let grid_paint = Paint::new(grid_color.with_alpha(100));
3367 let grid_stroke = Stroke::new(0.5);
3368 let label_style = TextStyle {
3369 size: 9.0,
3370 color: theme.tick_color,
3371 weight: FontWeight::Normal,
3372 family: theme.font_family.clone(),
3373 halign: HAlign::Left,
3374 valign: VAlign::Middle,
3375 };
3376
3377 for i in 1..=num_r_rings {
3378 let r_val = i as f64 * r_step;
3379 let r_px = r_val / r_max * max_radius_px;
3380 let circle = Path::circle(center, r_px);
3381 renderer.stroke_path(&circle, &grid_paint, &grid_stroke, Affine::IDENTITY);
3382
3383 let label_pt = Point::new(center.x + r_px + 3.0, center.y - 2.0);
3385 let label_text = if r_val == r_val.floor() {
3386 format!("{:.0}", r_val)
3387 } else {
3388 format!("{:.1}", r_val)
3389 };
3390 renderer.draw_text(&label_text, label_pt, &label_style, Affine::IDENTITY);
3391 }
3392
3393 let angle_label_style = TextStyle {
3395 size: 10.0,
3396 color: theme.tick_color,
3397 weight: FontWeight::Normal,
3398 family: theme.font_family.clone(),
3399 halign: HAlign::Center,
3400 valign: VAlign::Middle,
3401 };
3402
3403 for deg in (0..360).step_by(30) {
3404 let angle = (deg as f64).to_radians();
3405 let end_x = center.x + max_radius_px * angle.cos();
3406 let end_y = center.y - max_radius_px * angle.sin();
3407 let mut line = Path::new();
3408 line.move_to(center.x, center.y);
3409 line.line_to(end_x, end_y);
3410 renderer.stroke_path(&line, &grid_paint, &grid_stroke, Affine::IDENTITY);
3411
3412 let label_offset = 14.0;
3414 let lx = center.x + (max_radius_px + label_offset) * angle.cos();
3415 let ly = center.y - (max_radius_px + label_offset) * angle.sin();
3416 let label_text = format!("{}°", deg);
3417 renderer.draw_text(
3418 &label_text,
3419 Point::new(lx, ly),
3420 &angle_label_style,
3421 Affine::IDENTITY,
3422 );
3423 }
3424
3425 let to_px = |r: f64, theta: f64| -> Point {
3428 let px_r = r / r_max * max_radius_px;
3429 Point::new(center.x + px_r * theta.cos(), center.y - px_r * theta.sin())
3430 };
3431
3432 let mut path = Path::new();
3433 let mut started = false;
3434
3435 for i in 0..n {
3436 let r = artist.r[i];
3437 let theta = artist.theta[i];
3438 if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3439 continue;
3440 }
3441 let pt = to_px(r, theta);
3442 if !started {
3443 path.move_to(pt.x, pt.y);
3444 started = true;
3445 } else {
3446 path.line_to(pt.x, pt.y);
3447 }
3448 }
3449
3450 if !started {
3451 return;
3452 }
3453
3454 let alpha_byte = (artist.alpha * 255.0) as u8;
3455 let color = artist.color.with_alpha(alpha_byte);
3456
3457 if artist.filled {
3458 path.close();
3460 let fill_paint = Paint::new(color);
3461 renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
3462 let stroke_paint = Paint::new(
3464 artist
3465 .color
3466 .with_alpha(((artist.alpha.min(1.0) * 0.8 + 0.2) * 255.0) as u8),
3467 );
3468 let stroke = Stroke::new(artist.linewidth);
3469 renderer.stroke_path(&path, &stroke_paint, &stroke, Affine::IDENTITY);
3470 } else {
3471 let paint = Paint::new(color);
3473 let stroke = Stroke::new(artist.linewidth);
3474 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3475 }
3476
3477 if let Some(marker) = artist.marker {
3479 let marker_size = 5.0;
3480 let marker_radius = marker_size / 2.0;
3481 let marker_paint = Paint::new(color);
3482
3483 for i in 0..n {
3484 let r = artist.r[i];
3485 let theta = artist.theta[i];
3486 if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3487 continue;
3488 }
3489 let pt = to_px(r, theta);
3490 let marker_path = match marker {
3491 Marker::Circle | Marker::Point => Path::circle(pt, marker_radius),
3492 Marker::Square => Path::rect(Rect::new(
3493 pt.x - marker_radius,
3494 pt.y - marker_radius,
3495 marker_size,
3496 marker_size,
3497 )),
3498 Marker::Diamond => {
3499 let mut p = Path::new();
3500 p.move_to(pt.x, pt.y - marker_radius);
3501 p.line_to(pt.x + marker_radius, pt.y);
3502 p.line_to(pt.x, pt.y + marker_radius);
3503 p.line_to(pt.x - marker_radius, pt.y);
3504 p.close();
3505 p
3506 }
3507 Marker::Triangle => {
3508 let mut p = Path::new();
3509 let h = marker_radius * 1.1547;
3510 p.move_to(pt.x, pt.y - marker_radius);
3511 p.line_to(pt.x + h * 0.5, pt.y + marker_radius * 0.5);
3512 p.line_to(pt.x - h * 0.5, pt.y + marker_radius * 0.5);
3513 p.close();
3514 p
3515 }
3516 Marker::Plus => {
3517 let mut p = Path::new();
3518 p.move_to(pt.x - marker_radius, pt.y);
3519 p.line_to(pt.x + marker_radius, pt.y);
3520 p.move_to(pt.x, pt.y - marker_radius);
3521 p.line_to(pt.x, pt.y + marker_radius);
3522 let ms = Stroke::new(theme.line_width.max(1.0));
3523 renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3524 continue;
3525 }
3526 Marker::Cross => {
3527 let mut p = Path::new();
3528 let d = marker_radius * 0.707;
3529 p.move_to(pt.x - d, pt.y - d);
3530 p.line_to(pt.x + d, pt.y + d);
3531 p.move_to(pt.x + d, pt.y - d);
3532 p.line_to(pt.x - d, pt.y + d);
3533 let ms = Stroke::new(theme.line_width.max(1.0));
3534 renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3535 continue;
3536 }
3537 Marker::Star => {
3538 let mut p = Path::new();
3539 let inner = marker_radius * 0.382;
3540 for j in 0..10 {
3541 let a =
3542 std::f64::consts::FRAC_PI_2 + j as f64 * std::f64::consts::PI / 5.0;
3543 let r = if j % 2 == 0 { marker_radius } else { inner };
3544 let sx = pt.x + r * a.cos();
3545 let sy = pt.y - r * a.sin();
3546 if j == 0 {
3547 p.move_to(sx, sy);
3548 } else {
3549 p.line_to(sx, sy);
3550 }
3551 }
3552 p.close();
3553 p
3554 }
3555 };
3556 renderer.fill_path(&marker_path, &marker_paint, Affine::IDENTITY);
3557 }
3558 }
3559 }
3560
3561 fn draw_hexbin(
3564 &self,
3565 renderer: &mut impl Renderer,
3566 artist: &HexbinArtist,
3567 plot_area: &Rect,
3568 xmin: f64,
3569 xmax: f64,
3570 ymin: f64,
3571 ymax: f64,
3572 ) {
3573 use crate::charts::hexbin::{bin_hexagonal, hex_size_for_gridsize, hexagon_vertices};
3574
3575 let result = bin_hexagonal(&artist.x, &artist.y, artist.gridsize, artist.mincnt);
3576 if result.cells.is_empty() {
3577 return;
3578 }
3579
3580 let vmin = result.min_count as f64;
3581 let vmax = result.max_count as f64;
3582
3583 let data_xrange = (xmax - xmin).max(f64::EPSILON);
3585 let hex_data_size = hex_size_for_gridsize(data_xrange, artist.gridsize);
3586
3587 let alpha_byte = (artist.alpha * 255.0).round() as u8;
3588
3589 for &(cx, cy, count) in &result.cells {
3590 let mut fill_color = artist.cmap.map_value(count as f64, vmin, vmax);
3592 fill_color = fill_color.with_alpha(alpha_byte);
3593
3594 let data_verts = hexagon_vertices(cx, cy, hex_data_size);
3596
3597 let mut path = Path::new();
3598 for (i, &(vx, vy)) in data_verts.iter().enumerate() {
3599 let p = self.data_to_pixel(vx, vy, plot_area, xmin, xmax, ymin, ymax);
3600 if i == 0 {
3601 path.move_to(p.x, p.y);
3602 } else {
3603 path.line_to(p.x, p.y);
3604 }
3605 }
3606 path.close();
3607
3608 renderer.fill_path(&path, &Paint::new(fill_color), Affine::IDENTITY);
3609
3610 if let Some(edge_color) = artist.edgecolor {
3612 let stroke = Stroke::new(0.5);
3613 renderer.stroke_path(
3614 &path,
3615 &Paint::new(edge_color.with_alpha(alpha_byte)),
3616 &stroke,
3617 Affine::IDENTITY,
3618 );
3619 }
3620 }
3621 }
3622
3623 fn draw_waterfall(
3625 &self,
3626 renderer: &mut impl Renderer,
3627 artist: &WaterfallArtist,
3628 plot_area: &Rect,
3629 xmin: f64,
3630 xmax: f64,
3631 ymin: f64,
3632 ymax: f64,
3633 ) {
3634 let n = artist.categories.len();
3635 if n == 0 {
3636 return;
3637 }
3638
3639 let positions = artist.bar_positions();
3640 let cumsum = artist.cumulative_sums();
3641
3642 let cat_range = xmax - xmin;
3643 let cat_step = cat_range / n as f64;
3644 let bar_half = cat_step * artist.bar_width * 0.5;
3645
3646 for i in 0..n {
3647 let (base, top) = positions[i];
3648
3649 let bar_color = if artist.total_indices.contains(&i) {
3651 artist.total_color
3652 } else if artist.values.data[i] >= 0.0 {
3653 artist.increase_color
3654 } else {
3655 artist.decrease_color
3656 };
3657
3658 let color = bar_color.with_alpha((artist.alpha * 255.0) as u8);
3659 let paint = Paint::new(color);
3660
3661 let cat_center = xmin + (i as f64 + 0.5) * cat_step;
3662 let bottom_val = base.min(top);
3663 let top_val = base.max(top);
3664
3665 let p_bl = self.data_to_pixel(
3666 cat_center - bar_half,
3667 bottom_val,
3668 plot_area,
3669 xmin,
3670 xmax,
3671 ymin,
3672 ymax,
3673 );
3674 let p_tr = self.data_to_pixel(
3675 cat_center + bar_half,
3676 top_val,
3677 plot_area,
3678 xmin,
3679 xmax,
3680 ymin,
3681 ymax,
3682 );
3683
3684 let rect = Rect::from_points(p_bl, p_tr);
3685 let bar_path = Path::rect(rect);
3686 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
3687
3688 if artist.show_values {
3690 let display_val = if artist.total_indices.contains(&i) {
3691 cumsum[i]
3692 } else {
3693 artist.values.data[i]
3694 };
3695 let label_text = format_waterfall_value(display_val);
3696
3697 let label_y = if display_val >= 0.0 {
3699 top_val
3700 } else {
3701 bottom_val
3702 };
3703 let label_pos =
3704 self.data_to_pixel(cat_center, label_y, plot_area, xmin, xmax, ymin, ymax);
3705
3706 let text_style = TextStyle {
3707 size: 10.0,
3708 color: Color::BLACK,
3709 weight: FontWeight::Normal,
3710 family: None,
3711 halign: HAlign::Center,
3712 valign: if display_val >= 0.0 {
3713 VAlign::Bottom
3714 } else {
3715 VAlign::Top
3716 },
3717 };
3718 let offset_pos = Point::new(
3719 label_pos.x,
3720 if display_val >= 0.0 {
3721 label_pos.y - 3.0
3722 } else {
3723 label_pos.y + 3.0
3724 },
3725 );
3726 renderer.draw_text(&label_text, offset_pos, &text_style, Affine::IDENTITY);
3727 }
3728 }
3729
3730 if artist.connector_lines && n > 1 {
3732 let connector_paint = Paint::new(Color::rgb(0x80, 0x80, 0x80).with_alpha(180));
3733 let connector_stroke = Stroke::new(0.8);
3734
3735 for (i, &connector_y) in cumsum.iter().enumerate().take(n - 1) {
3736 let right_edge = xmin + (i as f64 + 0.5) * cat_step + bar_half;
3737 let left_edge = xmin + ((i + 1) as f64 + 0.5) * cat_step - bar_half;
3738
3739 let p_from =
3740 self.data_to_pixel(right_edge, connector_y, plot_area, xmin, xmax, ymin, ymax);
3741 let p_to =
3742 self.data_to_pixel(left_edge, connector_y, plot_area, xmin, xmax, ymin, ymax);
3743
3744 let mut path = Path::new();
3745 path.move_to(p_from.x, p_from.y);
3746 path.line_to(p_to.x, p_to.y);
3747 renderer.stroke_path(&path, &connector_paint, &connector_stroke, Affine::IDENTITY);
3748 }
3749 }
3750 }
3751
3752 fn draw_annotations(
3758 &self,
3759 renderer: &mut impl Renderer,
3760 plot_area: &Rect,
3761 xmin: f64,
3762 xmax: f64,
3763 ymin: f64,
3764 ymax: f64,
3765 theme: &Theme,
3766 ) {
3767 for ta in &self.texts {
3769 let pt = self.data_to_pixel(ta.x, ta.y, plot_area, xmin, xmax, ymin, ymax);
3770 let size = ta.fontsize.unwrap_or(theme.axis_label_size);
3771 let color = ta.color.unwrap_or(theme.text_color);
3772 let style = TextStyle {
3773 size,
3774 color,
3775 weight: FontWeight::Normal,
3776 family: theme.font_family.clone(),
3777 halign: ta.ha,
3778 valign: ta.va,
3779 };
3780 if ta.rotation.abs() < f64::EPSILON {
3781 renderer.draw_text(&ta.text, pt, &style, Affine::IDENTITY);
3782 } else {
3783 let angle_rad = -ta.rotation.to_radians();
3784 let rotate = Affine::rotate(angle_rad);
3785 let translate_to = Affine::translate(kurbo::Vec2::new(pt.x, pt.y));
3786 let translate_back = Affine::translate(kurbo::Vec2::new(-pt.x, -pt.y));
3787 let transform = translate_to * rotate * translate_back;
3788 renderer.draw_text(&ta.text, pt, &style, transform);
3789 }
3790 }
3791
3792 for ann in &self.annotations {
3794 let text_pt = self.data_to_pixel(
3795 ann.xytext.0,
3796 ann.xytext.1,
3797 plot_area,
3798 xmin,
3799 xmax,
3800 ymin,
3801 ymax,
3802 );
3803 let target_pt =
3804 self.data_to_pixel(ann.xy.0, ann.xy.1, plot_area, xmin, xmax, ymin, ymax);
3805
3806 let size = ann.fontsize.unwrap_or(theme.axis_label_size);
3807 let color = ann.color.unwrap_or(theme.text_color);
3808 let style = TextStyle {
3809 size,
3810 color,
3811 weight: FontWeight::Normal,
3812 family: theme.font_family.clone(),
3813 halign: ann.ha,
3814 valign: ann.va,
3815 };
3816 renderer.draw_text(&ann.text, text_pt, &style, Affine::IDENTITY);
3817
3818 if ann.arrowstyle != ArrowStyle::None {
3820 let arrow_col = ann.arrow_color.unwrap_or(color);
3821 self.draw_annotation_arrow(
3822 renderer,
3823 text_pt,
3824 target_pt,
3825 arrow_col,
3826 &ann.arrowstyle,
3827 );
3828 }
3829 }
3830 }
3831
3832 fn draw_annotation_arrow(
3834 &self,
3835 renderer: &mut impl Renderer,
3836 from: Point,
3837 to: Point,
3838 color: Color,
3839 style: &ArrowStyle,
3840 ) {
3841 let paint = Paint::new(color);
3842 let stroke = Stroke::new(1.0);
3843
3844 let dx = to.x - from.x;
3846 let dy = to.y - from.y;
3847 let len = (dx * dx + dy * dy).sqrt();
3848 if len < 1e-6 {
3849 return;
3850 }
3851
3852 let ux = dx / len;
3854 let uy = dy / len;
3855
3856 let mut line = Path::new();
3858 line.move_to(from.x, from.y);
3859 line.line_to(to.x, to.y);
3860 renderer.stroke_path(&line, &paint, &stroke, Affine::IDENTITY);
3861
3862 let head_len = match style {
3864 ArrowStyle::None => return,
3865 ArrowStyle::Simple => 8.0,
3866 ArrowStyle::Fancy => 12.0,
3867 };
3868 let head_half_width = match style {
3869 ArrowStyle::None => return,
3870 ArrowStyle::Simple => 3.0,
3871 ArrowStyle::Fancy => 5.0,
3872 };
3873
3874 let px = -uy;
3876 let py = ux;
3877
3878 let base_x = to.x - ux * head_len;
3880 let base_y = to.y - uy * head_len;
3881
3882 let left_x = base_x + px * head_half_width;
3883 let left_y = base_y + py * head_half_width;
3884 let right_x = base_x - px * head_half_width;
3885 let right_y = base_y - py * head_half_width;
3886
3887 let mut arrow = Path::new();
3888 arrow.move_to(to.x, to.y);
3889 arrow.line_to(left_x, left_y);
3890 arrow.line_to(right_x, right_y);
3891 arrow.close();
3892 renderer.fill_path(&arrow, &paint, Affine::IDENTITY);
3893 }
3894
3895 fn draw_pie(
3901 &self,
3902 renderer: &mut impl Renderer,
3903 artist: &PieArtist,
3904 plot_area: &Rect,
3905 xmin: f64,
3906 xmax: f64,
3907 ymin: f64,
3908 ymax: f64,
3909 theme: &Theme,
3910 ) {
3911 let n = artist.sizes.len();
3912 if n == 0 {
3913 return;
3914 }
3915
3916 let total: f64 = artist
3918 .sizes
3919 .iter()
3920 .copied()
3921 .filter(|v| v.is_finite() && *v > 0.0)
3922 .sum();
3923 if total <= 0.0 {
3924 return;
3925 }
3926 let fractions: Vec<f64> = artist
3927 .sizes
3928 .iter()
3929 .map(|&s| {
3930 if s.is_finite() && s > 0.0 {
3931 s / total
3932 } else {
3933 0.0
3934 }
3935 })
3936 .collect();
3937
3938 let center_px = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3940
3941 let edge_px = self.data_to_pixel(artist.radius, 0.0, plot_area, xmin, xmax, ymin, ymax);
3943 let radius_px = (edge_px.x - center_px.x).abs();
3944
3945 let start_rad = artist.start_angle.to_radians();
3946 let mut current_angle = start_rad;
3947
3948 let pct_style = TextStyle {
3949 size: 10.0,
3950 color: Color::BLACK,
3951 weight: FontWeight::Normal,
3952 family: None,
3953 halign: HAlign::Center,
3954 valign: VAlign::Middle,
3955 };
3956 let label_style = TextStyle {
3957 size: 11.0,
3958 color: theme.tick_color,
3959 weight: FontWeight::Normal,
3960 family: None,
3961 halign: HAlign::Center,
3962 valign: VAlign::Middle,
3963 };
3964
3965 for i in 0..n {
3966 let frac = fractions[i];
3967 if frac <= 0.0 {
3968 current_angle += frac * std::f64::consts::TAU;
3969 continue;
3970 }
3971
3972 let sweep = frac * std::f64::consts::TAU;
3973 let mid_angle = current_angle + sweep / 2.0;
3974
3975 let wedge_color = if let Some(ref colors) = artist.colors {
3977 colors[i % colors.len()]
3978 } else {
3979 Color::TABLEAU_10[i % 10]
3980 };
3981
3982 let explode_frac = artist
3984 .explode
3985 .as_ref()
3986 .map(|e| if i < e.len() { e[i] } else { 0.0 })
3987 .unwrap_or(0.0);
3988 let offset_x = explode_frac * radius_px * mid_angle.cos();
3989 let offset_y = explode_frac * radius_px * (-mid_angle.sin()); let cx = center_px.x + offset_x;
3992 let cy = center_px.y + offset_y;
3993
3994 let mut path = Path::new();
3997 path.move_to(cx, cy);
3998
3999 let arc_start_x = cx + radius_px * current_angle.cos();
4000 let arc_start_y = cy - radius_px * current_angle.sin();
4001 path.line_to(arc_start_x, arc_start_y);
4002
4003 let max_sub = std::f64::consts::FRAC_PI_2;
4005 let num_segments = (sweep / max_sub).ceil() as usize;
4006 let seg_sweep = sweep / num_segments as f64;
4007 let mut seg_start = current_angle;
4008
4009 for _ in 0..num_segments {
4010 let seg_end = seg_start + seg_sweep;
4011 let half = seg_sweep / 2.0;
4013 let alpha = (4.0 / 3.0) * (half / 2.0).tan();
4014
4015 let p0x = cx + radius_px * seg_start.cos();
4016 let p0y = cy - radius_px * seg_start.sin();
4017 let p3x = cx + radius_px * seg_end.cos();
4018 let p3y = cy - radius_px * seg_end.sin();
4019
4020 let t0x = -seg_start.sin();
4022 let t0y = -seg_start.cos(); let t1x = -seg_end.sin();
4025 let t1y = -seg_end.cos(); let cp1x = p0x + alpha * radius_px * t0x;
4028 let cp1y = p0y + alpha * radius_px * t0y;
4029 let cp2x = p3x - alpha * radius_px * t1x;
4030 let cp2y = p3y - alpha * radius_px * t1y;
4031
4032 let _ = path.curve_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
4033 seg_start = seg_end;
4034 }
4035
4036 path.close();
4037
4038 let paint = Paint::new(wedge_color);
4039 renderer.fill_path(&path, &paint, Affine::IDENTITY);
4040
4041 let outline_paint = Paint::new(Color::WHITE);
4043 let outline_stroke = Stroke::new(1.5);
4044 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
4045
4046 if artist.autopct {
4048 let pct_r = radius_px * 0.6;
4049 let pct_x = cx + pct_r * mid_angle.cos();
4050 let pct_y = cy - pct_r * mid_angle.sin();
4051 let pct_text = format!("{:.1}%", frac * 100.0);
4052 renderer.draw_text(
4053 &pct_text,
4054 Point::new(pct_x, pct_y),
4055 &pct_style,
4056 Affine::IDENTITY,
4057 );
4058 }
4059
4060 if let Some(ref labels) = artist.labels {
4062 if i < labels.len() {
4063 let label_r = radius_px * 1.15;
4064 let lx = cx + label_r * mid_angle.cos();
4065 let ly = cy - label_r * mid_angle.sin();
4066 renderer.draw_text(
4067 &labels[i],
4068 Point::new(lx, ly),
4069 &label_style,
4070 Affine::IDENTITY,
4071 );
4072 }
4073 }
4074
4075 current_angle += sweep;
4076 }
4077 }
4078
4079 fn draw_contour(
4081 &self,
4082 renderer: &mut impl Renderer,
4083 artist: &ContourArtist,
4084 plot_area: &Rect,
4085 xmin: f64,
4086 xmax: f64,
4087 ymin: f64,
4088 ymax: f64,
4089 ) {
4090 let nx = artist.x.len();
4091 let ny = artist.y.len();
4092 if nx < 2 || ny < 2 || artist.z.len() < 2 {
4093 return;
4094 }
4095
4096 let levels = artist.effective_levels();
4097 let (zmin, zmax) = artist.z_bounds();
4098
4099 if artist.filled {
4100 let avgs = artist.cell_averages();
4101 for (j, row) in avgs.iter().enumerate() {
4102 for (i, &avg) in row.iter().enumerate() {
4103 if !avg.is_finite() {
4104 continue;
4105 }
4106 let cell_color = if let Some(ref colors) = artist.colors {
4107 let idx = levels
4108 .iter()
4109 .position(|&l| avg < l)
4110 .unwrap_or(levels.len())
4111 .saturating_sub(1);
4112 colors[idx % colors.len()]
4113 } else {
4114 artist.cmap.map_value(avg, zmin, zmax)
4115 };
4116 let p_bl = self.data_to_pixel(
4117 artist.x[i],
4118 artist.y[j],
4119 plot_area,
4120 xmin,
4121 xmax,
4122 ymin,
4123 ymax,
4124 );
4125 let p_tr = self.data_to_pixel(
4126 artist.x[i + 1],
4127 artist.y[j + 1],
4128 plot_area,
4129 xmin,
4130 xmax,
4131 ymin,
4132 ymax,
4133 );
4134 let rect = Rect::from_points(p_bl, p_tr);
4135 let cell_path = Path::rect(rect);
4136 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
4137 }
4138 }
4139 }
4140
4141 if !artist.filled {
4142 for (li, &level) in levels.iter().enumerate() {
4143 let segments = artist.marching_squares(level);
4144 if segments.is_empty() {
4145 continue;
4146 }
4147 let line_color = if let Some(ref colors) = artist.colors {
4148 colors[li % colors.len()]
4149 } else {
4150 artist.cmap.map_value(level, zmin, zmax)
4151 };
4152 let paint = Paint::new(line_color);
4153 let stroke = Stroke::new(artist.linewidths);
4154 for (sx0, sy0, sx1, sy1) in &segments {
4155 let p0 = self.data_to_pixel(*sx0, *sy0, plot_area, xmin, xmax, ymin, ymax);
4156 let p1 = self.data_to_pixel(*sx1, *sy1, plot_area, xmin, xmax, ymin, ymax);
4157 let mut path = Path::new();
4158 path.move_to(p0.x, p0.y);
4159 path.line_to(p1.x, p1.y);
4160 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
4161 }
4162 }
4163 }
4164 }
4165
4166 fn draw_violin(
4168 &self,
4169 renderer: &mut impl Renderer,
4170 artist: &ViolinArtist,
4171 plot_area: &Rect,
4172 xmin: f64,
4173 xmax: f64,
4174 ymin: f64,
4175 ymax: f64,
4176 theme: &Theme,
4177 ) {
4178 use crate::charts::violin::{gaussian_kde, silverman_bandwidth};
4179
4180 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
4181 let fill_paint = Paint::new(fill_color);
4182 let outline_paint = Paint::new(artist.color);
4183 let outline_stroke = Stroke::new(1.0);
4184
4185 for (di, data) in artist.datasets.iter().enumerate() {
4186 let mut sorted: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
4187 if sorted.is_empty() {
4188 continue;
4189 }
4190 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
4191
4192 let pos = artist
4193 .positions
4194 .as_ref()
4195 .and_then(|p| p.get(di).copied())
4196 .unwrap_or(di as f64 + 1.0);
4197
4198 let bw = if artist.bw_method > 0.0 {
4199 artist.bw_method
4200 } else {
4201 silverman_bandwidth(&sorted)
4202 };
4203
4204 let data_min = sorted[0];
4205 let data_max = sorted[sorted.len() - 1];
4206 let n_eval = 100;
4207 let eval_points: Vec<f64> = (0..n_eval)
4208 .map(|i| data_min + (data_max - data_min) * i as f64 / (n_eval - 1) as f64)
4209 .collect();
4210 let densities = gaussian_kde(&sorted, bw, &eval_points);
4211
4212 let max_density = densities.iter().copied().fold(0.0_f64, f64::max);
4213 if max_density <= 0.0 {
4214 continue;
4215 }
4216
4217 let half_width = artist.widths * 0.5;
4218
4219 let mut path = Path::new();
4221 let first_y = eval_points[0];
4223 let first_w = densities[0] / max_density * half_width;
4224 let fp = self.data_to_pixel(pos + first_w, first_y, plot_area, xmin, xmax, ymin, ymax);
4225 path.move_to(fp.x, fp.y);
4226 for i in 1..n_eval {
4227 let w = densities[i] / max_density * half_width;
4228 let p =
4229 self.data_to_pixel(pos + w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4230 path.line_to(p.x, p.y);
4231 }
4232 for i in (0..n_eval).rev() {
4234 let w = densities[i] / max_density * half_width;
4235 let p =
4236 self.data_to_pixel(pos - w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4237 path.line_to(p.x, p.y);
4238 }
4239 path.close();
4240
4241 renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
4242 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
4243
4244 let n = sorted.len();
4246 if artist.show_median {
4247 let median = if n % 2 == 0 {
4248 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
4249 } else {
4250 sorted[n / 2]
4251 };
4252 let med_density = gaussian_kde(&sorted, bw, &[median])[0];
4253 let med_w = med_density / max_density * half_width;
4254 let p1 = self.data_to_pixel(pos - med_w, median, plot_area, xmin, xmax, ymin, ymax);
4255 let p2 = self.data_to_pixel(pos + med_w, median, plot_area, xmin, xmax, ymin, ymax);
4256 let mut mp = Path::new();
4257 mp.move_to(p1.x, p1.y);
4258 mp.line_to(p2.x, p2.y);
4259 let med_paint = Paint::new(theme.text_color);
4260 let med_stroke = Stroke::new(2.0);
4261 renderer.stroke_path(&mp, &med_paint, &med_stroke, Affine::IDENTITY);
4262 }
4263
4264 if artist.show_quartiles && n >= 4 {
4265 let q1 = sorted[n / 4];
4266 let q3 = sorted[3 * n / 4];
4267 for q in [q1, q3] {
4268 let q_density = gaussian_kde(&sorted, bw, &[q])[0];
4269 let q_w = q_density / max_density * half_width;
4270 let p1 = self.data_to_pixel(pos - q_w, q, plot_area, xmin, xmax, ymin, ymax);
4271 let p2 = self.data_to_pixel(pos + q_w, q, plot_area, xmin, xmax, ymin, ymax);
4272 let mut qp = Path::new();
4273 qp.move_to(p1.x, p1.y);
4274 qp.line_to(p2.x, p2.y);
4275 let q_stroke = Stroke::new(1.0).with_dash(DashPattern {
4276 dashes: vec![4.0, 2.0],
4277 offset: 0.0,
4278 });
4279 renderer.stroke_path(
4280 &qp,
4281 &Paint::new(theme.text_color),
4282 &q_stroke,
4283 Affine::IDENTITY,
4284 );
4285 }
4286 }
4287 }
4288 }
4289
4290 fn draw_legend(&self, renderer: &mut impl Renderer, plot_area: &Rect, theme: &Theme) {
4291 let entries: Vec<LegendEntry> = self
4294 .artists
4295 .iter()
4296 .filter_map(|a| {
4297 let (label, color, swatch) = match a {
4298 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4299 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4300 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4301 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4302 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4303 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4304 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4305 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4306 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4307 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4308 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4309 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4310 Artist::Contour(a) => (
4311 a.label.as_deref(),
4312 a.color,
4313 if a.filled {
4314 SwatchKind::Filled
4315 } else {
4316 SwatchKind::Line
4317 },
4318 ),
4319 Artist::Polar(a) => (
4320 a.label.as_deref(),
4321 a.color,
4322 if a.filled {
4323 SwatchKind::Filled
4324 } else {
4325 SwatchKind::Line
4326 },
4327 ),
4328 Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4329 Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4330 };
4331 label.map(|l| LegendEntry {
4332 label: l.to_string(),
4333 color,
4334 swatch,
4335 })
4336 })
4337 .collect();
4338
4339 legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
4340 }
4341
4342 fn data_to_pixel(
4352 &self,
4353 x: f64,
4354 y: f64,
4355 plot_area: &Rect,
4356 xmin: f64,
4357 xmax: f64,
4358 ymin: f64,
4359 ymax: f64,
4360 ) -> Point {
4361 let tx = self.xscale.transform(x, xmin, xmax);
4362 let ty = self.yscale.transform(y, ymin, ymax);
4363 Point::new(
4364 plot_area.x + tx * plot_area.width,
4365 plot_area.y + (1.0 - ty) * plot_area.height, )
4367 }
4368}
4369
4370fn format_waterfall_value(v: f64) -> String {
4380 if v == v.trunc() && v.abs() < 1e15 {
4381 format!("{}", v as i64)
4382 } else {
4383 let s = format!("{:.2}", v);
4384 s.trim_end_matches('0').trim_end_matches('.').to_string()
4385 }
4386}
4387
4388#[cfg(test)]
4393mod tests {
4394 use super::*;
4395
4396 #[test]
4397 fn new_axes_has_defaults() {
4398 let ax = Axes::new();
4399 assert!(ax.artists.is_empty());
4400 assert!(ax.title.is_none());
4401 assert!(ax.xlabel.is_none());
4402 assert!(ax.ylabel.is_none());
4403 assert!(ax.xlim.is_none());
4404 assert!(ax.ylim.is_none());
4405 assert!(!ax.show_legend);
4406 assert_eq!(ax.color_index, 0);
4407 }
4408
4409 #[test]
4410 fn plot_creates_line_artist() {
4411 let mut ax = Axes::new();
4412 let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
4413 assert!(result.is_ok());
4414 assert_eq!(ax.artists.len(), 1);
4415 assert!(matches!(&ax.artists[0], Artist::Line(_)));
4416 assert_eq!(ax.color_index, 1);
4417 }
4418
4419 #[test]
4420 fn plot_length_mismatch() {
4421 let mut ax = Axes::new();
4422 let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
4423 assert!(matches!(
4424 result,
4425 Err(PlotError::SeriesLengthMismatch {
4426 expected: 2,
4427 got: 1
4428 })
4429 ));
4430 }
4431
4432 #[test]
4433 fn plot_empty_data() {
4434 let mut ax = Axes::new();
4435 let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
4436 assert!(matches!(result, Err(PlotError::EmptyData)));
4437 }
4438
4439 #[test]
4440 fn scatter_creates_artist() {
4441 let mut ax = Axes::new();
4442 let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
4443 assert!(result.is_ok());
4444 assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
4445 }
4446
4447 #[test]
4448 fn bar_creates_artist() {
4449 let mut ax = Axes::new();
4450 let cats: &[&str] = &["a", "b", "c"];
4451 let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
4452 assert!(result.is_ok());
4453 match &ax.artists[0] {
4454 Artist::Bar(a) => {
4455 assert!(!a.horizontal);
4456 assert_eq!(a.categories.len(), 3);
4457 }
4458 _ => panic!("expected Bar artist"),
4459 }
4460 }
4461
4462 #[test]
4463 fn barh_creates_horizontal_artist() {
4464 let mut ax = Axes::new();
4465 let cats: &[&str] = &["x", "y"];
4466 let result = ax.barh(cats, vec![5.0, 10.0]);
4467 assert!(result.is_ok());
4468 match &ax.artists[0] {
4469 Artist::Bar(a) => assert!(a.horizontal),
4470 _ => panic!("expected Bar artist"),
4471 }
4472 }
4473
4474 #[test]
4475 fn hist_computes_bins() {
4476 let mut ax = Axes::new();
4477 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
4478 let result = ax.hist(data, 5);
4479 assert!(result.is_ok());
4480 match &ax.artists[0] {
4481 Artist::Histogram(a) => {
4482 assert_eq!(a.bin_edges.len(), 6); assert_eq!(a.counts.len(), 5);
4484 let total: f64 = a.counts.iter().sum();
4486 assert_eq!(total, 10.0);
4487 }
4488 _ => panic!("expected Hist artist"),
4489 }
4490 }
4491
4492 #[test]
4493 fn hist_single_value() {
4494 let mut ax = Axes::new();
4495 let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
4496 assert!(result.is_ok());
4497 match &ax.artists[0] {
4498 Artist::Histogram(a) => {
4499 let total: f64 = a.counts.iter().sum();
4500 assert_eq!(total, 3.0);
4501 }
4502 _ => panic!("expected Hist artist"),
4503 }
4504 }
4505
4506 #[test]
4507 fn hist_empty_data() {
4508 let mut ax = Axes::new();
4509 let result = ax.hist(Vec::<f64>::new(), 10);
4510 assert!(matches!(result, Err(PlotError::EmptyData)));
4511 }
4512
4513 #[test]
4514 fn fill_between_creates_artist() {
4515 let mut ax = Axes::new();
4516 let result = ax.fill_between(
4517 vec![1.0, 2.0, 3.0],
4518 vec![1.0, 2.0, 1.0],
4519 vec![0.0, 0.0, 0.0],
4520 );
4521 assert!(result.is_ok());
4522 assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
4523 }
4524
4525 #[test]
4526 fn fill_between_length_mismatch() {
4527 let mut ax = Axes::new();
4528 let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
4529 assert!(matches!(
4530 result,
4531 Err(PlotError::SeriesLengthMismatch { .. })
4532 ));
4533 }
4534
4535 #[test]
4536 fn configuration_methods_return_self() {
4537 let mut ax = Axes::new();
4538 ax.set_title("Test")
4539 .set_xlabel("X")
4540 .set_ylabel("Y")
4541 .set_xlim(0.0, 10.0)
4542 .set_ylim(-1.0, 1.0)
4543 .set_xscale(Scale::Linear)
4544 .set_yscale(Scale::Log10)
4545 .grid(true)
4546 .legend();
4547
4548 assert_eq!(ax.title.as_deref(), Some("Test"));
4549 assert_eq!(ax.xlabel.as_deref(), Some("X"));
4550 assert_eq!(ax.ylabel.as_deref(), Some("Y"));
4551 assert_eq!(ax.xlim, Some((0.0, 10.0)));
4552 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
4553 assert_eq!(ax.show_grid, Some(true));
4554 assert!(ax.show_legend);
4555 }
4556
4557 #[test]
4558 fn color_cycle_advances() {
4559 let mut ax = Axes::new();
4560 for _ in 0..12 {
4561 ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4562 }
4563 assert_eq!(ax.color_index, 12);
4564 match (&ax.artists[0], &ax.artists[10]) {
4567 (Artist::Line(a), Artist::Line(b)) => {
4568 assert_eq!(a.color, b.color);
4569 }
4570 _ => panic!("expected Line artists"),
4571 }
4572 }
4573
4574 #[test]
4575 fn data_to_pixel_linear() {
4576 let ax = Axes::new();
4577 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4578
4579 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4581 assert!((p.x - 100.0).abs() < 1e-10);
4582 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);
4586 assert!((p.x - 500.0).abs() < 1e-10);
4587 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);
4591 assert!((p.x - 300.0).abs() < 1e-10);
4592 assert!((p.y - 200.0).abs() < 1e-10);
4593 }
4594
4595 #[test]
4596 fn compute_data_limits_no_artists() {
4597 let ax = Axes::new();
4598 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4599 assert!(xmin < xmax);
4601 assert!(ymin < ymax);
4602 }
4603
4604 #[test]
4605 fn compute_data_limits_with_user_override() {
4606 let mut ax = Axes::new();
4607 ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
4608 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4609 assert!((xmin - (-5.0)).abs() < f64::EPSILON);
4610 assert!((xmax - 5.0).abs() < f64::EPSILON);
4611 assert!((ymin - 0.0).abs() < f64::EPSILON);
4612 assert!((ymax - 100.0).abs() < f64::EPSILON);
4613 }
4614
4615 #[test]
4616 fn compute_data_limits_from_line_data() {
4617 let mut ax = Axes::new();
4618 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4619 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4620 assert!(xmin < 1.0);
4622 assert!(xmax > 10.0);
4623 assert!(ymin < 2.0);
4624 assert!(ymax > 8.0);
4625 }
4626 #[test]
4629 fn step_creates_artist() {
4630 let mut ax = Axes::new();
4631 let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4632 assert!(result.is_ok());
4633 assert!(matches!(&ax.artists[0], Artist::Step(_)));
4634 }
4635
4636 #[test]
4637 fn step_length_mismatch() {
4638 let mut ax = Axes::new();
4639 let result = ax.step(vec![1.0, 2.0], vec![1.0]);
4640 assert!(matches!(
4641 result,
4642 Err(PlotError::SeriesLengthMismatch { .. })
4643 ));
4644 }
4645
4646 #[test]
4647 fn step_empty_data() {
4648 let mut ax = Axes::new();
4649 let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
4650 assert!(matches!(result, Err(PlotError::EmptyData)));
4651 }
4652
4653 #[test]
4654 fn step_default_where() {
4655 let mut ax = Axes::new();
4656 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4657 match &ax.artists[0] {
4658 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
4659 _ => panic!("expected Step"),
4660 }
4661 }
4662
4663 #[test]
4664 fn step_color_cycle() {
4665 let mut ax = Axes::new();
4666 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4667 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4668 let c0 = ax.artists[0].color();
4669 let c1 = ax.artists[1].color();
4670 assert_ne!(c0, c1);
4671 }
4672
4673 #[test]
4674 fn step_builder_chaining() {
4675 let mut ax = Axes::new();
4676 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4677 .unwrap()
4678 .color(Color::TAB_RED)
4679 .width(3.0)
4680 .where_step(StepWhere::Post)
4681 .label("steps")
4682 .alpha(0.5);
4683 match &ax.artists[0] {
4684 Artist::Step(a) => {
4685 assert_eq!(a.color, Color::TAB_RED);
4686 assert!((a.width - 3.0).abs() < 1e-12);
4687 assert!(matches!(a.where_step, StepWhere::Post));
4688 assert_eq!(a.label.as_deref(), Some("steps"));
4689 assert!((a.alpha - 0.5).abs() < 1e-12);
4690 }
4691 _ => panic!("expected Step"),
4692 }
4693 }
4694
4695 #[test]
4696 fn step_data_bounds() {
4697 let mut ax = Axes::new();
4698 ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4699 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4700 assert!(xmin < 1.0);
4701 assert!(xmax > 10.0);
4702 assert!(ymin < 2.0);
4703 assert!(ymax > 8.0);
4704 }
4705
4706 #[test]
4707 fn step_legend_label() {
4708 let mut ax = Axes::new();
4709 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
4710 assert_eq!(ax.artists[0].label(), Some("S"));
4711 }
4712
4713 #[test]
4714 fn step_default_alpha() {
4715 let mut ax = Axes::new();
4716 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4717 match &ax.artists[0] {
4718 Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4719 _ => panic!("expected Step"),
4720 }
4721 }
4722
4723 #[test]
4724 fn step_default_width() {
4725 let mut ax = Axes::new();
4726 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4727 match &ax.artists[0] {
4728 Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
4729 _ => panic!("expected Step"),
4730 }
4731 }
4732
4733 #[test]
4734 fn step_mid_mode() {
4735 let mut ax = Axes::new();
4736 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4737 .unwrap()
4738 .where_step(StepWhere::Mid);
4739 match &ax.artists[0] {
4740 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
4741 _ => panic!("expected Step"),
4742 }
4743 }
4744
4745 #[test]
4748 fn stem_creates_artist() {
4749 let mut ax = Axes::new();
4750 let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4751 assert!(result.is_ok());
4752 assert!(matches!(&ax.artists[0], Artist::Stem(_)));
4753 }
4754
4755 #[test]
4756 fn stem_length_mismatch() {
4757 let mut ax = Axes::new();
4758 let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
4759 assert!(matches!(
4760 result,
4761 Err(PlotError::SeriesLengthMismatch { .. })
4762 ));
4763 }
4764
4765 #[test]
4766 fn stem_empty_data() {
4767 let mut ax = Axes::new();
4768 let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
4769 assert!(matches!(result, Err(PlotError::EmptyData)));
4770 }
4771
4772 #[test]
4773 fn stem_default_baseline() {
4774 let mut ax = Axes::new();
4775 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4776 match &ax.artists[0] {
4777 Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
4778 _ => panic!("expected Stem"),
4779 }
4780 }
4781
4782 #[test]
4783 fn stem_default_marker_size() {
4784 let mut ax = Axes::new();
4785 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4786 match &ax.artists[0] {
4787 Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
4788 _ => panic!("expected Stem"),
4789 }
4790 }
4791
4792 #[test]
4793 fn stem_builder_chaining() {
4794 let mut ax = Axes::new();
4795 ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4796 .unwrap()
4797 .color(Color::TAB_GREEN)
4798 .baseline(1.0)
4799 .marker_size(8.0)
4800 .width(2.0)
4801 .label("stems")
4802 .alpha(0.7);
4803 match &ax.artists[0] {
4804 Artist::Stem(a) => {
4805 assert_eq!(a.color, Color::TAB_GREEN);
4806 assert!((a.baseline - 1.0).abs() < 1e-12);
4807 assert!((a.marker_size - 8.0).abs() < 1e-12);
4808 assert!((a.line_width - 2.0).abs() < 1e-12);
4809 assert_eq!(a.label.as_deref(), Some("stems"));
4810 assert!((a.alpha - 0.7).abs() < 1e-12);
4811 }
4812 _ => panic!("expected Stem"),
4813 }
4814 }
4815
4816 #[test]
4817 fn stem_data_bounds_include_baseline() {
4818 let mut ax = Axes::new();
4819 ax.stem(vec![1.0, 5.0], vec![2.0, 8.0])
4820 .unwrap()
4821 .baseline(-5.0);
4822 let (_xmin, _xmax, ymin, _ymax) = ax.compute_data_limits();
4823 assert!(ymin < -5.0);
4824 }
4825
4826 #[test]
4827 fn stem_legend_label() {
4828 let mut ax = Axes::new();
4829 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
4830 assert_eq!(ax.artists[0].label(), Some("L"));
4831 }
4832
4833 #[test]
4834 fn stem_color_cycle() {
4835 let mut ax = Axes::new();
4836 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4837 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4838 let c0 = ax.artists[0].color();
4839 let c1 = ax.artists[1].color();
4840 assert_ne!(c0, c1);
4841 }
4842
4843 #[test]
4844 fn stem_alpha_default() {
4845 let mut ax = Axes::new();
4846 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4847 match &ax.artists[0] {
4848 Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4849 _ => panic!("expected Stem"),
4850 }
4851 }
4852
4853 #[test]
4854 fn stem_negative_baseline() {
4855 let mut ax = Axes::new();
4856 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0])
4857 .unwrap()
4858 .baseline(-3.0);
4859 match &ax.artists[0] {
4860 Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
4861 _ => panic!("expected Stem"),
4862 }
4863 }
4864
4865 #[test]
4868 fn text_creates_annotation() {
4869 let mut ax = Axes::new();
4870 ax.text(1.0, 2.0, "hello");
4871 assert_eq!(ax.texts.len(), 1);
4872 assert_eq!(ax.texts[0].text, "hello");
4873 assert!((ax.texts[0].x - 1.0).abs() < f64::EPSILON);
4874 assert!((ax.texts[0].y - 2.0).abs() < f64::EPSILON);
4875 }
4876
4877 #[test]
4878 fn text_default_alignment() {
4879 let mut ax = Axes::new();
4880 ax.text(0.0, 0.0, "test");
4881 assert_eq!(ax.texts[0].ha, HAlign::Left);
4882 assert_eq!(ax.texts[0].va, VAlign::Baseline);
4883 assert!((ax.texts[0].rotation - 0.0).abs() < f64::EPSILON);
4884 }
4885
4886 #[test]
4887 fn text_builder_chaining() {
4888 let mut ax = Axes::new();
4889 ax.text(1.0, 2.0, "styled")
4890 .fontsize(14.0)
4891 .color(Color::TAB_RED)
4892 .ha(HAlign::Center)
4893 .va(VAlign::Top)
4894 .rotation(45.0);
4895 let t = &ax.texts[0];
4896 assert_eq!(t.fontsize, Some(14.0));
4897 assert_eq!(t.color, Some(Color::TAB_RED));
4898 assert_eq!(t.ha, HAlign::Center);
4899 assert_eq!(t.va, VAlign::Top);
4900 assert!((t.rotation - 45.0).abs() < f64::EPSILON);
4901 }
4902
4903 #[test]
4904 fn text_does_not_affect_autoscale() {
4905 let mut ax = Axes::new();
4906 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4907 let limits_before = ax.compute_data_limits();
4908 ax.text(100.0, 100.0, "far away");
4910 let limits_after = ax.compute_data_limits();
4911 assert_eq!(limits_before, limits_after);
4912 }
4913
4914 #[test]
4915 fn multiple_texts() {
4916 let mut ax = Axes::new();
4917 ax.text(1.0, 1.0, "first");
4918 ax.text(2.0, 2.0, "second");
4919 ax.text(3.0, 3.0, "third");
4920 assert_eq!(ax.texts.len(), 3);
4921 assert_eq!(ax.texts[0].text, "first");
4922 assert_eq!(ax.texts[1].text, "second");
4923 assert_eq!(ax.texts[2].text, "third");
4924 }
4925
4926 #[test]
4929 fn annotate_creates_annotation() {
4930 let mut ax = Axes::new();
4931 ax.annotate("peak", (1.0, 2.0), (3.0, 4.0));
4932 assert_eq!(ax.annotations.len(), 1);
4933 assert_eq!(ax.annotations[0].text, "peak");
4934 assert_eq!(ax.annotations[0].xy, (1.0, 2.0));
4935 assert_eq!(ax.annotations[0].xytext, (3.0, 4.0));
4936 }
4937
4938 #[test]
4939 fn annotate_default_no_arrow() {
4940 let mut ax = Axes::new();
4941 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4942 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::None);
4943 }
4944
4945 #[test]
4946 fn annotate_default_alignment() {
4947 let mut ax = Axes::new();
4948 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4949 assert_eq!(ax.annotations[0].ha, HAlign::Center);
4950 assert_eq!(ax.annotations[0].va, VAlign::Bottom);
4951 }
4952
4953 #[test]
4954 fn annotate_with_arrow() {
4955 let mut ax = Axes::new();
4956 ax.annotate("peak", (1.0, 1.0), (2.0, 2.0))
4957 .arrowstyle(ArrowStyle::Simple);
4958 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Simple);
4959 }
4960
4961 #[test]
4962 fn annotate_with_fancy_arrow() {
4963 let mut ax = Axes::new();
4964 ax.annotate("label", (0.0, 0.0), (1.0, 1.0))
4965 .arrowstyle(ArrowStyle::Fancy);
4966 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Fancy);
4967 }
4968
4969 #[test]
4970 fn annotate_builder_chaining() {
4971 let mut ax = Axes::new();
4972 ax.annotate("note", (1.0, 2.0), (3.0, 4.0))
4973 .fontsize(12.0)
4974 .color(Color::TAB_BLUE)
4975 .ha(HAlign::Right)
4976 .va(VAlign::Top)
4977 .arrowstyle(ArrowStyle::Fancy)
4978 .arrow_color(Color::TAB_RED);
4979 let a = &ax.annotations[0];
4980 assert_eq!(a.fontsize, Some(12.0));
4981 assert_eq!(a.color, Some(Color::TAB_BLUE));
4982 assert_eq!(a.ha, HAlign::Right);
4983 assert_eq!(a.va, VAlign::Top);
4984 assert_eq!(a.arrowstyle, ArrowStyle::Fancy);
4985 assert_eq!(a.arrow_color, Some(Color::TAB_RED));
4986 }
4987
4988 #[test]
4989 fn annotate_does_not_affect_autoscale() {
4990 let mut ax = Axes::new();
4991 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4992 let limits_before = ax.compute_data_limits();
4993 ax.annotate("far", (100.0, 100.0), (200.0, 200.0));
4995 let limits_after = ax.compute_data_limits();
4996 assert_eq!(limits_before, limits_after);
4997 }
4998
4999 #[test]
5000 fn multiple_annotations() {
5001 let mut ax = Axes::new();
5002 ax.annotate("a", (0.0, 0.0), (1.0, 1.0));
5003 ax.annotate("b", (2.0, 2.0), (3.0, 3.0));
5004 assert_eq!(ax.annotations.len(), 2);
5005 }
5006
5007 #[test]
5008 fn text_at_plot_boundary() {
5009 let mut ax = Axes::new();
5010 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5011 ax.text(0.0, 0.0, "origin");
5013 ax.text(10.0, 10.0, "corner");
5014 assert_eq!(ax.texts.len(), 2);
5015 }
5016
5017 #[test]
5018 fn overlapping_annotations() {
5019 let mut ax = Axes::new();
5020 ax.annotate("one", (5.0, 5.0), (6.0, 6.0));
5022 ax.annotate("two", (5.0, 5.0), (6.0, 6.0));
5023 ax.text(6.0, 6.0, "three");
5024 assert_eq!(ax.annotations.len(), 2);
5025 assert_eq!(ax.texts.len(), 1);
5026 }
5027
5028 #[test]
5029 fn new_axes_has_empty_annotations() {
5030 let ax = Axes::new();
5031 assert!(ax.texts.is_empty());
5032 assert!(ax.annotations.is_empty());
5033 }
5034
5035 #[test]
5036 fn text_pixel_placement() {
5037 let ax = Axes::new();
5039 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
5040 let pt = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5042 assert!((pt.x - 300.0).abs() < 1e-10);
5043 assert!((pt.y - 200.0).abs() < 1e-10);
5044 }
5045
5046 #[test]
5047 fn annotation_pixel_placement() {
5048 let ax = Axes::new();
5050 let plot_area = Rect::new(0.0, 0.0, 100.0, 100.0);
5051 let target = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5052 let text_pos = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
5053 assert!((target.x - 0.0).abs() < 1e-10);
5055 assert!((target.y - 100.0).abs() < 1e-10);
5056 assert!((text_pos.x - 50.0).abs() < 1e-10);
5058 assert!((text_pos.y - 50.0).abs() < 1e-10);
5059 }
5060
5061 #[test]
5064 fn set_xlim_overrides_autoscale() {
5065 let mut ax = Axes::new();
5066 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5067 ax.set_xlim(2.0, 8.0);
5068 let (xmin, xmax, _, _) = ax.compute_data_limits();
5069 assert!((xmin - 2.0).abs() < f64::EPSILON);
5070 assert!((xmax - 8.0).abs() < f64::EPSILON);
5071 }
5072
5073 #[test]
5074 fn set_ylim_overrides_autoscale() {
5075 let mut ax = Axes::new();
5076 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5077 ax.set_ylim(-5.0, 15.0);
5078 let (_, _, ymin, ymax) = ax.compute_data_limits();
5079 assert!((ymin - (-5.0)).abs() < f64::EPSILON);
5080 assert!((ymax - 15.0).abs() < f64::EPSILON);
5081 }
5082
5083 #[test]
5084 fn set_xlim_with_min_greater_than_max() {
5085 let mut ax = Axes::new();
5086 ax.set_xlim(10.0, 2.0);
5087 let (xmin, xmax, _, _) = ax.compute_data_limits();
5088 assert!((xmin - 10.0).abs() < f64::EPSILON);
5090 assert!((xmax - 2.0).abs() < f64::EPSILON);
5091 }
5092
5093 #[test]
5094 fn invert_xaxis_swaps_limits() {
5095 let mut ax = Axes::new();
5096 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
5097 ax.set_xlim(0.0, 10.0);
5098 ax.invert_xaxis();
5099 let (xmin, xmax, _, _) = ax.compute_data_limits();
5100 assert!((xmin - 10.0).abs() < f64::EPSILON);
5102 assert!((xmax - 0.0).abs() < f64::EPSILON);
5103 }
5104
5105 #[test]
5106 fn invert_yaxis_swaps_limits() {
5107 let mut ax = Axes::new();
5108 ax.set_ylim(0.0, 100.0);
5109 ax.invert_yaxis();
5110 let (_, _, ymin, ymax) = ax.compute_data_limits();
5111 assert!((ymin - 100.0).abs() < f64::EPSILON);
5112 assert!((ymax - 0.0).abs() < f64::EPSILON);
5113 }
5114
5115 #[test]
5116 fn custom_xticks_appear_in_output() {
5117 let mut ax = Axes::new();
5118 ax.set_xticks(&[1.0, 2.0, 3.0]);
5119 let ticks = ax.resolve_xticks(0.0, 10.0);
5120 assert_eq!(ticks.len(), 3);
5121 assert!((ticks[0].value - 1.0).abs() < f64::EPSILON);
5122 assert!((ticks[1].value - 2.0).abs() < f64::EPSILON);
5123 assert!((ticks[2].value - 3.0).abs() < f64::EPSILON);
5124 }
5125
5126 #[test]
5127 fn custom_yticks_appear_in_output() {
5128 let mut ax = Axes::new();
5129 ax.set_yticks(&[0.0, 50.0, 100.0]);
5130 let ticks = ax.resolve_yticks(0.0, 100.0);
5131 assert_eq!(ticks.len(), 3);
5132 assert!((ticks[0].value - 0.0).abs() < f64::EPSILON);
5133 assert!((ticks[1].value - 50.0).abs() < f64::EPSILON);
5134 assert!((ticks[2].value - 100.0).abs() < f64::EPSILON);
5135 }
5136
5137 #[test]
5138 fn custom_tick_labels_override_default_format() {
5139 let mut ax = Axes::new();
5140 ax.set_xticks(&[0.0, 3.125, 6.25]);
5141 ax.set_xticklabels(&["0", "pi", "2pi"]);
5142 let ticks = ax.resolve_xticks(0.0, 7.0);
5143 assert_eq!(ticks.len(), 3);
5144 assert_eq!(ticks[0].label, "0");
5145 assert_eq!(ticks[1].label, "pi");
5146 assert_eq!(ticks[2].label, "2pi");
5147 }
5148
5149 #[test]
5150 fn custom_tick_labels_partial_match() {
5151 let mut ax = Axes::new();
5153 ax.set_xticks(&[1.0, 2.0, 3.0]);
5154 ax.set_xticklabels(&["one"]);
5155 let ticks = ax.resolve_xticks(0.0, 5.0);
5156 assert_eq!(ticks[0].label, "one");
5157 assert_eq!(ticks[1].label, "2");
5158 assert_eq!(ticks[2].label, "3");
5159 }
5160
5161 #[test]
5162 fn empty_custom_ticks() {
5163 let mut ax = Axes::new();
5164 ax.set_xticks(&[]);
5165 let ticks = ax.resolve_xticks(0.0, 10.0);
5166 assert!(ticks.is_empty());
5167 }
5168
5169 #[test]
5170 fn grid_visibility_toggle() {
5171 let mut ax = Axes::new();
5172 assert!(ax.show_grid.is_none());
5173 ax.grid(true);
5174 assert_eq!(ax.show_grid, Some(true));
5175 ax.grid(false);
5176 assert_eq!(ax.show_grid, Some(false));
5177 }
5178
5179 #[test]
5180 fn grid_axis_setting() {
5181 let mut ax = Axes::new();
5182 assert_eq!(ax.grid_axis, GridAxis::Both);
5183 ax.grid_axis("x");
5184 assert_eq!(ax.grid_axis, GridAxis::X);
5185 ax.grid_axis("y");
5186 assert_eq!(ax.grid_axis, GridAxis::Y);
5187 ax.grid_axis("both");
5188 assert_eq!(ax.grid_axis, GridAxis::Both);
5189 }
5190
5191 #[test]
5192 fn grid_alpha_setting() {
5193 let mut ax = Axes::new();
5194 ax.grid_alpha(0.5);
5195 assert!((ax.grid_alpha.unwrap() - 0.5).abs() < f64::EPSILON);
5196 }
5197
5198 #[test]
5199 fn grid_alpha_clamps() {
5200 let mut ax = Axes::new();
5201 ax.grid_alpha(2.0);
5202 assert!((ax.grid_alpha.unwrap() - 1.0).abs() < f64::EPSILON);
5203 ax.grid_alpha(-0.5);
5204 assert!((ax.grid_alpha.unwrap() - 0.0).abs() < f64::EPSILON);
5205 }
5206
5207 #[test]
5208 fn grid_style_setting() {
5209 let mut ax = Axes::new();
5210 ax.grid_style(crate::theme::LineStyle::Dashed);
5211 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dashed));
5212 }
5213
5214 #[test]
5215 fn tick_rotation_setting() {
5216 let mut ax = Axes::new();
5217 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
5218 ax.tick_params_x_rotation(45.0);
5219 assert!((ax.xtick_rotation - 45.0).abs() < f64::EPSILON);
5220 ax.tick_params_y_rotation(-30.0);
5221 assert!((ax.ytick_rotation - (-30.0)).abs() < f64::EPSILON);
5222 }
5223
5224 #[test]
5225 fn axis_control_chaining() {
5226 let mut ax = Axes::new();
5227 ax.set_xlim(0.0, 10.0)
5228 .set_ylim(-1.0, 1.0)
5229 .invert_xaxis()
5230 .grid(true)
5231 .grid_axis("y")
5232 .grid_alpha(0.3)
5233 .grid_style(crate::theme::LineStyle::Dotted)
5234 .set_xticks(&[0.0, 5.0, 10.0])
5235 .set_xticklabels(&["start", "mid", "end"])
5236 .tick_params_x_rotation(90.0);
5237
5238 assert_eq!(ax.xlim, Some((0.0, 10.0)));
5239 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
5240 assert!(ax.x_inverted);
5241 assert_eq!(ax.show_grid, Some(true));
5242 assert_eq!(ax.grid_axis, GridAxis::Y);
5243 assert!((ax.grid_alpha.unwrap() - 0.3).abs() < f64::EPSILON);
5244 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dotted));
5245 assert_eq!(ax.custom_xticks.as_ref().unwrap().len(), 3);
5246 assert_eq!(ax.custom_xticklabels.as_ref().unwrap().len(), 3);
5247 assert!((ax.xtick_rotation - 90.0).abs() < f64::EPSILON);
5248 }
5249
5250 #[test]
5251 fn new_axes_has_axis_control_defaults() {
5252 let ax = Axes::new();
5253 assert_eq!(ax.grid_axis, GridAxis::Both);
5254 assert!(ax.grid_alpha.is_none());
5255 assert!(ax.grid_style.is_none());
5256 assert!(!ax.x_inverted);
5257 assert!(!ax.y_inverted);
5258 assert!(ax.custom_xticks.is_none());
5259 assert!(ax.custom_yticks.is_none());
5260 assert!(ax.custom_xticklabels.is_none());
5261 assert!(ax.custom_yticklabels.is_none());
5262 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
5263 assert!((ax.ytick_rotation - 0.0).abs() < f64::EPSILON);
5264 }
5265
5266 #[test]
5267 fn autoscale_not_broken_without_user_limits() {
5268 let mut ax = Axes::new();
5269 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
5270 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5271 assert!(xmin < 1.0);
5273 assert!(xmax > 10.0);
5274 assert!(ymin < 2.0);
5275 assert!(ymax > 8.0);
5276 }
5277
5278 #[test]
5279 fn resolve_xticks_auto_when_not_set() {
5280 let ax = Axes::new();
5281 let ticks = ax.resolve_xticks(0.0, 10.0);
5282 assert!(!ticks.is_empty());
5284 }
5285
5286 #[test]
5287 fn resolve_yticks_auto_when_not_set() {
5288 let ax = Axes::new();
5289 let ticks = ax.resolve_yticks(0.0, 100.0);
5290 assert!(!ticks.is_empty());
5291 }
5292
5293 #[test]
5296 fn data_to_pixel_log10() {
5297 let mut ax = Axes::new();
5298 ax.set_xscale(Scale::Log10);
5299 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
5300
5301 let p = ax.data_to_pixel(1.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5303 assert!(
5304 (p.x - 100.0).abs() < 1e-6,
5305 "log10(1)=0 should be left edge, got {}",
5306 p.x
5307 );
5308
5309 let p = ax.data_to_pixel(1000.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5311 assert!(
5312 (p.x - 500.0).abs() < 1e-6,
5313 "log10(1000)=3 should be right edge, got {}",
5314 p.x
5315 );
5316
5317 let p = ax.data_to_pixel(10.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5319 let expected_x = 100.0 + 400.0 / 3.0;
5320 assert!(
5321 (p.x - expected_x).abs() < 1e-6,
5322 "log10(10)=1/3 of range, expected {}, got {}",
5323 expected_x,
5324 p.x
5325 );
5326
5327 let p = ax.data_to_pixel(100.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5329 let expected_x = 100.0 + 400.0 * 2.0 / 3.0;
5330 assert!(
5331 (p.x - expected_x).abs() < 1e-6,
5332 "log10(100)=2/3 of range, expected {}, got {}",
5333 expected_x,
5334 p.x
5335 );
5336 }
5337
5338 #[test]
5339 fn data_to_pixel_log10_y() {
5340 let mut ax = Axes::new();
5341 ax.set_yscale(Scale::Log10);
5342 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5343
5344 let p = ax.data_to_pixel(0.0, 10.0, &plot_area, 0.0, 10.0, 1.0, 100.0);
5346 assert!(
5349 (p.y - 150.0).abs() < 1e-6,
5350 "log10(10) should be vertical center, got {}",
5351 p.y
5352 );
5353 }
5354
5355 #[test]
5356 fn compute_data_limits_log_clamps_positive() {
5357 let mut ax = Axes::new();
5358 ax.set_xscale(Scale::Log10);
5359 ax.set_yscale(Scale::Log10);
5360 ax.plot(vec![0.1, 1.0, 10.0], vec![1.0, 10.0, 100.0])
5361 .unwrap();
5362 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5363 assert!(xmin > 0.0, "log x-min must be positive, got {}", xmin);
5364 assert!(xmax > xmin, "log x-max must be > x-min");
5365 assert!(ymin > 0.0, "log y-min must be positive, got {}", ymin);
5366 assert!(ymax > ymin, "log y-max must be > y-min");
5367 }
5368
5369 #[test]
5370 fn compute_data_limits_log_with_zeros() {
5371 let mut ax = Axes::new();
5373 ax.set_xscale(Scale::Log10);
5374 ax.plot(vec![0.0, 1.0, 10.0], vec![1.0, 2.0, 3.0]).unwrap();
5375 let (xmin, xmax, _ymin, _ymax) = ax.compute_data_limits();
5376 assert!(
5377 xmin > 0.0,
5378 "log x-min should be clamped positive, got {}",
5379 xmin
5380 );
5381 assert!(xmax > xmin);
5382 }
5383
5384 #[test]
5385 fn data_to_pixel_symlog() {
5386 let mut ax = Axes::new();
5387 ax.set_xscale(Scale::SymLog { linthresh: 1.0 });
5388 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5389
5390 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, -100.0, 100.0, 0.0, 1.0);
5392 assert!(
5393 (p.x - 200.0).abs() < 1e-6,
5394 "symlog(0) should be center for symmetric range, got {}",
5395 p.x
5396 );
5397 }
5398
5399 #[test]
5400 fn set_scale_methods_return_self() {
5401 let mut ax = Axes::new();
5402 ax.set_xscale(Scale::Log10)
5403 .set_yscale(Scale::Log10)
5404 .set_title("Log plot");
5405 assert!(matches!(ax.xscale, Scale::Log10));
5406 assert!(matches!(ax.yscale, Scale::Log10));
5407 assert_eq!(ax.title.as_deref(), Some("Log plot"));
5408 }
5409
5410 #[test]
5411 fn compute_data_limits_log_no_artists() {
5412 let mut ax = Axes::new();
5413 ax.set_xscale(Scale::Log10);
5414 ax.set_yscale(Scale::Log10);
5415 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5416 assert!(xmin > 0.0, "default log x-min must be positive");
5417 assert!(xmax > xmin);
5418 assert!(ymin > 0.0, "default log y-min must be positive");
5419 assert!(ymax > ymin);
5420 }
5421
5422 #[test]
5423 fn data_to_pixel_log10_very_large_range() {
5424 let mut ax = Axes::new();
5425 ax.set_xscale(Scale::Log10);
5426 let plot_area = Rect::new(0.0, 0.0, 1000.0, 100.0);
5427
5428 let p_lo = ax.data_to_pixel(1e-5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5430 let p_hi = ax.data_to_pixel(1e5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5431 assert!((p_lo.x - 0.0).abs() < 1e-6);
5432 assert!((p_hi.x - 1000.0).abs() < 1e-6);
5433 }
5434
5435 #[test]
5440 fn new_axes_has_no_twin_fields() {
5441 let ax = Axes::new();
5442 assert!(!ax.is_twin());
5443 assert_eq!(ax.twin_side(), None);
5444 }
5445
5446 #[test]
5451 fn colorbar_attaches_to_axes() {
5452 let mut ax = Axes::new();
5453 let cb = ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5454 cb.set_label("Test");
5455 assert!(ax.colorbar.is_some());
5456 assert_eq!(ax.colorbar.as_ref().unwrap().label.as_deref(), Some("Test"));
5457 }
5458
5459 #[test]
5460 fn colorbar_replaces_previous() {
5461 let mut ax = Axes::new();
5462 ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5463 ax.colorbar(crate::colormap::Colormap::Plasma, -10.0, 10.0);
5464 let cb = ax.colorbar.as_ref().unwrap();
5465 assert_eq!(cb.cmap, crate::colormap::Colormap::Plasma);
5466 assert!((cb.vmin - (-10.0)).abs() < f64::EPSILON);
5467 assert!((cb.vmax - 10.0).abs() < f64::EPSILON);
5468 }
5469
5470 #[test]
5471 fn heatmap_auto_colorbar() {
5472 let mut ax = Axes::new();
5473 ax.heatmap(vec![vec![1.0, 2.0], vec![3.0, 4.0]])
5474 .unwrap()
5475 .colorbar(true);
5476 let auto_cb = ax.auto_colorbar_from_artists();
5477 assert!(auto_cb.is_some());
5478 let cb = auto_cb.unwrap();
5479 assert!((cb.vmin - 1.0).abs() < f64::EPSILON);
5480 assert!((cb.vmax - 4.0).abs() < f64::EPSILON);
5481 }
5482
5483 #[test]
5484 fn heatmap_no_auto_colorbar_by_default() {
5485 let mut ax = Axes::new();
5486 ax.heatmap(vec![vec![1.0, 2.0]]).unwrap();
5487 let auto_cb = ax.auto_colorbar_from_artists();
5488 assert!(auto_cb.is_none());
5489 }
5490
5491 #[test]
5492 fn new_axes_has_no_colorbar() {
5493 let ax = Axes::new();
5494 assert!(ax.colorbar.is_none());
5495 }
5496}