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: None,
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 };
271 self.artists.push(Artist::Scatter(artist));
272 match self.artists.last_mut().expect("just pushed") {
273 Artist::Scatter(a) => Ok(a),
274 _ => unreachable!(),
275 }
276 }
277
278 pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
288 where
289 C: IntoCategories,
290 H: IntoSeries,
291 {
292 let cats = categories.into_categories();
293 let vals = heights.into_series();
294 if cats.len() != vals.len() {
295 return Err(PlotError::SeriesLengthMismatch {
296 expected: cats.len(),
297 got: vals.len(),
298 });
299 }
300 if cats.is_empty() {
301 return Err(PlotError::EmptyData);
302 }
303 let color = Color::TABLEAU_10[self.color_index % 10];
304 self.color_index += 1;
305 let artist = BarArtist {
306 categories: cats,
307 heights: vals,
308 color,
309 horizontal: false,
310 bar_width: 0.8,
311 label: None,
312 alpha: 1.0,
313 bottom: None,
314 offset: None,
315 };
316 self.artists.push(Artist::Bar(artist));
317 match self.artists.last_mut().expect("just pushed") {
318 Artist::Bar(a) => Ok(a),
319 _ => unreachable!(),
320 }
321 }
322
323 pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
332 where
333 C: IntoCategories,
334 W: IntoSeries,
335 {
336 let cats = categories.into_categories();
337 let vals = widths.into_series();
338 if cats.len() != vals.len() {
339 return Err(PlotError::SeriesLengthMismatch {
340 expected: cats.len(),
341 got: vals.len(),
342 });
343 }
344 if cats.is_empty() {
345 return Err(PlotError::EmptyData);
346 }
347 let color = Color::TABLEAU_10[self.color_index % 10];
348 self.color_index += 1;
349 let artist = BarArtist {
350 categories: cats,
351 heights: vals,
352 color,
353 horizontal: true,
354 bar_width: 0.8,
355 label: None,
356 alpha: 1.0,
357 bottom: None,
358 offset: None,
359 };
360 self.artists.push(Artist::Bar(artist));
361 match self.artists.last_mut().expect("just pushed") {
362 Artist::Bar(a) => Ok(a),
363 _ => unreachable!(),
364 }
365 }
366
367 pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
377 where
378 D: IntoSeries,
379 {
380 let series = data.into_series();
381 if series.is_empty() {
382 return Err(PlotError::EmptyData);
383 }
384 let bins = bins.max(1);
385
386 let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
388
389 let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
391 (data_min - 0.5, data_max + 0.5)
392 } else {
393 (data_min, data_max)
394 };
395
396 let bin_width = (hi - lo) / bins as f64;
397
398 let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
400 *edges.last_mut().expect("edges is non-empty") = hi;
402
403 let mut counts = vec![0.0f64; bins];
405 for &v in &series.data {
406 if !v.is_finite() {
407 continue;
408 }
409 let idx = if v >= hi {
411 bins - 1
413 } else {
414 let raw = ((v - lo) / bin_width) as usize;
415 raw.min(bins - 1)
416 };
417 counts[idx] += 1.0;
418 }
419
420 let color = Color::TABLEAU_10[self.color_index % 10];
421 self.color_index += 1;
422 let artist = HistArtist {
423 data: series,
424 bins,
425 bin_edges: edges,
426 counts,
427 color,
428 label: None,
429 alpha: 0.85,
430 density: false,
431 };
432
433
434
435
436
437
438
439 self.artists.push(Artist::Histogram(artist));
440 match self.artists.last_mut().expect("just pushed") {
441 Artist::Histogram(a) => Ok(a),
442 _ => unreachable!(),
443 }
444 }
445
446 pub fn fill_between<X, Y1, Y2>(
455 &mut self,
456 x: X,
457 y1: Y1,
458 y2: Y2,
459 ) -> Result<&mut FillBetweenArtist>
460 where
461 X: IntoSeries,
462 Y1: IntoSeries,
463 Y2: IntoSeries,
464 {
465 let xs = x.into_series();
466 let y1s = y1.into_series();
467 let y2s = y2.into_series();
468 if xs.len() != y1s.len() {
469 return Err(PlotError::SeriesLengthMismatch {
470 expected: xs.len(),
471 got: y1s.len(),
472 });
473 }
474 if xs.len() != y2s.len() {
475 return Err(PlotError::SeriesLengthMismatch {
476 expected: xs.len(),
477 got: y2s.len(),
478 });
479 }
480 if xs.is_empty() {
481 return Err(PlotError::EmptyData);
482 }
483 let color = Color::TABLEAU_10[self.color_index % 10];
484 self.color_index += 1;
485 let artist = FillBetweenArtist {
486 x: xs,
487 y1: y1s,
488 y2: y2s,
489 color,
490 label: None,
491 alpha: 0.3,
492 };
493 self.artists.push(Artist::FillBetween(artist));
494 match self.artists.last_mut().expect("just pushed") {
495 Artist::FillBetween(a) => Ok(a),
496 _ => unreachable!(),
497 }
498 }
499
500 pub fn step<X: IntoSeries, Y: IntoSeries>(
509 &mut self,
510 x: X,
511 y: Y,
512 ) -> Result<&mut StepArtist> {
513 let xs = x.into_series();
514 let ys = y.into_series();
515 if xs.len() != ys.len() {
516 return Err(PlotError::SeriesLengthMismatch {
517 expected: xs.len(),
518 got: ys.len(),
519 });
520 }
521 if xs.is_empty() {
522 return Err(PlotError::EmptyData);
523 }
524 let color = Color::TABLEAU_10[self.color_index % 10];
525 self.color_index += 1;
526 let artist = StepArtist {
527 x: xs,
528 y: ys,
529 color,
530 width: 1.5,
531 where_step: StepWhere::Pre,
532 label: None,
533 alpha: 1.0,
534 };
535 self.artists.push(Artist::Step(artist));
536 match self.artists.last_mut().expect("just pushed") {
537 Artist::Step(a) => Ok(a),
538 _ => unreachable!(),
539 }
540 }
541
542 pub fn stem<X: IntoSeries, Y: IntoSeries>(
551 &mut self,
552 x: X,
553 y: Y,
554 ) -> Result<&mut StemArtist> {
555 let xs = x.into_series();
556 let ys = y.into_series();
557 if xs.len() != ys.len() {
558 return Err(PlotError::SeriesLengthMismatch {
559 expected: xs.len(),
560 got: ys.len(),
561 });
562 }
563 if xs.is_empty() {
564 return Err(PlotError::EmptyData);
565 }
566 let color = Color::TABLEAU_10[self.color_index % 10];
567 self.color_index += 1;
568 let artist = StemArtist {
569 x: xs,
570 y: ys,
571 color,
572 line_width: 1.5,
573 marker_size: 6.0,
574 baseline: 0.0,
575 label: None,
576 alpha: 1.0,
577 };
578 self.artists.push(Artist::Stem(artist));
579 match self.artists.last_mut().expect("just pushed") {
580 Artist::Stem(a) => Ok(a),
581 _ => unreachable!(),
582 }
583 }
584
585 pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
594 use crate::charts::boxplot::compute_stats;
595 if datasets.is_empty() {
596 return Err(PlotError::EmptyData);
597 }
598 let color = Color::TABLEAU_10[self.color_index % 10];
599 self.color_index += 1;
600 let factor = 1.5;
601 let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
602 let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
603 let artist = BoxPlotArtist {
604 stats,
605 labels,
606 color,
607 label: None,
608 alpha: 1.0,
609 box_width: 0.5,
610 show_outliers: true,
611 whisker_iq_factor: factor,
612 raw_data: datasets,
613 };
614 self.artists.push(Artist::BoxPlot(artist));
615 match self.artists.last_mut().expect("just pushed") {
616 Artist::BoxPlot(a) => Ok(a),
617 _ => unreachable!(),
618 }
619 }
620
621 pub fn errorbar<X: IntoSeries, Y: IntoSeries>(
632 &mut self,
633 x: X,
634 y: Y,
635 ) -> Result<ErrorBarArtist> {
636 let xs = x.into_series();
637 let ys = y.into_series();
638 if xs.len() != ys.len() {
639 return Err(PlotError::SeriesLengthMismatch {
640 expected: xs.len(),
641 got: ys.len(),
642 });
643 }
644 if xs.is_empty() {
645 return Err(PlotError::EmptyData);
646 }
647 let color = Color::TABLEAU_10[self.color_index % 10];
648 self.color_index += 1;
649 Ok(ErrorBarArtist {
650 x: xs,
651 y: ys,
652 xerr: None,
653 yerr: None,
654 color,
655 label: None,
656 cap_size: 4.0,
657 line_width: 1.0,
658 })
659 }
660
661 pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
666 self.artists.push(Artist::ErrorBar(artist));
667 }
668
669 pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
678 if data.is_empty() {
679 return Err(PlotError::EmptyData);
680 }
681 let color = Color::TABLEAU_10[self.color_index % 10];
682 self.color_index += 1;
683 let artist = HeatmapArtist {
684 data,
685 x_labels: None,
686 y_labels: None,
687 cmap: crate::colormap::Colormap::Viridis,
688 vmin: None,
689 vmax: None,
690 show_values: false,
691 color,
692 label: None,
693 show_colorbar: false,
694 };
695 self.artists.push(Artist::Heatmap(artist));
696 match self.artists.last_mut().expect("just pushed") {
697 Artist::Heatmap(a) => Ok(a),
698 _ => unreachable!(),
699 }
700 }
701
702 pub fn pie<S: IntoSeries>(&mut self, sizes: S) -> Result<&mut PieArtist> {
713 let series = sizes.into_series();
714 if series.is_empty() {
715 return Err(PlotError::EmptyData);
716 }
717 let color = Color::TABLEAU_10[self.color_index % 10];
718 self.color_index += 1;
719 let artist = PieArtist {
720 sizes: series.data,
721 labels: None,
722 colors: None,
723 explode: None,
724 autopct: false,
725 start_angle: 90.0,
726 radius: 1.0,
727 label: None,
728 color,
729 };
730 self.artists.push(Artist::Pie(artist));
731 match self.artists.last_mut().expect("just pushed") {
732 Artist::Pie(a) => Ok(a),
733 _ => unreachable!(),
734 }
735 }
736}
737
738impl Axes {
743 pub fn set_title(&mut self, title: &str) -> &mut Self {
745 self.title = Some(title.to_string());
746 self
747 }
748
749 pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
751 self.xlabel = Some(label.to_string());
752 self
753 }
754
755 pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
757 self.ylabel = Some(label.to_string());
758 self
759 }
760
761 pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
763 self.xlim = Some((min, max));
764 self
765 }
766
767 pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
769 self.ylim = Some((min, max));
770 self
771 }
772
773 pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
775 self.xscale = scale;
776 self
777 }
778
779 pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
781 self.yscale = scale;
782 self
783 }
784
785 pub fn grid(&mut self, show: bool) -> &mut Self {
787 self.show_grid = Some(show);
788 self
789 }
790
791 pub fn legend(&mut self) -> &mut Self {
793 self.show_legend = true;
794 self
795 }
796
797 pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
799 self.legend_loc = loc;
800 self
801 }
802
803 pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
805 self.theme_override = Some(theme);
806 self
807 }
808
809 pub fn grid_axis(&mut self, axis: &str) -> &mut Self {
814 self.grid_axis = match axis {
815 "x" => GridAxis::X,
816 "y" => GridAxis::Y,
817 _ => GridAxis::Both,
818 };
819 self
820 }
821
822 pub fn grid_alpha(&mut self, alpha: f64) -> &mut Self {
824 self.grid_alpha = Some(alpha.clamp(0.0, 1.0));
825 self
826 }
827
828 pub fn grid_style(&mut self, style: LineStyle) -> &mut Self {
830 self.grid_style = Some(style);
831 self
832 }
833
834 pub fn invert_xaxis(&mut self) -> &mut Self {
836 self.x_inverted = true;
837 self
838 }
839
840 pub fn invert_yaxis(&mut self) -> &mut Self {
842 self.y_inverted = true;
843 self
844 }
845
846 pub fn set_xticks(&mut self, ticks: &[f64]) -> &mut Self {
850 self.custom_xticks = Some(ticks.to_vec());
851 self
852 }
853
854 pub fn set_yticks(&mut self, ticks: &[f64]) -> &mut Self {
858 self.custom_yticks = Some(ticks.to_vec());
859 self
860 }
861
862 pub fn set_xticklabels(&mut self, labels: &[&str]) -> &mut Self {
868 self.custom_xticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
869 self
870 }
871
872 pub fn set_yticklabels(&mut self, labels: &[&str]) -> &mut Self {
878 self.custom_yticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
879 self
880 }
881
882 pub fn tick_params_x_rotation(&mut self, degrees: f64) -> &mut Self {
884 self.xtick_rotation = degrees;
885 self
886 }
887
888 pub fn tick_params_y_rotation(&mut self, degrees: f64) -> &mut Self {
890 self.ytick_rotation = degrees;
891 self
892 }
893
894 pub fn text(&mut self, x: f64, y: f64, text: &str) -> &mut TextAnnotation {
902 self.texts.push(TextAnnotation {
903 text: text.to_string(),
904 x,
905 y,
906 fontsize: None,
907 color: None,
908 ha: HAlign::Left,
909 va: VAlign::Baseline,
910 rotation: 0.0,
911 });
912 self.texts.last_mut().expect("just pushed")
913 }
914
915 pub fn annotate(&mut self, text: &str, xy: (f64, f64), xytext: (f64, f64)) -> &mut Annotation {
924 self.annotations.push(Annotation {
925 text: text.to_string(),
926 xy,
927 xytext,
928 fontsize: None,
929 color: None,
930 ha: HAlign::Center,
931 va: VAlign::Bottom,
932 arrowstyle: ArrowStyle::None,
933 arrow_color: None,
934 });
935 self.annotations.last_mut().expect("just pushed")
936 }
937}
938
939impl Axes {
944 pub fn colorbar(&mut self, cmap: crate::colormap::Colormap, vmin: f64, vmax: f64) -> &mut Colorbar {
946 self.colorbar = Some(Colorbar::new(cmap, vmin, vmax));
947 self.colorbar.as_mut().expect("just set")
948 }
949}
950
951impl Axes {
956 pub fn contour(
958 &mut self,
959 x: &[f64],
960 y: &[f64],
961 z: Vec<Vec<f64>>,
962 ) -> Result<&mut ContourArtist> {
963 if x.is_empty() || y.is_empty() || z.is_empty() {
964 return Err(PlotError::EmptyData);
965 }
966 let color = Color::TABLEAU_10[self.color_index % 10];
967 self.color_index += 1;
968 let artist = ContourArtist {
969 x: x.to_vec(),
970 y: y.to_vec(),
971 z,
972 levels: None,
973 filled: false,
974 cmap: crate::colormap::Colormap::Viridis,
975 colors: None,
976 linewidths: 1.0,
977 label: None,
978 color,
979 num_levels: 10,
980 };
981 self.artists.push(Artist::Contour(artist));
982 match self.artists.last_mut().expect("just pushed") {
983 Artist::Contour(a) => Ok(a),
984 _ => unreachable!(),
985 }
986 }
987
988 pub fn contourf(
990 &mut self,
991 x: &[f64],
992 y: &[f64],
993 z: Vec<Vec<f64>>,
994 ) -> Result<&mut ContourArtist> {
995 if x.is_empty() || y.is_empty() || z.is_empty() {
996 return Err(PlotError::EmptyData);
997 }
998 let color = Color::TABLEAU_10[self.color_index % 10];
999 self.color_index += 1;
1000 let artist = ContourArtist {
1001 x: x.to_vec(),
1002 y: y.to_vec(),
1003 z,
1004 levels: None,
1005 filled: true,
1006 cmap: crate::colormap::Colormap::Viridis,
1007 colors: None,
1008 linewidths: 1.0,
1009 label: None,
1010 color,
1011 num_levels: 10,
1012 };
1013 self.artists.push(Artist::Contour(artist));
1014 match self.artists.last_mut().expect("just pushed") {
1015 Artist::Contour(a) => Ok(a),
1016 _ => unreachable!(),
1017 }
1018 }
1019
1020 pub fn violin(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut ViolinArtist> {
1022 if datasets.is_empty() {
1023 return Err(PlotError::EmptyData);
1024 }
1025 let color = Color::TABLEAU_10[self.color_index % 10];
1026 self.color_index += 1;
1027 let artist = ViolinArtist {
1028 datasets,
1029 positions: None,
1030 widths: 0.7,
1031 show_median: true,
1032 show_quartiles: true,
1033 color,
1034 alpha: 0.7,
1035 label: None,
1036 bw_method: 0.0,
1037 };
1038 self.artists.push(Artist::Violin(artist));
1039 match self.artists.last_mut().expect("just pushed") {
1040 Artist::Violin(a) => Ok(a),
1041 _ => unreachable!(),
1042 }
1043 }
1044
1045 pub fn bar_group<C: IntoCategories>(
1050 &mut self,
1051 categories: C,
1052 series: &[(&str, Vec<f64>)],
1053 ) -> Result<()> {
1054 let cat_labels: Vec<String> = categories.into_categories().labels.iter().map(|s| s.to_string()).collect();
1055 let n_series = series.len();
1056 if n_series == 0 {
1057 return Err(PlotError::EmptyData);
1058 }
1059 let total_width = 0.8;
1060 let bar_width = total_width / n_series as f64;
1061
1062 for (si, (label, heights)) in series.iter().enumerate() {
1063 let offset_val = (si as f64 - (n_series as f64 - 1.0) / 2.0) * bar_width;
1064 let offsets: Vec<f64> = vec![offset_val; heights.len()];
1065 let artist_ref = self.bar(cat_labels.clone(), heights.as_slice())?;
1066 artist_ref.bar_width(bar_width);
1067 artist_ref.offset(offsets);
1068 artist_ref.label(label);
1069 }
1070 Ok(())
1071 }
1072
1073 pub fn hexbin<X, Y>(&mut self, x: X, y: Y) -> Result<&mut HexbinArtist>
1083 where
1084 X: IntoSeries,
1085 Y: IntoSeries,
1086 {
1087 let xs = x.into_series();
1088 let ys = y.into_series();
1089 if xs.is_empty() || ys.is_empty() {
1090 return Err(PlotError::EmptyData);
1091 }
1092 if xs.len() != ys.len() {
1093 return Err(PlotError::SeriesLengthMismatch {
1094 expected: xs.len(),
1095 got: ys.len(),
1096 });
1097 }
1098 let color = Color::TABLEAU_10[self.color_index % 10];
1099 self.color_index += 1;
1100 let artist = HexbinArtist {
1101 x: xs.data,
1102 y: ys.data,
1103 gridsize: 20,
1104 cmap: crate::colormap::Colormap::Viridis,
1105 mincnt: 1,
1106 alpha: 1.0,
1107 color,
1108 label: None,
1109 edgecolor: None,
1110 show_colorbar: false,
1111 };
1112 self.artists.push(Artist::Hexbin(artist));
1113 match self.artists.last_mut().expect("just pushed") {
1114 Artist::Hexbin(a) => Ok(a),
1115 _ => unreachable!(),
1116 }
1117 }
1118
1119 pub fn polar_plot<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1132 where
1133 T: IntoSeries,
1134 R: IntoSeries,
1135 {
1136 let ts = theta.into_series();
1137 let rs = r.into_series();
1138 if ts.len() != rs.len() {
1139 return Err(PlotError::SeriesLengthMismatch {
1140 expected: ts.len(),
1141 got: rs.len(),
1142 });
1143 }
1144 if ts.is_empty() {
1145 return Err(PlotError::EmptyData);
1146 }
1147 let color = Color::TABLEAU_10[self.color_index % 10];
1148 self.color_index += 1;
1149 let artist = PolarArtist {
1150 theta: ts.data,
1151 r: rs.data,
1152 color,
1153 label: None,
1154 alpha: 1.0,
1155 linewidth: 1.5,
1156 filled: false,
1157 marker: None,
1158 };
1159 self.artists.push(Artist::Polar(artist));
1160 match self.artists.last_mut().expect("just pushed") {
1161 Artist::Polar(a) => Ok(a),
1162 _ => unreachable!(),
1163 }
1164 }
1165
1166 pub fn polar_fill<T, R>(&mut self, theta: T, r: R) -> Result<&mut PolarArtist>
1176 where
1177 T: IntoSeries,
1178 R: IntoSeries,
1179 {
1180 let artist_ref = self.polar_plot(theta, r)?;
1181 artist_ref.filled(true);
1182 artist_ref.alpha(0.3);
1183 Ok(artist_ref)
1184 }
1185
1186 pub fn waterfall<C, V>(&mut self, categories: C, values: V) -> Result<&mut WaterfallArtist>
1200 where
1201 C: IntoCategories,
1202 V: IntoSeries,
1203 {
1204 let cats = categories.into_categories();
1205 let vals = values.into_series();
1206 if cats.len() != vals.len() {
1207 return Err(PlotError::SeriesLengthMismatch {
1208 expected: cats.len(),
1209 got: vals.len(),
1210 });
1211 }
1212 if cats.is_empty() {
1213 return Err(PlotError::EmptyData);
1214 }
1215
1216 let n = cats.len();
1218 let tick_positions: Vec<f64> = (0..n).map(|i| i as f64).collect();
1219 self.custom_xticks = Some(tick_positions);
1220 self.custom_xticklabels = Some(cats.labels.iter().map(|s| s.to_string()).collect());
1221
1222 let color = Color::TABLEAU_10[self.color_index % 10];
1223 self.color_index += 1;
1224 let artist = WaterfallArtist {
1225 categories: cats,
1226 values: vals,
1227 total_indices: Vec::new(),
1228 increase_color: Color::rgb(0x2C, 0xA0, 0x2C), decrease_color: Color::rgb(0xD6, 0x27, 0x28), total_color: Color::rgb(0x4E, 0x79, 0xA7), connector_lines: true,
1232 show_values: false,
1233 bar_width: 0.6,
1234 label: None,
1235 color,
1236 alpha: 1.0,
1237 };
1238 self.artists.push(Artist::Waterfall(artist));
1239 match self.artists.last_mut().expect("just pushed") {
1240 Artist::Waterfall(a) => Ok(a),
1241 _ => unreachable!(),
1242 }
1243 }
1244}
1245
1246#[allow(clippy::too_many_arguments)]
1251impl Axes {
1252 pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
1268 self.render_primary(renderer, bounds, fig_theme, false);
1269 }
1270
1271 pub(crate) fn render_primary(
1272 &self,
1273 renderer: &mut impl Renderer,
1274 bounds: Rect,
1275 fig_theme: &Theme,
1276 suppress_legend: bool,
1277 ) {
1278 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1279
1280 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1282
1283 let xticks = self.resolve_xticks(xmin, xmax);
1285 let yticks = self.resolve_yticks(ymin, ymax);
1286
1287 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1289 layout_config.has_title = self.title.is_some();
1290 layout_config.has_xlabel = self.xlabel.is_some();
1291 layout_config.has_ylabel = self.ylabel.is_some();
1292 layout_config.has_legend = self.show_legend;
1293
1294 let layout_result = layout::compute_layout(&layout_config);
1295
1296 let full_plot_area = Rect::new(
1297 bounds.x + layout_result.plot_area.x,
1298 bounds.y + layout_result.plot_area.y,
1299 layout_result.plot_area.width,
1300 layout_result.plot_area.height,
1301 );
1302
1303 let effective_colorbar = if self.colorbar.is_some() {
1305 self.colorbar.clone()
1306 } else {
1307 self.auto_colorbar_from_artists()
1308 };
1309
1310 let (plot_area, colorbar_rect) = if effective_colorbar.is_some() {
1312 let cb_width = (full_plot_area.width * colorbar::COLORBAR_WIDTH_FRACTION).max(30.0);
1313 let shrunk = Rect::new(
1314 full_plot_area.x,
1315 full_plot_area.y,
1316 full_plot_area.width - cb_width - colorbar::COLORBAR_GAP,
1317 full_plot_area.height,
1318 );
1319 let cb_rect = Rect::new(
1320 full_plot_area.x + full_plot_area.width - cb_width,
1321 full_plot_area.y,
1322 cb_width,
1323 full_plot_area.height,
1324 );
1325 (shrunk, Some(cb_rect))
1326 } else {
1327 (full_plot_area, None)
1328 };
1329
1330 let bg_path = Path::rect(plot_area);
1332 renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
1333
1334 if self.show_grid.unwrap_or(theme.show_grid) {
1336 self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1337 }
1338
1339 let clip_path = Path::rect(plot_area);
1341 renderer.push_clip(&clip_path, Affine::IDENTITY);
1342 for artist in &self.artists {
1343 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1344 }
1345 renderer.pop_clip();
1346
1347 let x_minor = if matches!(self.xscale, Scale::Log10) {
1349 ticks::generate_log_minor_ticks(xmin, xmax)
1350 } else {
1351 Vec::new()
1352 };
1353 let y_minor = if matches!(self.yscale, Scale::Log10) {
1354 ticks::generate_log_minor_ticks(ymin, ymax)
1355 } else {
1356 Vec::new()
1357 };
1358
1359 self.draw_spines(renderer, &plot_area, theme);
1361
1362 self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1364 if !x_minor.is_empty() || !y_minor.is_empty() {
1365 self.draw_minor_ticks(renderer, &plot_area, &x_minor, &y_minor, xmin, xmax, ymin, ymax, theme);
1366 }
1367
1368 self.draw_labels(renderer, &plot_area, &bounds, theme);
1370
1371 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1373
1374 if self.show_legend && !suppress_legend {
1376 self.draw_legend(renderer, &plot_area, theme);
1377 }
1378
1379 if let (Some(ref cb), Some(ref cb_rect)) = (&effective_colorbar, &colorbar_rect) {
1381 colorbar::draw_colorbar(renderer, cb, cb_rect, theme);
1382 }
1383 }
1384
1385 fn auto_colorbar_from_artists(&self) -> Option<Colorbar> {
1386 for artist in &self.artists {
1387 match artist {
1388 Artist::Heatmap(a) if a.show_colorbar => {
1389 return Some(Colorbar::new(
1390 a.cmap,
1391 a.effective_vmin(),
1392 a.effective_vmax(),
1393 ));
1394 }
1395 Artist::Hexbin(a) if a.show_colorbar => {
1396 let result = crate::charts::hexbin::bin_hexagonal(
1397 &a.x, &a.y, a.gridsize, a.mincnt,
1398 );
1399 let vmin = result.min_count as f64;
1400 let vmax = (result.max_count as f64).max(vmin + 1.0);
1401 return Some(Colorbar::new(a.cmap, vmin, vmax));
1402 }
1403 _ => {}
1404 }
1405 }
1406 None
1407 }
1408
1409 pub(crate) fn render_twin(
1411 &self,
1412 renderer: &mut impl Renderer,
1413 plot_area: Rect,
1414 bounds: Rect,
1415 fig_theme: &Theme,
1416 ) {
1417 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1418 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1419 let yticks = self.resolve_yticks(ymin, ymax);
1420 let xticks = self.resolve_xticks(xmin, xmax);
1421
1422 let clip_path = Path::rect(plot_area);
1423 renderer.push_clip(&clip_path, Affine::IDENTITY);
1424 for artist in &self.artists {
1425 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1426 }
1427 renderer.pop_clip();
1428
1429 let side = self.twin_side.unwrap_or(TwinSide::Right);
1430 match side {
1431 TwinSide::Right => {
1432 let paint = Paint::new(theme.spine_color);
1433 let stroke = Stroke::new(theme.spine_width);
1434 let mut p = Path::new();
1435 p.move_to(plot_area.right(), plot_area.y);
1436 p.line_to(plot_area.right(), plot_area.bottom());
1437 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1438 self.draw_ticks_right(renderer, &plot_area, &yticks, ymin, ymax, theme);
1439 self.draw_ylabel_right(renderer, &plot_area, &bounds, theme);
1440 }
1441 TwinSide::Top => {
1442 let paint = Paint::new(theme.spine_color);
1443 let stroke = Stroke::new(theme.spine_width);
1444 let mut p = Path::new();
1445 p.move_to(plot_area.x, plot_area.y);
1446 p.line_to(plot_area.right(), plot_area.y);
1447 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1448 self.draw_ticks_top(renderer, &plot_area, &xticks, xmin, xmax, theme);
1449 self.draw_xlabel_top(renderer, &plot_area, theme);
1450 }
1451 }
1452
1453 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1454 }
1455
1456 pub(crate) fn compute_plot_area(&self, bounds: &Rect) -> Rect {
1458 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1459 layout_config.has_title = self.title.is_some();
1460 layout_config.has_xlabel = self.xlabel.is_some();
1461 layout_config.has_ylabel = self.ylabel.is_some();
1462 layout_config.has_legend = self.show_legend;
1463 let layout_result = layout::compute_layout(&layout_config);
1464 Rect::new(
1465 bounds.x + layout_result.plot_area.x,
1466 bounds.y + layout_result.plot_area.y,
1467 layout_result.plot_area.width,
1468 layout_result.plot_area.height,
1469 )
1470 }
1471
1472 pub fn collect_legend_entries(&self) -> Vec<LegendEntry> {
1474 self.artists
1475 .iter()
1476 .filter_map(|a| {
1477 let (label, color, swatch) = match a {
1478 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1479 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1480 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1481 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1482 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1483 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1484 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1485 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1486 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1487 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1488 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1489 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1490 Artist::Contour(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
1491 Artist::Polar(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
1492 Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1493 Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1494 };
1495 label.map(|l| LegendEntry {
1496 label: l.to_string(),
1497 color,
1498 swatch,
1499 })
1500 })
1501 .collect()
1502 }
1503
1504 fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
1511 let mut x_lo = f64::INFINITY;
1512 let mut x_hi = f64::NEG_INFINITY;
1513 let mut y_lo = f64::INFINITY;
1514 let mut y_hi = f64::NEG_INFINITY;
1515
1516 for artist in &self.artists {
1517 match artist {
1518 Artist::Line(a) => {
1519 if let Some((lo, hi)) = a.x.bounds() {
1520 x_lo = x_lo.min(lo);
1521 x_hi = x_hi.max(hi);
1522 }
1523 if let Some((lo, hi)) = a.y.bounds() {
1524 y_lo = y_lo.min(lo);
1525 y_hi = y_hi.max(hi);
1526 }
1527 }
1528 Artist::Scatter(a) => {
1529 if let Some((lo, hi)) = a.x.bounds() {
1530 x_lo = x_lo.min(lo);
1531 x_hi = x_hi.max(hi);
1532 }
1533 if let Some((lo, hi)) = a.y.bounds() {
1534 y_lo = y_lo.min(lo);
1535 y_hi = y_hi.max(hi);
1536 }
1537 }
1538 Artist::Bar(a) => {
1539 let n = a.categories.len() as f64;
1540 if a.horizontal {
1541 y_lo = 0.0_f64.min(y_lo);
1543 y_hi = n.max(y_hi);
1544 x_lo = 0.0_f64.min(x_lo);
1545 if let Some((lo, hi)) = a.heights.bounds() {
1546 x_lo = x_lo.min(lo.min(0.0));
1547 x_hi = x_hi.max(hi);
1548 }
1549 } else {
1550 x_lo = 0.0_f64.min(x_lo);
1552 x_hi = n.max(x_hi);
1553 y_lo = 0.0_f64.min(y_lo);
1554 if let Some((lo, hi)) = a.heights.bounds() {
1555 y_lo = y_lo.min(lo.min(0.0));
1556 y_hi = y_hi.max(hi);
1557 }
1558 }
1559 }
1560 Artist::Histogram(a) => {
1561 if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
1562 x_lo = x_lo.min(first);
1563 x_hi = x_hi.max(last);
1564 }
1565 y_lo = 0.0_f64.min(y_lo);
1566 let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
1567 y_hi = y_hi.max(max_count);
1568 }
1569 Artist::FillBetween(a) => {
1570 if let Some((lo, hi)) = a.x.bounds() {
1571 x_lo = x_lo.min(lo);
1572 x_hi = x_hi.max(hi);
1573 }
1574 if let Some((lo, hi)) = a.y1.bounds() {
1575 y_lo = y_lo.min(lo);
1576 y_hi = y_hi.max(hi);
1577 }
1578 if let Some((lo, hi)) = a.y2.bounds() {
1579 y_lo = y_lo.min(lo);
1580 y_hi = y_hi.max(hi);
1581 }
1582 }
1583 Artist::Step(a) => {
1584 if let Some((lo, hi)) = a.x.bounds() {
1585 x_lo = x_lo.min(lo);
1586 x_hi = x_hi.max(hi);
1587 }
1588 if let Some((lo, hi)) = a.y.bounds() {
1589 y_lo = y_lo.min(lo);
1590 y_hi = y_hi.max(hi);
1591 }
1592 }
1593 Artist::Stem(a) => {
1594 if let Some((lo, hi)) = a.x.bounds() {
1595 x_lo = x_lo.min(lo);
1596 x_hi = x_hi.max(hi);
1597 }
1598 if let Some((lo, hi)) = a.y.bounds() {
1599 y_lo = y_lo.min(lo.min(a.baseline));
1600 y_hi = y_hi.max(hi.max(a.baseline));
1601 }
1602 }
1603 Artist::BoxPlot(a) => {
1604 let n = a.stats.len() as f64;
1605 x_lo = 0.0_f64.min(x_lo);
1606 x_hi = n.max(x_hi);
1607 for s in &a.stats {
1608 y_lo = y_lo.min(s.whisker_low);
1609 y_hi = y_hi.max(s.whisker_high);
1610 for &o in &s.outliers {
1611 y_lo = y_lo.min(o);
1612 y_hi = y_hi.max(o);
1613 }
1614 }
1615 }
1616 Artist::ErrorBar(a) => {
1617 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1618 x_lo = x_lo.min(bxlo);
1619 x_hi = x_hi.max(bxhi);
1620 y_lo = y_lo.min(bylo);
1621 y_hi = y_hi.max(byhi);
1622 }
1623 Artist::Heatmap(a) => {
1624 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1625 x_lo = x_lo.min(bxlo);
1626 x_hi = x_hi.max(bxhi);
1627 y_lo = y_lo.min(bylo);
1628 y_hi = y_hi.max(byhi);
1629 }
1630 Artist::Pie(a) => {
1631 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1634 x_lo = x_lo.min(bxlo);
1635 x_hi = x_hi.max(bxhi);
1636 y_lo = y_lo.min(bylo);
1637 y_hi = y_hi.max(byhi);
1638 }
1639 Artist::Violin(a) => {
1640 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1641 x_lo = x_lo.min(bxlo);
1642 x_hi = x_hi.max(bxhi);
1643 y_lo = y_lo.min(bylo);
1644 y_hi = y_hi.max(byhi);
1645 }
1646 Artist::Contour(a) => {
1647 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1648 x_lo = x_lo.min(bxlo);
1649 x_hi = x_hi.max(bxhi);
1650 y_lo = y_lo.min(bylo);
1651 y_hi = y_hi.max(byhi);
1652 }
1653 Artist::Polar(a) => {
1654 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1655 x_lo = x_lo.min(bxlo);
1656 x_hi = x_hi.max(bxhi);
1657 y_lo = y_lo.min(bylo);
1658 y_hi = y_hi.max(byhi);
1659 }
1660 Artist::Hexbin(a) => {
1661 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1662 x_lo = x_lo.min(bxlo);
1663 x_hi = x_hi.max(bxhi);
1664 y_lo = y_lo.min(bylo);
1665 y_hi = y_hi.max(byhi);
1666 }
1667 Artist::Waterfall(a) => {
1668 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1669 x_lo = x_lo.min(bxlo);
1670 x_hi = x_hi.max(bxhi);
1671 y_lo = y_lo.min(bylo);
1672 y_hi = y_hi.max(byhi);
1673 }
1674 }
1675 }
1676
1677 if !x_lo.is_finite() || !x_hi.is_finite() {
1679 x_lo = if self.xscale.requires_positive() { 1.0 } else { 0.0 };
1680 x_hi = if self.xscale.requires_positive() { 10.0 } else { 1.0 };
1681 }
1682 if !y_lo.is_finite() || !y_hi.is_finite() {
1683 y_lo = if self.yscale.requires_positive() { 1.0 } else { 0.0 };
1684 y_hi = if self.yscale.requires_positive() { 10.0 } else { 1.0 };
1685 }
1686
1687 if self.xscale.requires_positive() {
1689 if x_lo <= 0.0 {
1690 x_lo = if x_hi > 0.0 { x_hi * 1e-4 } else { 1.0 };
1692 }
1693 if x_hi <= x_lo {
1694 x_hi = x_lo * 10.0;
1695 }
1696 }
1697 if self.yscale.requires_positive() {
1698 if y_lo <= 0.0 {
1699 y_lo = if y_hi > 0.0 { y_hi * 1e-4 } else { 1.0 };
1700 }
1701 if y_hi <= y_lo {
1702 y_hi = y_lo * 10.0;
1703 }
1704 }
1705
1706 if (x_hi - x_lo).abs() < f64::EPSILON {
1708 x_lo -= 0.5;
1709 x_hi += 0.5;
1710 }
1711 if (y_hi - y_lo).abs() < f64::EPSILON {
1712 y_lo -= 0.5;
1713 y_hi += 0.5;
1714 }
1715
1716 let (x_pad_lo, x_pad_hi) = if self.xscale.requires_positive() {
1719 let factor = 1.0 + AUTOSCALE_PAD;
1721 (x_lo / factor, x_hi * factor)
1722 } else {
1723 let pad = (x_hi - x_lo) * AUTOSCALE_PAD;
1724 (x_lo - pad, x_hi + pad)
1725 };
1726 let (y_pad_lo, y_pad_hi) = if self.yscale.requires_positive() {
1727 let factor = 1.0 + AUTOSCALE_PAD;
1728 (y_lo / factor, y_hi * factor)
1729 } else {
1730 let pad = (y_hi - y_lo) * AUTOSCALE_PAD;
1731 (y_lo - pad, y_hi + pad)
1732 };
1733 x_lo = x_pad_lo;
1734 x_hi = x_pad_hi;
1735 y_lo = y_pad_lo;
1736 y_hi = y_pad_hi;
1737
1738 if let Some((lo, hi)) = self.xlim {
1740 x_lo = lo;
1741 x_hi = hi;
1742 }
1743 if let Some((lo, hi)) = self.ylim {
1744 y_lo = lo;
1745 y_hi = hi;
1746 }
1747
1748 if self.x_inverted {
1750 std::mem::swap(&mut x_lo, &mut x_hi);
1751 }
1752 if self.y_inverted {
1753 std::mem::swap(&mut y_lo, &mut y_hi);
1754 }
1755
1756 (x_lo, x_hi, y_lo, y_hi)
1757 }
1758
1759 fn resolve_xticks(&self, xmin: f64, xmax: f64) -> Vec<ticks::Tick> {
1766 if let Some(ref positions) = self.custom_xticks {
1767 let labels = self.custom_xticklabels.as_ref();
1768 positions
1769 .iter()
1770 .enumerate()
1771 .map(|(i, &v)| ticks::Tick {
1772 value: v,
1773 label: labels
1774 .and_then(|l| l.get(i))
1775 .cloned()
1776 .unwrap_or_else(|| ticks::format_tick_value(v)),
1777 })
1778 .collect()
1779 } else {
1780 let (lo, hi) = if xmin <= xmax { (xmin, xmax) } else { (xmax, xmin) };
1781 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.xscale)
1782 }
1783 }
1784
1785 fn resolve_yticks(&self, ymin: f64, ymax: f64) -> Vec<ticks::Tick> {
1788 if let Some(ref positions) = self.custom_yticks {
1789 let labels = self.custom_yticklabels.as_ref();
1790 positions
1791 .iter()
1792 .enumerate()
1793 .map(|(i, &v)| ticks::Tick {
1794 value: v,
1795 label: labels
1796 .and_then(|l| l.get(i))
1797 .cloned()
1798 .unwrap_or_else(|| ticks::format_tick_value(v)),
1799 })
1800 .collect()
1801 } else {
1802 let (lo, hi) = if ymin <= ymax { (ymin, ymax) } else { (ymax, ymin) };
1803 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.yscale)
1804 }
1805 }
1806
1807 fn draw_grid(
1813 &self,
1814 renderer: &mut impl Renderer,
1815 plot_area: &Rect,
1816 xticks: &[ticks::Tick],
1817 yticks: &[ticks::Tick],
1818 xmin: f64,
1819 xmax: f64,
1820 ymin: f64,
1821 ymax: f64,
1822 theme: &Theme,
1823 ) {
1824 let grid_color = if let Some(alpha) = self.grid_alpha {
1826 theme.grid_color.with_alpha((alpha * 255.0) as u8)
1827 } else {
1828 theme.grid_color
1829 };
1830 let paint = Paint::new(grid_color);
1831
1832 let mut stroke = Stroke::new(theme.grid_width);
1834 if let Some(style) = self.grid_style {
1835 stroke = match style {
1836 LineStyle::Solid => stroke,
1837 LineStyle::Dashed => stroke.with_dash(DashPattern {
1838 dashes: vec![6.0, 4.0],
1839 offset: 0.0,
1840 }),
1841 LineStyle::Dotted => stroke.with_dash(DashPattern {
1842 dashes: vec![2.0, 2.0],
1843 offset: 0.0,
1844 }),
1845 LineStyle::DashDot => stroke.with_dash(DashPattern {
1846 dashes: vec![6.0, 3.0, 2.0, 3.0],
1847 offset: 0.0,
1848 }),
1849 };
1850 }
1851
1852 let draw_x = matches!(self.grid_axis, GridAxis::X | GridAxis::Both);
1853 let draw_y = matches!(self.grid_axis, GridAxis::Y | GridAxis::Both);
1854
1855 if draw_x {
1857 for tick in xticks {
1858 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1859 let mut path = Path::new();
1860 path.move_to(pt.x, plot_area.y);
1861 path.line_to(pt.x, plot_area.bottom());
1862 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1863 }
1864 }
1865
1866 if draw_y {
1868 for tick in yticks {
1869 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1870 let mut path = Path::new();
1871 path.move_to(plot_area.x, pt.y);
1872 path.line_to(plot_area.right(), pt.y);
1873 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1874 }
1875 }
1876 }
1877
1878 fn draw_artist(
1884 &self,
1885 renderer: &mut impl Renderer,
1886 artist: &Artist,
1887 plot_area: &Rect,
1888 xmin: f64,
1889 xmax: f64,
1890 ymin: f64,
1891 ymax: f64,
1892 theme: &Theme,
1893 ) {
1894 match artist {
1895 Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1896 Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1897 Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1898 Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1899 Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1900 Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1901 Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1902 Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1903 Artist::ErrorBar(a) => self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1904 Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1905 Artist::Pie(a) => self.draw_pie(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1906 Artist::Violin(a) => self.draw_violin(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1907 Artist::Contour(a) => self.draw_contour(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1908 Artist::Polar(a) => self.draw_polar(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1909 Artist::Hexbin(a) => self.draw_hexbin(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1910 Artist::Waterfall(a) => self.draw_waterfall(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1911 }
1912 }
1913
1914 fn draw_line(
1919 &self,
1920 renderer: &mut impl Renderer,
1921 artist: &LineArtist,
1922 plot_area: &Rect,
1923 xmin: f64,
1924 xmax: f64,
1925 ymin: f64,
1926 ymax: f64,
1927 ) {
1928 if artist.x.is_empty() {
1929 return;
1930 }
1931
1932 let indices: Vec<usize> = match artist.decimate {
1934 Some((threshold, method)) if artist.x.len() > threshold => {
1935 use crate::decimate::{self, DecimateMethod};
1936 match method {
1937 DecimateMethod::Lttb => {
1938 decimate::lttb(&artist.x.data, &artist.y.data, threshold)
1939 }
1940 DecimateMethod::MinMax => {
1941 decimate::minmax(&artist.x.data, &artist.y.data, threshold)
1942 }
1943 }
1944 }
1945 _ => (0..artist.x.len()).collect(),
1946 };
1947
1948 if indices.is_empty() {
1949 return;
1950 }
1951
1952 let mut path = Path::new();
1953 let first = self.data_to_pixel(
1954 artist.x.data[indices[0]],
1955 artist.y.data[indices[0]],
1956 plot_area,
1957 xmin, xmax, ymin, ymax,
1958 );
1959 path.move_to(first.x, first.y);
1960
1961 for &i in &indices[1..] {
1962 let pt = self.data_to_pixel(
1963 artist.x.data[i],
1964 artist.y.data[i],
1965 plot_area,
1966 xmin, xmax, ymin, ymax,
1967 );
1968 path.line_to(pt.x, pt.y);
1969 }
1970
1971 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1972 let paint = Paint::new(color);
1973 let mut stroke = Stroke::new(artist.width);
1974
1975 match artist.style {
1977 crate::theme::LineStyle::Solid => {}
1978 crate::theme::LineStyle::Dashed => {
1979 stroke = stroke.with_dash(DashPattern {
1980 dashes: vec![6.0, 4.0],
1981 offset: 0.0,
1982 });
1983 }
1984 crate::theme::LineStyle::Dotted => {
1985 stroke = stroke.with_dash(DashPattern {
1986 dashes: vec![2.0, 2.0],
1987 offset: 0.0,
1988 });
1989 }
1990 crate::theme::LineStyle::DashDot => {
1991 stroke = stroke.with_dash(DashPattern {
1992 dashes: vec![6.0, 3.0, 2.0, 3.0],
1993 offset: 0.0,
1994 });
1995 }
1996 }
1997
1998 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1999 }
2000
2001 fn draw_scatter(
2003 &self,
2004 renderer: &mut impl Renderer,
2005 artist: &ScatterArtist,
2006 plot_area: &Rect,
2007 xmin: f64,
2008 xmax: f64,
2009 ymin: f64,
2010 ymax: f64,
2011 theme: &Theme,
2012 ) {
2013 let alpha_byte = (artist.alpha * 255.0) as u8;
2014
2015 let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
2017 (Some(c_vals), Some(cmap)) if !c_vals.is_empty() => Some(cmap.map_values(c_vals)),
2018 _ => None,
2019 };
2020
2021 let default_color = artist.color.with_alpha(alpha_byte);
2022 let default_paint = Paint::new(default_color);
2023 let radius = artist.size / 2.0;
2024
2025 for i in 0..artist.x.len() {
2026 let pt = self.data_to_pixel(
2027 artist.x.data[i],
2028 artist.y.data[i],
2029 plot_area,
2030 xmin, xmax, ymin, ymax,
2031 );
2032
2033 let paint = if let Some(ref cc) = cmap_colors {
2035 Paint::new(cc[i].with_alpha(alpha_byte))
2036 } else if let Some(ref cs) = artist.colors {
2037 Paint::new(cs[i].with_alpha(alpha_byte))
2038 } else {
2039 default_paint
2040 };
2041
2042 let marker_path = match artist.marker {
2043 Marker::Circle | Marker::Point => Path::circle(pt, radius),
2044 Marker::Square => {
2045 Path::rect(Rect::new(pt.x - radius, pt.y - radius, radius * 2.0, radius * 2.0))
2046 }
2047 Marker::Diamond => {
2048 let mut p = Path::new();
2049 p.move_to(pt.x, pt.y - radius);
2050 p.line_to(pt.x + radius, pt.y);
2051 p.line_to(pt.x, pt.y + radius);
2052 p.line_to(pt.x - radius, pt.y);
2053 p.close();
2054 p
2055 }
2056 Marker::Triangle => {
2057 let mut p = Path::new();
2058 let h = radius * 1.1547; p.move_to(pt.x, pt.y - radius);
2060 p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
2061 p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
2062 p.close();
2063 p
2064 }
2065 Marker::Plus => {
2066 let mut p = Path::new();
2068 p.move_to(pt.x - radius, pt.y);
2069 p.line_to(pt.x + radius, pt.y);
2070 p.move_to(pt.x, pt.y - radius);
2071 p.line_to(pt.x, pt.y + radius);
2072 let stroke = Stroke::new(theme.line_width.max(1.0));
2073 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2074 continue;
2075 }
2076 Marker::Cross => {
2077 let mut p = Path::new();
2078 let d = radius * 0.707; p.move_to(pt.x - d, pt.y - d);
2080 p.line_to(pt.x + d, pt.y + d);
2081 p.move_to(pt.x + d, pt.y - d);
2082 p.line_to(pt.x - d, pt.y + d);
2083 let stroke = Stroke::new(theme.line_width.max(1.0));
2084 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2085 continue;
2086 }
2087 Marker::Star => {
2088 let mut p = Path::new();
2090 let inner = radius * 0.382;
2091 for j in 0..10 {
2092 let angle = std::f64::consts::FRAC_PI_2
2093 + j as f64 * std::f64::consts::PI / 5.0;
2094 let r = if j % 2 == 0 { radius } else { inner };
2095 let sx = pt.x + r * angle.cos();
2096 let sy = pt.y - r * angle.sin();
2097 if j == 0 {
2098 p.move_to(sx, sy);
2099 } else {
2100 p.line_to(sx, sy);
2101 }
2102 }
2103 p.close();
2104 p
2105 }
2106 };
2107
2108 renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
2109 }
2110 }
2111
2112 fn draw_bar(
2114 &self,
2115 renderer: &mut impl Renderer,
2116 artist: &BarArtist,
2117 plot_area: &Rect,
2118 xmin: f64,
2119 xmax: f64,
2120 ymin: f64,
2121 ymax: f64,
2122 ) {
2123 let n = artist.categories.len();
2124 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2125 let paint = Paint::new(color);
2126
2127 if artist.horizontal {
2128 let cat_range = ymax - ymin;
2130 let cat_step = cat_range / n as f64;
2131 let bar_half = cat_step * artist.bar_width * 0.5;
2132
2133 for i in 0..n {
2134 let base_center = ymin + (i as f64 + 0.5) * cat_step;
2135 let cat_center = if let Some(ref off) = artist.offset {
2136 base_center + if i < off.len() { off[i] } else { 0.0 }
2137 } else {
2138 base_center
2139 };
2140 let value = artist.heights.data[i];
2141 let base = if let Some(ref bot) = artist.bottom {
2142 if i < bot.len() { bot[i] } else { 0.0 }
2143 } else {
2144 0.0
2145 };
2146
2147 let left_val = base.min(base + value);
2148 let right_val = base.max(base + value);
2149
2150 let p_left = self.data_to_pixel(left_val, cat_center - bar_half, plot_area, xmin, xmax, ymin, ymax);
2151 let p_right = self.data_to_pixel(right_val, cat_center + bar_half, plot_area, xmin, xmax, ymin, ymax);
2152
2153 let rect = Rect::from_points(p_left, p_right);
2154 let bar_path = Path::rect(rect);
2155 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2156 }
2157 } else {
2158 let cat_range = xmax - xmin;
2160 let cat_step = cat_range / n as f64;
2161 let bar_half = cat_step * artist.bar_width * 0.5;
2162
2163 for i in 0..n {
2164 let base_center = xmin + (i as f64 + 0.5) * cat_step;
2165 let cat_center = if let Some(ref off) = artist.offset {
2166 base_center + if i < off.len() { off[i] } else { 0.0 }
2167 } else {
2168 base_center
2169 };
2170 let value = artist.heights.data[i];
2171 let base = if let Some(ref bot) = artist.bottom {
2172 if i < bot.len() { bot[i] } else { 0.0 }
2173 } else {
2174 0.0
2175 };
2176
2177 let bottom_val = base.min(base + value);
2178 let top_val = base.max(base + value);
2179
2180 let p_bl = self.data_to_pixel(cat_center - bar_half, bottom_val, plot_area, xmin, xmax, ymin, ymax);
2181 let p_tr = self.data_to_pixel(cat_center + bar_half, top_val, plot_area, xmin, xmax, ymin, ymax);
2182
2183 let rect = Rect::from_points(p_bl, p_tr);
2184 let bar_path = Path::rect(rect);
2185 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2186 }
2187 }
2188 }
2189
2190 fn draw_hist(
2192 &self,
2193 renderer: &mut impl Renderer,
2194 artist: &HistArtist,
2195 plot_area: &Rect,
2196 xmin: f64,
2197 xmax: f64,
2198 ymin: f64,
2199 ymax: f64,
2200 ) {
2201 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2202 let paint = Paint::new(color);
2203 let stroke_paint = Paint::new(Color::WHITE);
2204 let stroke = Stroke::new(0.5);
2205
2206 for i in 0..artist.counts.len() {
2207 let left = artist.bin_edges[i];
2208 let right = artist.bin_edges[i + 1];
2209 let height = artist.counts[i];
2210
2211 if height <= 0.0 {
2212 continue;
2213 }
2214
2215 let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
2216 let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
2217
2218 let rect = Rect::from_points(p_bl, p_tr);
2219 let bar_path = Path::rect(rect);
2220 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
2221 renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
2223 }
2224 }
2225
2226 fn draw_fill_between(
2229 &self,
2230 renderer: &mut impl Renderer,
2231 artist: &FillBetweenArtist,
2232 plot_area: &Rect,
2233 xmin: f64,
2234 xmax: f64,
2235 ymin: f64,
2236 ymax: f64,
2237 ) {
2238 if artist.x.is_empty() {
2239 return;
2240 }
2241
2242 let n = artist.x.len();
2243 let mut path = Path::new();
2244
2245 let first = self.data_to_pixel(
2247 artist.x.data[0],
2248 artist.y1.data[0],
2249 plot_area,
2250 xmin, xmax, ymin, ymax,
2251 );
2252 path.move_to(first.x, first.y);
2253 for i in 1..n {
2254 let pt = self.data_to_pixel(
2255 artist.x.data[i],
2256 artist.y1.data[i],
2257 plot_area,
2258 xmin, xmax, ymin, ymax,
2259 );
2260 path.line_to(pt.x, pt.y);
2261 }
2262
2263 for i in (0..n).rev() {
2265 let pt = self.data_to_pixel(
2266 artist.x.data[i],
2267 artist.y2.data[i],
2268 plot_area,
2269 xmin, xmax, ymin, ymax,
2270 );
2271 path.line_to(pt.x, pt.y);
2272 }
2273 path.close();
2274
2275 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2276 let paint = Paint::new(color);
2277 renderer.fill_path(&path, &paint, Affine::IDENTITY);
2278 }
2279
2280
2281 fn draw_spines(
2287 &self,
2288 renderer: &mut impl Renderer,
2289 plot_area: &Rect,
2290 theme: &Theme,
2291 ) {
2292 let paint = Paint::new(theme.spine_color);
2293 let stroke = Stroke::new(theme.spine_width);
2294
2295 if theme.show_bottom_spine {
2297 let mut p = Path::new();
2298 p.move_to(plot_area.x, plot_area.bottom());
2299 p.line_to(plot_area.right(), plot_area.bottom());
2300 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2301 }
2302 if theme.show_left_spine {
2304 let mut p = Path::new();
2305 p.move_to(plot_area.x, plot_area.y);
2306 p.line_to(plot_area.x, plot_area.bottom());
2307 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2308 }
2309 if theme.show_top_spine {
2311 let mut p = Path::new();
2312 p.move_to(plot_area.x, plot_area.y);
2313 p.line_to(plot_area.right(), plot_area.y);
2314 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2315 }
2316 if theme.show_right_spine {
2318 let mut p = Path::new();
2319 p.move_to(plot_area.right(), plot_area.y);
2320 p.line_to(plot_area.right(), plot_area.bottom());
2321 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2322 }
2323 }
2324
2325 fn draw_ticks(
2331 &self,
2332 renderer: &mut impl Renderer,
2333 plot_area: &Rect,
2334 xticks: &[ticks::Tick],
2335 yticks: &[ticks::Tick],
2336 xmin: f64,
2337 xmax: f64,
2338 ymin: f64,
2339 ymax: f64,
2340 theme: &Theme,
2341 ) {
2342 let tick_paint = Paint::new(theme.tick_color);
2343 let tick_stroke = Stroke::new(1.0);
2344 let tick_len = theme.tick_length;
2345
2346 let x_label_style = TextStyle {
2347 size: theme.tick_label_size,
2348 color: theme.text_color,
2349 weight: FontWeight::Normal,
2350 family: theme.font_family.clone(),
2351 halign: if self.xtick_rotation.abs() > 1.0 {
2352 HAlign::Right
2353 } else {
2354 HAlign::Center
2355 },
2356 valign: VAlign::Top,
2357 };
2358
2359 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2361
2362 let x_rot_rad = -self.xtick_rotation.to_radians();
2364
2365 for tick in xticks {
2367 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
2368 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2370 continue;
2371 }
2372 let x = pt.x;
2373 let y_base = plot_area.bottom();
2374
2375 let (y_start, y_end) = if outward {
2377 (y_base, y_base + tick_len)
2378 } else {
2379 (y_base - tick_len, y_base)
2380 };
2381 let mut tp = Path::new();
2382 tp.move_to(x, y_start);
2383 tp.line_to(x, y_end);
2384 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2385
2386 let label_y = if outward {
2388 y_base + tick_len + 2.0
2389 } else {
2390 y_base + 2.0
2391 };
2392 let label_pos = Point::new(x, label_y);
2393 let transform = if self.xtick_rotation.abs() > 0.01 {
2394 let rotate = Affine::rotate(x_rot_rad);
2395 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2396 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2397 from_origin * rotate * to_origin
2398 } else {
2399 Affine::IDENTITY
2400 };
2401 renderer.draw_text(
2402 &tick.label,
2403 label_pos,
2404 &x_label_style,
2405 transform,
2406 );
2407 }
2408
2409 let y_label_style = TextStyle {
2411 halign: HAlign::Right,
2412 valign: VAlign::Middle,
2413 ..x_label_style.clone()
2414 };
2415
2416 let y_rot_rad = -self.ytick_rotation.to_radians();
2418
2419 for tick in yticks {
2420 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
2421 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2423 continue;
2424 }
2425 let y = pt.y;
2426 let x_base = plot_area.x;
2427
2428 let (x_start, x_end) = if outward {
2430 (x_base - tick_len, x_base)
2431 } else {
2432 (x_base, x_base + tick_len)
2433 };
2434 let mut tp = Path::new();
2435 tp.move_to(x_start, y);
2436 tp.line_to(x_end, y);
2437 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2438
2439 let label_x = if outward {
2441 x_base - tick_len - 3.0
2442 } else {
2443 x_base - 3.0
2444 };
2445 let label_pos = Point::new(label_x, y);
2446 let transform = if self.ytick_rotation.abs() > 0.01 {
2447 let rotate = Affine::rotate(y_rot_rad);
2448 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2449 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2450 from_origin * rotate * to_origin
2451 } else {
2452 Affine::IDENTITY
2453 };
2454 renderer.draw_text(
2455 &tick.label,
2456 label_pos,
2457 &y_label_style,
2458 transform,
2459 );
2460 }
2461 }
2462
2463 fn draw_minor_ticks(
2469 &self,
2470 renderer: &mut impl Renderer,
2471 plot_area: &Rect,
2472 x_minor: &[f64],
2473 y_minor: &[f64],
2474 xmin: f64,
2475 xmax: f64,
2476 ymin: f64,
2477 ymax: f64,
2478 theme: &Theme,
2479 ) {
2480 let tick_paint = Paint::new(theme.tick_color);
2481 let tick_stroke = Stroke::new(0.5);
2482 let tick_len = theme.tick_length * 0.5;
2484 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2485
2486 for &val in x_minor {
2488 let pt = self.data_to_pixel(val, ymin, plot_area, xmin, xmax, ymin, ymax);
2489 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2490 continue;
2491 }
2492 let x = pt.x;
2493 let y_base = plot_area.bottom();
2494 let (y_start, y_end) = if outward {
2495 (y_base, y_base + tick_len)
2496 } else {
2497 (y_base - tick_len, y_base)
2498 };
2499 let mut tp = Path::new();
2500 tp.move_to(x, y_start);
2501 tp.line_to(x, y_end);
2502 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2503 }
2504
2505 for &val in y_minor {
2507 let pt = self.data_to_pixel(xmin, val, plot_area, xmin, xmax, ymin, ymax);
2508 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2509 continue;
2510 }
2511 let y = pt.y;
2512 let x_base = plot_area.x;
2513 let (x_start, x_end) = if outward {
2514 (x_base - tick_len, x_base)
2515 } else {
2516 (x_base, x_base + tick_len)
2517 };
2518 let mut tp = Path::new();
2519 tp.move_to(x_start, y);
2520 tp.line_to(x_end, y);
2521 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2522 }
2523 }
2524
2525 fn draw_ticks_right(
2527 &self,
2528 renderer: &mut impl Renderer,
2529 plot_area: &Rect,
2530 yticks: &[ticks::Tick],
2531 ymin: f64,
2532 ymax: f64,
2533 theme: &Theme,
2534 ) {
2535 let tick_paint = Paint::new(theme.tick_color);
2536 let tick_stroke = Stroke::new(1.0);
2537 let tick_len = theme.tick_length;
2538 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2539
2540 let y_label_style = TextStyle {
2541 size: theme.tick_label_size,
2542 color: theme.text_color,
2543 weight: FontWeight::Normal,
2544 family: theme.font_family.clone(),
2545 halign: HAlign::Left,
2546 valign: VAlign::Middle,
2547 };
2548
2549 let y_rot_rad = -self.ytick_rotation.to_radians();
2550
2551 for tick in yticks {
2552 let pt = self.data_to_pixel(0.0, tick.value, plot_area, 0.0, 1.0, ymin, ymax);
2553 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2554 continue;
2555 }
2556 let y = pt.y;
2557 let x_base = plot_area.right();
2558 let (x_start, x_end) = if outward {
2559 (x_base, x_base + tick_len)
2560 } else {
2561 (x_base - tick_len, x_base)
2562 };
2563 let mut tp = Path::new();
2564 tp.move_to(x_start, y);
2565 tp.line_to(x_end, y);
2566 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2567
2568 let label_x = if outward { x_base + tick_len + 3.0 } else { x_base + 3.0 };
2569 let label_pos = Point::new(label_x, y);
2570 let transform = if self.ytick_rotation.abs() > 0.01 {
2571 let rotate = Affine::rotate(y_rot_rad);
2572 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2573 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2574 from_origin * rotate * to_origin
2575 } else {
2576 Affine::IDENTITY
2577 };
2578 renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2579 }
2580 }
2581
2582 fn draw_ylabel_right(
2584 &self,
2585 renderer: &mut impl Renderer,
2586 plot_area: &Rect,
2587 bounds: &Rect,
2588 theme: &Theme,
2589 ) {
2590 if let Some(ylabel) = &self.ylabel {
2591 let style = TextStyle {
2592 size: theme.axis_label_size,
2593 color: theme.text_color,
2594 weight: FontWeight::Normal,
2595 family: theme.font_family.clone(),
2596 halign: HAlign::Center,
2597 valign: VAlign::Top,
2598 };
2599 let x = bounds.right() - 4.0;
2600 let y = plot_area.y + plot_area.height / 2.0;
2601 let rotate = Affine::rotate(std::f64::consts::FRAC_PI_2);
2602 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2603 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2604 let transform = translate_to * rotate * translate_back;
2605 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2606 }
2607 }
2608
2609 fn draw_ticks_top(
2611 &self,
2612 renderer: &mut impl Renderer,
2613 plot_area: &Rect,
2614 xticks: &[ticks::Tick],
2615 xmin: f64,
2616 xmax: f64,
2617 theme: &Theme,
2618 ) {
2619 let tick_paint = Paint::new(theme.tick_color);
2620 let tick_stroke = Stroke::new(1.0);
2621 let tick_len = theme.tick_length;
2622 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2623
2624 let x_label_style = TextStyle {
2625 size: theme.tick_label_size,
2626 color: theme.text_color,
2627 weight: FontWeight::Normal,
2628 family: theme.font_family.clone(),
2629 halign: if self.xtick_rotation.abs() > 1.0 { HAlign::Left } else { HAlign::Center },
2630 valign: VAlign::Bottom,
2631 };
2632
2633 let x_rot_rad = -self.xtick_rotation.to_radians();
2634
2635 for tick in xticks {
2636 let pt = self.data_to_pixel(tick.value, 0.0, plot_area, xmin, xmax, 0.0, 1.0);
2637 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2638 continue;
2639 }
2640 let x = pt.x;
2641 let y_base = plot_area.y;
2642 let (y_start, y_end) = if outward {
2643 (y_base - tick_len, y_base)
2644 } else {
2645 (y_base, y_base + tick_len)
2646 };
2647 let mut tp = Path::new();
2648 tp.move_to(x, y_start);
2649 tp.line_to(x, y_end);
2650 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2651
2652 let label_y = if outward { y_base - tick_len - 2.0 } else { y_base - 2.0 };
2653 let label_pos = Point::new(x, label_y);
2654 let transform = if self.xtick_rotation.abs() > 0.01 {
2655 let rotate = Affine::rotate(x_rot_rad);
2656 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2657 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2658 from_origin * rotate * to_origin
2659 } else {
2660 Affine::IDENTITY
2661 };
2662 renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2663 }
2664 }
2665
2666 fn draw_xlabel_top(
2668 &self,
2669 renderer: &mut impl Renderer,
2670 plot_area: &Rect,
2671 theme: &Theme,
2672 ) {
2673 if let Some(xlabel) = &self.xlabel {
2674 let style = TextStyle {
2675 size: theme.axis_label_size,
2676 color: theme.text_color,
2677 weight: FontWeight::Normal,
2678 family: theme.font_family.clone(),
2679 halign: HAlign::Center,
2680 valign: VAlign::Bottom,
2681 };
2682 let x = plot_area.x + plot_area.width / 2.0;
2683 let y = plot_area.y - theme.tick_length - theme.tick_label_size - 8.0;
2684 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2685 }
2686 }
2687
2688 fn draw_labels(
2694 &self,
2695 renderer: &mut impl Renderer,
2696 plot_area: &Rect,
2697 bounds: &Rect,
2698 theme: &Theme,
2699 ) {
2700 if let Some(title) = &self.title {
2702 let style = TextStyle {
2703 size: theme.title_size,
2704 color: theme.text_color,
2705 weight: theme.title_weight,
2706 family: theme.font_family.clone(),
2707 halign: HAlign::Center,
2708 valign: VAlign::Bottom,
2709 };
2710 let x = plot_area.x + plot_area.width / 2.0;
2711 let y = plot_area.y - 10.0;
2712 renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
2713 }
2714
2715 if let Some(xlabel) = &self.xlabel {
2717 let style = TextStyle {
2718 size: theme.axis_label_size,
2719 color: theme.text_color,
2720 weight: FontWeight::Normal,
2721 family: theme.font_family.clone(),
2722 halign: HAlign::Center,
2723 valign: VAlign::Top,
2724 };
2725 let x = plot_area.x + plot_area.width / 2.0;
2726 let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
2728 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2729 }
2730
2731 if let Some(ylabel) = &self.ylabel {
2733 let style = TextStyle {
2734 size: theme.axis_label_size,
2735 color: theme.text_color,
2736 weight: FontWeight::Normal,
2737 family: theme.font_family.clone(),
2738 halign: HAlign::Center,
2739 valign: VAlign::Bottom,
2740 };
2741 let x = bounds.x + 4.0;
2742 let y = plot_area.y + plot_area.height / 2.0;
2743 let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
2745 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2746 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2747 let transform = translate_to * rotate * translate_back;
2748 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2749 }
2750 }
2751
2752 fn draw_boxplot(
2760 &self,
2761 renderer: &mut impl Renderer,
2762 artist: &BoxPlotArtist,
2763 plot_area: &Rect,
2764 xmin: f64,
2765 xmax: f64,
2766 ymin: f64,
2767 ymax: f64,
2768 ) {
2769 let n = artist.stats.len();
2770 if n == 0 {
2771 return;
2772 }
2773
2774 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2775 let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
2776 let paint = Paint::new(stroke_color);
2777 let thin = Stroke::new(1.0);
2778 let thick = Stroke::new(2.0);
2779 let hair = Stroke::new(0.5);
2780
2781 for (i, stats) in artist.stats.iter().enumerate() {
2782 let cx = i as f64 + 0.5;
2783 let half = artist.box_width / 2.0;
2784 let left = cx - half;
2785 let right = cx + half;
2786
2787 let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2789 let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2790 let box_rect_path = {
2791 let mut p = Path::new();
2792 p.move_to(tl.x, tl.y);
2793 p.line_to(br.x, tl.y);
2794 p.line_to(br.x, br.y);
2795 p.line_to(tl.x, br.y);
2796 p.close();
2797 p
2798 };
2799 renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
2800 renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
2801
2802 let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
2804 let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
2805 let mut median_path = Path::new();
2806 median_path.move_to(ml.x, ml.y);
2807 median_path.line_to(mr.x, mr.y);
2808 renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
2809
2810 let wl_bottom = self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2812 let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2813 let mut wl_path = Path::new();
2814 wl_path.move_to(wl_top.x, wl_top.y);
2815 wl_path.line_to(wl_bottom.x, wl_bottom.y);
2816 renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
2817
2818 let cap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2820 let cap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2821 let mut cap_path = Path::new();
2822 cap_path.move_to(cap_left.x, cap_left.y);
2823 cap_path.line_to(cap_right.x, cap_right.y);
2824 renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
2825
2826 let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2828 let wu_top = self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2829 let mut wu_path = Path::new();
2830 wu_path.move_to(wu_bottom.x, wu_bottom.y);
2831 wu_path.line_to(wu_top.x, wu_top.y);
2832 renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
2833
2834 let ucap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2836 let ucap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2837 let mut ucap_path = Path::new();
2838 ucap_path.move_to(ucap_left.x, ucap_left.y);
2839 ucap_path.line_to(ucap_right.x, ucap_right.y);
2840 renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
2841
2842 if artist.show_outliers {
2844 let r = 3.0;
2845 for &val in &stats.outliers {
2846 let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
2847 let mut dot = Path::new();
2848 for seg in 0..8 {
2849 let angle = std::f64::consts::TAU * seg as f64 / 8.0;
2850 let dx = r * angle.cos();
2851 let dy = r * angle.sin();
2852 if seg == 0 {
2853 dot.move_to(pt.x + dx, pt.y + dy);
2854 } else {
2855 dot.line_to(pt.x + dx, pt.y + dy);
2856 }
2857 }
2858 dot.close();
2859 renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
2860 renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
2861 }
2862 }
2863 }
2864 }
2865
2866 fn draw_step(
2868 &self,
2869 renderer: &mut impl Renderer,
2870 artist: &StepArtist,
2871 plot_area: &Rect,
2872 xmin: f64,
2873 xmax: f64,
2874 ymin: f64,
2875 ymax: f64,
2876 ) {
2877 if artist.x.len() < 2 {
2878 return;
2879 }
2880 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2881 let paint = Paint::new(color);
2882 let stroke = Stroke::new(artist.width);
2883
2884 let mut path = Path::new();
2885 let first = self.data_to_pixel(
2886 artist.x.data[0], artist.y.data[0],
2887 plot_area, xmin, xmax, ymin, ymax,
2888 );
2889 path.move_to(first.x, first.y);
2890
2891 for i in 1..artist.x.len() {
2892 let prev = self.data_to_pixel(
2893 artist.x.data[i - 1], artist.y.data[i - 1],
2894 plot_area, xmin, xmax, ymin, ymax,
2895 );
2896 let cur = self.data_to_pixel(
2897 artist.x.data[i], artist.y.data[i],
2898 plot_area, xmin, xmax, ymin, ymax,
2899 );
2900 match artist.where_step {
2901 StepWhere::Pre => {
2902 path.line_to(prev.x, cur.y);
2903 path.line_to(cur.x, cur.y);
2904 }
2905 StepWhere::Post => {
2906 path.line_to(cur.x, prev.y);
2907 path.line_to(cur.x, cur.y);
2908 }
2909 StepWhere::Mid => {
2910 let mid_x = (prev.x + cur.x) / 2.0;
2911 path.line_to(mid_x, prev.y);
2912 path.line_to(mid_x, cur.y);
2913 path.line_to(cur.x, cur.y);
2914 }
2915 }
2916 }
2917 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
2918 }
2919
2920 fn draw_stem(
2922 &self,
2923 renderer: &mut impl Renderer,
2924 artist: &StemArtist,
2925 plot_area: &Rect,
2926 xmin: f64,
2927 xmax: f64,
2928 ymin: f64,
2929 ymax: f64,
2930 ) {
2931 if artist.x.is_empty() {
2932 return;
2933 }
2934 let alpha_byte = (artist.alpha * 255.0) as u8;
2935 let color = artist.color.with_alpha(alpha_byte);
2936 let paint = Paint::new(color);
2937 let stroke = Stroke::new(artist.line_width);
2938 let radius = artist.marker_size / 2.0;
2939
2940 let bl_left = self.data_to_pixel(
2942 artist.x.data[0], artist.baseline,
2943 plot_area, xmin, xmax, ymin, ymax,
2944 );
2945 let bl_right = self.data_to_pixel(
2946 *artist.x.data.last().unwrap(), artist.baseline,
2947 plot_area, xmin, xmax, ymin, ymax,
2948 );
2949 let mut bl_path = Path::new();
2950 bl_path.move_to(bl_left.x, bl_left.y);
2951 bl_path.line_to(bl_right.x, bl_right.y);
2952 let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
2953 let bl_stroke = Stroke::new(0.8);
2954 renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
2955
2956 for i in 0..artist.x.len() {
2958 let base = self.data_to_pixel(
2959 artist.x.data[i], artist.baseline,
2960 plot_area, xmin, xmax, ymin, ymax,
2961 );
2962 let tip = self.data_to_pixel(
2963 artist.x.data[i], artist.y.data[i],
2964 plot_area, xmin, xmax, ymin, ymax,
2965 );
2966 let mut stem_path = Path::new();
2967 stem_path.move_to(base.x, base.y);
2968 stem_path.line_to(tip.x, tip.y);
2969 renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
2970 let marker = Path::circle(tip, radius);
2971 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
2972 }
2973 }
2974
2975 fn draw_errorbar(
2978 &self,
2979 renderer: &mut impl Renderer,
2980 artist: &ErrorBarArtist,
2981 plot_area: &Rect,
2982 xmin: f64,
2983 xmax: f64,
2984 ymin: f64,
2985 ymax: f64,
2986 ) {
2987 if artist.x.is_empty() {
2988 return;
2989 }
2990 let paint = Paint::new(artist.color);
2991 let stroke = Stroke::new(artist.line_width);
2992 let marker_radius = 3.0;
2993
2994 let mut line_path = Path::new();
2996 let first = self.data_to_pixel(
2997 artist.x.data[0], artist.y.data[0],
2998 plot_area, xmin, xmax, ymin, ymax,
2999 );
3000 line_path.move_to(first.x, first.y);
3001 for i in 1..artist.x.len() {
3002 let pt = self.data_to_pixel(
3003 artist.x.data[i], artist.y.data[i],
3004 plot_area, xmin, xmax, ymin, ymax,
3005 );
3006 line_path.line_to(pt.x, pt.y);
3007 }
3008 renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
3009
3010 for i in 0..artist.x.len() {
3012 let xv = artist.x.data[i];
3013 let yv = artist.y.data[i];
3014 let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
3015
3016 let marker = Path::circle(center, marker_radius);
3018 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
3019
3020 if let Some(ref yerr) = artist.yerr {
3022 let (lo, hi) = match yerr {
3023 ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
3024 ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
3025 };
3026 let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
3027 let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
3028
3029 let mut bar = Path::new();
3031 bar.move_to(pt_lo.x, pt_lo.y);
3032 bar.line_to(pt_hi.x, pt_hi.y);
3033 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3034
3035 if artist.cap_size > 0.0 {
3037 let half_cap = artist.cap_size / 2.0;
3038 let mut cap_lo = Path::new();
3039 cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
3040 cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
3041 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3042
3043 let mut cap_hi = Path::new();
3044 cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
3045 cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
3046 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3047 }
3048 }
3049
3050 if let Some(ref xerr) = artist.xerr {
3052 let (lo, hi) = match xerr {
3053 ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
3054 ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
3055 };
3056 let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
3057 let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
3058
3059 let mut bar = Path::new();
3061 bar.move_to(pt_lo.x, pt_lo.y);
3062 bar.line_to(pt_hi.x, pt_hi.y);
3063 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
3064
3065 if artist.cap_size > 0.0 {
3067 let half_cap = artist.cap_size / 2.0;
3068 let mut cap_lo = Path::new();
3069 cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
3070 cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
3071 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
3072
3073 let mut cap_hi = Path::new();
3074 cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
3075 cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
3076 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
3077 }
3078 }
3079 }
3080 }
3081
3082 fn draw_heatmap(
3085 &self,
3086 renderer: &mut impl Renderer,
3087 artist: &HeatmapArtist,
3088 plot_area: &Rect,
3089 xmin: f64,
3090 xmax: f64,
3091 ymin: f64,
3092 ymax: f64,
3093 ) {
3094 let nrows = artist.data.len();
3095 if nrows == 0 {
3096 return;
3097 }
3098 let ncols = artist.data[0].len();
3099 if ncols == 0 {
3100 return;
3101 }
3102
3103 let vmin = artist.effective_vmin();
3104 let vmax = artist.effective_vmax();
3105
3106 let text_style = TextStyle {
3107 size: 10.0,
3108 color: Color::BLACK,
3109 weight: FontWeight::Normal,
3110 family: None,
3111 halign: HAlign::Center,
3112 valign: VAlign::Middle,
3113 };
3114
3115 for row in 0..nrows {
3116 for col in 0..ncols {
3117 let val = artist.data[row][col];
3118 let cell_color = artist.cmap.map_value(val, vmin, vmax);
3119
3120 let p_bl = self.data_to_pixel(
3122 col as f64, row as f64,
3123 plot_area, xmin, xmax, ymin, ymax,
3124 );
3125 let p_tr = self.data_to_pixel(
3126 (col + 1) as f64, (row + 1) as f64,
3127 plot_area, xmin, xmax, ymin, ymax,
3128 );
3129 let rect = Rect::from_points(p_bl, p_tr);
3130 let cell_path = Path::rect(rect);
3131 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
3132
3133 if artist.show_values {
3135 let cx = (p_bl.x + p_tr.x) / 2.0;
3136 let cy = (p_bl.y + p_tr.y) / 2.0;
3137 let label = format!("{val:.1}");
3138 renderer.draw_text(
3139 &label,
3140 Point::new(cx, cy),
3141 &text_style,
3142 Affine::IDENTITY,
3143 );
3144 }
3145 }
3146 }
3147 }
3148
3149 fn draw_polar(
3152 &self,
3153 renderer: &mut impl Renderer,
3154 artist: &PolarArtist,
3155 plot_area: &Rect,
3156 xmin: f64,
3157 xmax: f64,
3158 ymin: f64,
3159 ymax: f64,
3160 theme: &Theme,
3161 ) {
3162 let n = artist.r.len().min(artist.theta.len());
3163 if n == 0 {
3164 return;
3165 }
3166
3167 let r_max = artist.max_finite_r();
3168 if r_max <= 0.0 || !r_max.is_finite() {
3169 return;
3170 }
3171
3172 let center = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3175
3176 let max_radius_px = (plot_area.width / 2.0).min(plot_area.height / 2.0) * 0.85;
3178
3179 let num_r_rings = 5;
3181 let r_step = r_max / num_r_rings as f64;
3182 let grid_color = theme.grid_color;
3183 let grid_paint = Paint::new(grid_color.with_alpha(100));
3184 let grid_stroke = Stroke::new(0.5);
3185 let label_style = TextStyle {
3186 size: 9.0,
3187 color: theme.tick_color,
3188 weight: FontWeight::Normal,
3189 family: theme.font_family.clone(),
3190 halign: HAlign::Left,
3191 valign: VAlign::Middle,
3192 };
3193
3194 for i in 1..=num_r_rings {
3195 let r_val = i as f64 * r_step;
3196 let r_px = r_val / r_max * max_radius_px;
3197 let circle = Path::circle(center, r_px);
3198 renderer.stroke_path(&circle, &grid_paint, &grid_stroke, Affine::IDENTITY);
3199
3200 let label_pt = Point::new(center.x + r_px + 3.0, center.y - 2.0);
3202 let label_text = if r_val == r_val.floor() {
3203 format!("{:.0}", r_val)
3204 } else {
3205 format!("{:.1}", r_val)
3206 };
3207 renderer.draw_text(&label_text, label_pt, &label_style, Affine::IDENTITY);
3208 }
3209
3210 let angle_label_style = TextStyle {
3212 size: 10.0,
3213 color: theme.tick_color,
3214 weight: FontWeight::Normal,
3215 family: theme.font_family.clone(),
3216 halign: HAlign::Center,
3217 valign: VAlign::Middle,
3218 };
3219
3220 for deg in (0..360).step_by(30) {
3221 let angle = (deg as f64).to_radians();
3222 let end_x = center.x + max_radius_px * angle.cos();
3223 let end_y = center.y - max_radius_px * angle.sin();
3224 let mut line = Path::new();
3225 line.move_to(center.x, center.y);
3226 line.line_to(end_x, end_y);
3227 renderer.stroke_path(&line, &grid_paint, &grid_stroke, Affine::IDENTITY);
3228
3229 let label_offset = 14.0;
3231 let lx = center.x + (max_radius_px + label_offset) * angle.cos();
3232 let ly = center.y - (max_radius_px + label_offset) * angle.sin();
3233 let label_text = format!("{}°", deg);
3234 renderer.draw_text(&label_text, Point::new(lx, ly), &angle_label_style, Affine::IDENTITY);
3235 }
3236
3237 let to_px = |r: f64, theta: f64| -> Point {
3240 let px_r = r / r_max * max_radius_px;
3241 Point::new(
3242 center.x + px_r * theta.cos(),
3243 center.y - px_r * theta.sin(),
3244 )
3245 };
3246
3247 let mut path = Path::new();
3248 let mut started = false;
3249
3250 for i in 0..n {
3251 let r = artist.r[i];
3252 let theta = artist.theta[i];
3253 if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3254 continue;
3255 }
3256 let pt = to_px(r, theta);
3257 if !started {
3258 path.move_to(pt.x, pt.y);
3259 started = true;
3260 } else {
3261 path.line_to(pt.x, pt.y);
3262 }
3263 }
3264
3265 if !started {
3266 return;
3267 }
3268
3269 let alpha_byte = (artist.alpha * 255.0) as u8;
3270 let color = artist.color.with_alpha(alpha_byte);
3271
3272 if artist.filled {
3273 path.close();
3275 let fill_paint = Paint::new(color);
3276 renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
3277 let stroke_paint = Paint::new(artist.color.with_alpha(((artist.alpha.min(1.0) * 0.8 + 0.2) * 255.0) as u8));
3279 let stroke = Stroke::new(artist.linewidth);
3280 renderer.stroke_path(&path, &stroke_paint, &stroke, Affine::IDENTITY);
3281 } else {
3282 let paint = Paint::new(color);
3284 let stroke = Stroke::new(artist.linewidth);
3285 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3286 }
3287
3288 if let Some(marker) = artist.marker {
3290 let marker_size = 5.0;
3291 let marker_radius = marker_size / 2.0;
3292 let marker_paint = Paint::new(color);
3293
3294 for i in 0..n {
3295 let r = artist.r[i];
3296 let theta = artist.theta[i];
3297 if !r.is_finite() || !theta.is_finite() || r < 0.0 {
3298 continue;
3299 }
3300 let pt = to_px(r, theta);
3301 let marker_path = match marker {
3302 Marker::Circle | Marker::Point => Path::circle(pt, marker_radius),
3303 Marker::Square => Path::rect(Rect::new(
3304 pt.x - marker_radius,
3305 pt.y - marker_radius,
3306 marker_size,
3307 marker_size,
3308 )),
3309 Marker::Diamond => {
3310 let mut p = Path::new();
3311 p.move_to(pt.x, pt.y - marker_radius);
3312 p.line_to(pt.x + marker_radius, pt.y);
3313 p.line_to(pt.x, pt.y + marker_radius);
3314 p.line_to(pt.x - marker_radius, pt.y);
3315 p.close();
3316 p
3317 }
3318 Marker::Triangle => {
3319 let mut p = Path::new();
3320 let h = marker_radius * 1.1547;
3321 p.move_to(pt.x, pt.y - marker_radius);
3322 p.line_to(pt.x + h * 0.5, pt.y + marker_radius * 0.5);
3323 p.line_to(pt.x - h * 0.5, pt.y + marker_radius * 0.5);
3324 p.close();
3325 p
3326 }
3327 Marker::Plus => {
3328 let mut p = Path::new();
3329 p.move_to(pt.x - marker_radius, pt.y);
3330 p.line_to(pt.x + marker_radius, pt.y);
3331 p.move_to(pt.x, pt.y - marker_radius);
3332 p.line_to(pt.x, pt.y + marker_radius);
3333 let ms = Stroke::new(theme.line_width.max(1.0));
3334 renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3335 continue;
3336 }
3337 Marker::Cross => {
3338 let mut p = Path::new();
3339 let d = marker_radius * 0.707;
3340 p.move_to(pt.x - d, pt.y - d);
3341 p.line_to(pt.x + d, pt.y + d);
3342 p.move_to(pt.x + d, pt.y - d);
3343 p.line_to(pt.x - d, pt.y + d);
3344 let ms = Stroke::new(theme.line_width.max(1.0));
3345 renderer.stroke_path(&p, &marker_paint, &ms, Affine::IDENTITY);
3346 continue;
3347 }
3348 Marker::Star => {
3349 let mut p = Path::new();
3350 let inner = marker_radius * 0.382;
3351 for j in 0..10 {
3352 let a = std::f64::consts::FRAC_PI_2 + j as f64 * std::f64::consts::PI / 5.0;
3353 let r = if j % 2 == 0 { marker_radius } else { inner };
3354 let sx = pt.x + r * a.cos();
3355 let sy = pt.y - r * a.sin();
3356 if j == 0 { p.move_to(sx, sy); } else { p.line_to(sx, sy); }
3357 }
3358 p.close();
3359 p
3360 }
3361 };
3362 renderer.fill_path(&marker_path, &marker_paint, Affine::IDENTITY);
3363 }
3364 }
3365 }
3366
3367 fn draw_hexbin(
3370 &self,
3371 renderer: &mut impl Renderer,
3372 artist: &HexbinArtist,
3373 plot_area: &Rect,
3374 xmin: f64,
3375 xmax: f64,
3376 ymin: f64,
3377 ymax: f64,
3378 ) {
3379 use crate::charts::hexbin::{bin_hexagonal, hexagon_vertices, hex_size_for_gridsize};
3380
3381 let result = bin_hexagonal(&artist.x, &artist.y, artist.gridsize, artist.mincnt);
3382 if result.cells.is_empty() {
3383 return;
3384 }
3385
3386 let vmin = result.min_count as f64;
3387 let vmax = result.max_count as f64;
3388
3389 let data_xrange = (xmax - xmin).max(f64::EPSILON);
3391 let hex_data_size = hex_size_for_gridsize(data_xrange, artist.gridsize);
3392
3393 let alpha_byte = (artist.alpha * 255.0).round() as u8;
3394
3395 for &(cx, cy, count) in &result.cells {
3396 let mut fill_color = artist.cmap.map_value(count as f64, vmin, vmax);
3398 fill_color = fill_color.with_alpha(alpha_byte);
3399
3400 let data_verts = hexagon_vertices(cx, cy, hex_data_size);
3402
3403 let mut path = Path::new();
3404 for (i, &(vx, vy)) in data_verts.iter().enumerate() {
3405 let p = self.data_to_pixel(vx, vy, plot_area, xmin, xmax, ymin, ymax);
3406 if i == 0 {
3407 path.move_to(p.x, p.y);
3408 } else {
3409 path.line_to(p.x, p.y);
3410 }
3411 }
3412 path.close();
3413
3414 renderer.fill_path(&path, &Paint::new(fill_color), Affine::IDENTITY);
3415
3416 if let Some(edge_color) = artist.edgecolor {
3418 let stroke = Stroke::new(0.5);
3419 renderer.stroke_path(
3420 &path,
3421 &Paint::new(edge_color.with_alpha(alpha_byte)),
3422 &stroke,
3423 Affine::IDENTITY,
3424 );
3425 }
3426 }
3427 }
3428
3429 fn draw_waterfall(
3431 &self,
3432 renderer: &mut impl Renderer,
3433 artist: &WaterfallArtist,
3434 plot_area: &Rect,
3435 xmin: f64,
3436 xmax: f64,
3437 ymin: f64,
3438 ymax: f64,
3439 ) {
3440 let n = artist.categories.len();
3441 if n == 0 {
3442 return;
3443 }
3444
3445 let positions = artist.bar_positions();
3446 let cumsum = artist.cumulative_sums();
3447
3448 let cat_range = xmax - xmin;
3449 let cat_step = cat_range / n as f64;
3450 let bar_half = cat_step * artist.bar_width * 0.5;
3451
3452 for i in 0..n {
3453 let (base, top) = positions[i];
3454
3455 let bar_color = if artist.total_indices.contains(&i) {
3457 artist.total_color
3458 } else if artist.values.data[i] >= 0.0 {
3459 artist.increase_color
3460 } else {
3461 artist.decrease_color
3462 };
3463
3464 let color = bar_color.with_alpha((artist.alpha * 255.0) as u8);
3465 let paint = Paint::new(color);
3466
3467 let cat_center = xmin + (i as f64 + 0.5) * cat_step;
3468 let bottom_val = base.min(top);
3469 let top_val = base.max(top);
3470
3471 let p_bl = self.data_to_pixel(
3472 cat_center - bar_half,
3473 bottom_val,
3474 plot_area,
3475 xmin,
3476 xmax,
3477 ymin,
3478 ymax,
3479 );
3480 let p_tr = self.data_to_pixel(
3481 cat_center + bar_half,
3482 top_val,
3483 plot_area,
3484 xmin,
3485 xmax,
3486 ymin,
3487 ymax,
3488 );
3489
3490 let rect = Rect::from_points(p_bl, p_tr);
3491 let bar_path = Path::rect(rect);
3492 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
3493
3494 if artist.show_values {
3496 let display_val = if artist.total_indices.contains(&i) {
3497 cumsum[i]
3498 } else {
3499 artist.values.data[i]
3500 };
3501 let label_text = format_waterfall_value(display_val);
3502
3503 let label_y = if display_val >= 0.0 {
3505 top_val
3506 } else {
3507 bottom_val
3508 };
3509 let label_pos = self.data_to_pixel(
3510 cat_center,
3511 label_y,
3512 plot_area,
3513 xmin,
3514 xmax,
3515 ymin,
3516 ymax,
3517 );
3518
3519 let text_style = TextStyle {
3520 size: 10.0,
3521 color: Color::BLACK,
3522 weight: FontWeight::Normal,
3523 family: None,
3524 halign: HAlign::Center,
3525 valign: if display_val >= 0.0 {
3526 VAlign::Bottom
3527 } else {
3528 VAlign::Top
3529 },
3530 };
3531 let offset_pos = Point::new(
3532 label_pos.x,
3533 if display_val >= 0.0 {
3534 label_pos.y - 3.0
3535 } else {
3536 label_pos.y + 3.0
3537 },
3538 );
3539 renderer.draw_text(&label_text, offset_pos, &text_style, Affine::IDENTITY);
3540 }
3541 }
3542
3543 if artist.connector_lines && n > 1 {
3545 let connector_paint = Paint::new(Color::rgb(0x80, 0x80, 0x80).with_alpha(180));
3546 let connector_stroke = Stroke::new(0.8);
3547
3548 for (i, &connector_y) in cumsum.iter().enumerate().take(n - 1) {
3549 let right_edge = xmin + (i as f64 + 0.5) * cat_step + bar_half;
3550 let left_edge = xmin + ((i + 1) as f64 + 0.5) * cat_step - bar_half;
3551
3552 let p_from = self.data_to_pixel(
3553 right_edge,
3554 connector_y,
3555 plot_area,
3556 xmin,
3557 xmax,
3558 ymin,
3559 ymax,
3560 );
3561 let p_to = self.data_to_pixel(
3562 left_edge,
3563 connector_y,
3564 plot_area,
3565 xmin,
3566 xmax,
3567 ymin,
3568 ymax,
3569 );
3570
3571 let mut path = Path::new();
3572 path.move_to(p_from.x, p_from.y);
3573 path.line_to(p_to.x, p_to.y);
3574 renderer.stroke_path(&path, &connector_paint, &connector_stroke, Affine::IDENTITY);
3575 }
3576 }
3577 }
3578
3579 fn draw_annotations(
3585 &self,
3586 renderer: &mut impl Renderer,
3587 plot_area: &Rect,
3588 xmin: f64,
3589 xmax: f64,
3590 ymin: f64,
3591 ymax: f64,
3592 theme: &Theme,
3593 ) {
3594 for ta in &self.texts {
3596 let pt = self.data_to_pixel(ta.x, ta.y, plot_area, xmin, xmax, ymin, ymax);
3597 let size = ta.fontsize.unwrap_or(theme.axis_label_size);
3598 let color = ta.color.unwrap_or(theme.text_color);
3599 let style = TextStyle {
3600 size,
3601 color,
3602 weight: FontWeight::Normal,
3603 family: theme.font_family.clone(),
3604 halign: ta.ha,
3605 valign: ta.va,
3606 };
3607 if ta.rotation.abs() < f64::EPSILON {
3608 renderer.draw_text(&ta.text, pt, &style, Affine::IDENTITY);
3609 } else {
3610 let angle_rad = -ta.rotation.to_radians();
3611 let rotate = Affine::rotate(angle_rad);
3612 let translate_to = Affine::translate(kurbo::Vec2::new(pt.x, pt.y));
3613 let translate_back = Affine::translate(kurbo::Vec2::new(-pt.x, -pt.y));
3614 let transform = translate_to * rotate * translate_back;
3615 renderer.draw_text(&ta.text, pt, &style, transform);
3616 }
3617 }
3618
3619 for ann in &self.annotations {
3621 let text_pt = self.data_to_pixel(
3622 ann.xytext.0, ann.xytext.1,
3623 plot_area, xmin, xmax, ymin, ymax,
3624 );
3625 let target_pt = self.data_to_pixel(
3626 ann.xy.0, ann.xy.1,
3627 plot_area, xmin, xmax, ymin, ymax,
3628 );
3629
3630 let size = ann.fontsize.unwrap_or(theme.axis_label_size);
3631 let color = ann.color.unwrap_or(theme.text_color);
3632 let style = TextStyle {
3633 size,
3634 color,
3635 weight: FontWeight::Normal,
3636 family: theme.font_family.clone(),
3637 halign: ann.ha,
3638 valign: ann.va,
3639 };
3640 renderer.draw_text(&ann.text, text_pt, &style, Affine::IDENTITY);
3641
3642 if ann.arrowstyle != ArrowStyle::None {
3644 let arrow_col = ann.arrow_color.unwrap_or(color);
3645 self.draw_annotation_arrow(
3646 renderer, text_pt, target_pt, arrow_col, &ann.arrowstyle,
3647 );
3648 }
3649 }
3650 }
3651
3652 fn draw_annotation_arrow(
3654 &self,
3655 renderer: &mut impl Renderer,
3656 from: Point,
3657 to: Point,
3658 color: Color,
3659 style: &ArrowStyle,
3660 ) {
3661 let paint = Paint::new(color);
3662 let stroke = Stroke::new(1.0);
3663
3664 let dx = to.x - from.x;
3666 let dy = to.y - from.y;
3667 let len = (dx * dx + dy * dy).sqrt();
3668 if len < 1e-6 {
3669 return;
3670 }
3671
3672 let ux = dx / len;
3674 let uy = dy / len;
3675
3676 let mut line = Path::new();
3678 line.move_to(from.x, from.y);
3679 line.line_to(to.x, to.y);
3680 renderer.stroke_path(&line, &paint, &stroke, Affine::IDENTITY);
3681
3682 let head_len = match style {
3684 ArrowStyle::None => return,
3685 ArrowStyle::Simple => 8.0,
3686 ArrowStyle::Fancy => 12.0,
3687 };
3688 let head_half_width = match style {
3689 ArrowStyle::None => return,
3690 ArrowStyle::Simple => 3.0,
3691 ArrowStyle::Fancy => 5.0,
3692 };
3693
3694 let px = -uy;
3696 let py = ux;
3697
3698 let base_x = to.x - ux * head_len;
3700 let base_y = to.y - uy * head_len;
3701
3702 let left_x = base_x + px * head_half_width;
3703 let left_y = base_y + py * head_half_width;
3704 let right_x = base_x - px * head_half_width;
3705 let right_y = base_y - py * head_half_width;
3706
3707 let mut arrow = Path::new();
3708 arrow.move_to(to.x, to.y);
3709 arrow.line_to(left_x, left_y);
3710 arrow.line_to(right_x, right_y);
3711 arrow.close();
3712 renderer.fill_path(&arrow, &paint, Affine::IDENTITY);
3713 }
3714
3715 fn draw_pie(
3721 &self,
3722 renderer: &mut impl Renderer,
3723 artist: &PieArtist,
3724 plot_area: &Rect,
3725 xmin: f64,
3726 xmax: f64,
3727 ymin: f64,
3728 ymax: f64,
3729 theme: &Theme,
3730 ) {
3731 let n = artist.sizes.len();
3732 if n == 0 {
3733 return;
3734 }
3735
3736 let total: f64 = artist.sizes.iter().copied().filter(|v| v.is_finite() && *v > 0.0).sum();
3738 if total <= 0.0 {
3739 return;
3740 }
3741 let fractions: Vec<f64> = artist.sizes.iter().map(|&s| {
3742 if s.is_finite() && s > 0.0 { s / total } else { 0.0 }
3743 }).collect();
3744
3745 let center_px = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3747
3748 let edge_px = self.data_to_pixel(artist.radius, 0.0, plot_area, xmin, xmax, ymin, ymax);
3750 let radius_px = (edge_px.x - center_px.x).abs();
3751
3752 let start_rad = artist.start_angle.to_radians();
3753 let mut current_angle = start_rad;
3754
3755 let pct_style = TextStyle {
3756 size: 10.0,
3757 color: Color::BLACK,
3758 weight: FontWeight::Normal,
3759 family: None,
3760 halign: HAlign::Center,
3761 valign: VAlign::Middle,
3762 };
3763 let label_style = TextStyle {
3764 size: 11.0,
3765 color: theme.tick_color,
3766 weight: FontWeight::Normal,
3767 family: None,
3768 halign: HAlign::Center,
3769 valign: VAlign::Middle,
3770 };
3771
3772 for i in 0..n {
3773 let frac = fractions[i];
3774 if frac <= 0.0 {
3775 current_angle += frac * std::f64::consts::TAU;
3776 continue;
3777 }
3778
3779 let sweep = frac * std::f64::consts::TAU;
3780 let mid_angle = current_angle + sweep / 2.0;
3781
3782 let wedge_color = if let Some(ref colors) = artist.colors {
3784 colors[i % colors.len()]
3785 } else {
3786 Color::TABLEAU_10[i % 10]
3787 };
3788
3789 let explode_frac = artist.explode.as_ref().map(|e| {
3791 if i < e.len() { e[i] } else { 0.0 }
3792 }).unwrap_or(0.0);
3793 let offset_x = explode_frac * radius_px * mid_angle.cos();
3794 let offset_y = explode_frac * radius_px * (-mid_angle.sin()); let cx = center_px.x + offset_x;
3797 let cy = center_px.y + offset_y;
3798
3799 let mut path = Path::new();
3802 path.move_to(cx, cy);
3803
3804 let arc_start_x = cx + radius_px * current_angle.cos();
3805 let arc_start_y = cy - radius_px * current_angle.sin();
3806 path.line_to(arc_start_x, arc_start_y);
3807
3808 let max_sub = std::f64::consts::FRAC_PI_2;
3810 let num_segments = (sweep / max_sub).ceil() as usize;
3811 let seg_sweep = sweep / num_segments as f64;
3812 let mut seg_start = current_angle;
3813
3814 for _ in 0..num_segments {
3815 let seg_end = seg_start + seg_sweep;
3816 let half = seg_sweep / 2.0;
3818 let alpha = (4.0 / 3.0) * (half / 2.0).tan();
3819
3820 let p0x = cx + radius_px * seg_start.cos();
3821 let p0y = cy - radius_px * seg_start.sin();
3822 let p3x = cx + radius_px * seg_end.cos();
3823 let p3y = cy - radius_px * seg_end.sin();
3824
3825 let t0x = -seg_start.sin();
3827 let t0y = -seg_start.cos(); let t1x = -seg_end.sin();
3830 let t1y = -seg_end.cos(); let cp1x = p0x + alpha * radius_px * t0x;
3833 let cp1y = p0y + alpha * radius_px * t0y;
3834 let cp2x = p3x - alpha * radius_px * t1x;
3835 let cp2y = p3y - alpha * radius_px * t1y;
3836
3837 let _ = path.curve_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
3838 seg_start = seg_end;
3839 }
3840
3841 path.close();
3842
3843 let paint = Paint::new(wedge_color);
3844 renderer.fill_path(&path, &paint, Affine::IDENTITY);
3845
3846 let outline_paint = Paint::new(Color::WHITE);
3848 let outline_stroke = Stroke::new(1.5);
3849 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
3850
3851 if artist.autopct {
3853 let pct_r = radius_px * 0.6;
3854 let pct_x = cx + pct_r * mid_angle.cos();
3855 let pct_y = cy - pct_r * mid_angle.sin();
3856 let pct_text = format!("{:.1}%", frac * 100.0);
3857 renderer.draw_text(
3858 &pct_text,
3859 Point::new(pct_x, pct_y),
3860 &pct_style,
3861 Affine::IDENTITY,
3862 );
3863 }
3864
3865 if let Some(ref labels) = artist.labels {
3867 if i < labels.len() {
3868 let label_r = radius_px * 1.15;
3869 let lx = cx + label_r * mid_angle.cos();
3870 let ly = cy - label_r * mid_angle.sin();
3871 renderer.draw_text(
3872 &labels[i],
3873 Point::new(lx, ly),
3874 &label_style,
3875 Affine::IDENTITY,
3876 );
3877 }
3878 }
3879
3880 current_angle += sweep;
3881 }
3882 }
3883
3884 fn draw_contour(
3886 &self,
3887 renderer: &mut impl Renderer,
3888 artist: &ContourArtist,
3889 plot_area: &Rect,
3890 xmin: f64,
3891 xmax: f64,
3892 ymin: f64,
3893 ymax: f64,
3894 ) {
3895 let nx = artist.x.len();
3896 let ny = artist.y.len();
3897 if nx < 2 || ny < 2 || artist.z.len() < 2 {
3898 return;
3899 }
3900
3901 let levels = artist.effective_levels();
3902 let (zmin, zmax) = artist.z_bounds();
3903
3904 if artist.filled {
3905 let avgs = artist.cell_averages();
3906 for (j, row) in avgs.iter().enumerate() {
3907 for (i, &avg) in row.iter().enumerate() {
3908 if !avg.is_finite() {
3909 continue;
3910 }
3911 let cell_color = if let Some(ref colors) = artist.colors {
3912 let idx = levels
3913 .iter()
3914 .position(|&l| avg < l)
3915 .unwrap_or(levels.len())
3916 .saturating_sub(1);
3917 colors[idx % colors.len()]
3918 } else {
3919 artist.cmap.map_value(avg, zmin, zmax)
3920 };
3921 let p_bl = self.data_to_pixel(
3922 artist.x[i], artist.y[j],
3923 plot_area, xmin, xmax, ymin, ymax,
3924 );
3925 let p_tr = self.data_to_pixel(
3926 artist.x[i + 1], artist.y[j + 1],
3927 plot_area, xmin, xmax, ymin, ymax,
3928 );
3929 let rect = Rect::from_points(p_bl, p_tr);
3930 let cell_path = Path::rect(rect);
3931 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
3932 }
3933 }
3934 }
3935
3936 if !artist.filled {
3937 for (li, &level) in levels.iter().enumerate() {
3938 let segments = artist.marching_squares(level);
3939 if segments.is_empty() {
3940 continue;
3941 }
3942 let line_color = if let Some(ref colors) = artist.colors {
3943 colors[li % colors.len()]
3944 } else {
3945 artist.cmap.map_value(level, zmin, zmax)
3946 };
3947 let paint = Paint::new(line_color);
3948 let stroke = Stroke::new(artist.linewidths);
3949 for (sx0, sy0, sx1, sy1) in &segments {
3950 let p0 = self.data_to_pixel(*sx0, *sy0, plot_area, xmin, xmax, ymin, ymax);
3951 let p1 = self.data_to_pixel(*sx1, *sy1, plot_area, xmin, xmax, ymin, ymax);
3952 let mut path = Path::new();
3953 path.move_to(p0.x, p0.y);
3954 path.line_to(p1.x, p1.y);
3955 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3956 }
3957 }
3958 }
3959 }
3960
3961 fn draw_violin(
3963 &self,
3964 renderer: &mut impl Renderer,
3965 artist: &ViolinArtist,
3966 plot_area: &Rect,
3967 xmin: f64,
3968 xmax: f64,
3969 ymin: f64,
3970 ymax: f64,
3971 theme: &Theme,
3972 ) {
3973 use crate::charts::violin::{gaussian_kde, silverman_bandwidth};
3974
3975 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
3976 let fill_paint = Paint::new(fill_color);
3977 let outline_paint = Paint::new(artist.color);
3978 let outline_stroke = Stroke::new(1.0);
3979
3980 for (di, data) in artist.datasets.iter().enumerate() {
3981 let mut sorted: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
3982 if sorted.is_empty() {
3983 continue;
3984 }
3985 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
3986
3987 let pos = artist.positions.as_ref()
3988 .and_then(|p| p.get(di).copied())
3989 .unwrap_or(di as f64 + 1.0);
3990
3991 let bw = if artist.bw_method > 0.0 {
3992 artist.bw_method
3993 } else {
3994 silverman_bandwidth(&sorted)
3995 };
3996
3997 let data_min = sorted[0];
3998 let data_max = sorted[sorted.len() - 1];
3999 let n_eval = 100;
4000 let eval_points: Vec<f64> = (0..n_eval)
4001 .map(|i| data_min + (data_max - data_min) * i as f64 / (n_eval - 1) as f64)
4002 .collect();
4003 let densities = gaussian_kde(&sorted, bw, &eval_points);
4004
4005 let max_density = densities.iter().copied().fold(0.0_f64, f64::max);
4006 if max_density <= 0.0 {
4007 continue;
4008 }
4009
4010 let half_width = artist.widths * 0.5;
4011
4012 let mut path = Path::new();
4014 let first_y = eval_points[0];
4016 let first_w = densities[0] / max_density * half_width;
4017 let fp = self.data_to_pixel(pos + first_w, first_y, plot_area, xmin, xmax, ymin, ymax);
4018 path.move_to(fp.x, fp.y);
4019 for i in 1..n_eval {
4020 let w = densities[i] / max_density * half_width;
4021 let p = self.data_to_pixel(pos + w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4022 path.line_to(p.x, p.y);
4023 }
4024 for i in (0..n_eval).rev() {
4026 let w = densities[i] / max_density * half_width;
4027 let p = self.data_to_pixel(pos - w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
4028 path.line_to(p.x, p.y);
4029 }
4030 path.close();
4031
4032 renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
4033 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
4034
4035 let n = sorted.len();
4037 if artist.show_median {
4038 let median = if n % 2 == 0 {
4039 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
4040 } else {
4041 sorted[n / 2]
4042 };
4043 let med_density = gaussian_kde(&sorted, bw, &[median])[0];
4044 let med_w = med_density / max_density * half_width;
4045 let p1 = self.data_to_pixel(pos - med_w, median, plot_area, xmin, xmax, ymin, ymax);
4046 let p2 = self.data_to_pixel(pos + med_w, median, plot_area, xmin, xmax, ymin, ymax);
4047 let mut mp = Path::new();
4048 mp.move_to(p1.x, p1.y);
4049 mp.line_to(p2.x, p2.y);
4050 let med_paint = Paint::new(theme.text_color);
4051 let med_stroke = Stroke::new(2.0);
4052 renderer.stroke_path(&mp, &med_paint, &med_stroke, Affine::IDENTITY);
4053 }
4054
4055 if artist.show_quartiles && n >= 4 {
4056 let q1 = sorted[n / 4];
4057 let q3 = sorted[3 * n / 4];
4058 for q in [q1, q3] {
4059 let q_density = gaussian_kde(&sorted, bw, &[q])[0];
4060 let q_w = q_density / max_density * half_width;
4061 let p1 = self.data_to_pixel(pos - q_w, q, plot_area, xmin, xmax, ymin, ymax);
4062 let p2 = self.data_to_pixel(pos + q_w, q, plot_area, xmin, xmax, ymin, ymax);
4063 let mut qp = Path::new();
4064 qp.move_to(p1.x, p1.y);
4065 qp.line_to(p2.x, p2.y);
4066 let q_stroke = Stroke::new(1.0).with_dash(DashPattern { dashes: vec![4.0, 2.0], offset: 0.0 });
4067 renderer.stroke_path(&qp, &Paint::new(theme.text_color), &q_stroke, Affine::IDENTITY);
4068 }
4069 }
4070 }
4071 }
4072
4073 fn draw_legend(
4074 &self,
4075 renderer: &mut impl Renderer,
4076 plot_area: &Rect,
4077 theme: &Theme,
4078 ) {
4079 let entries: Vec<LegendEntry> = self
4082 .artists
4083 .iter()
4084 .filter_map(|a| {
4085 let (label, color, swatch) = match a {
4086 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4087 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4088 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4089 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4090 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4091 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4092 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4093 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4094 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
4095 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4096 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4097 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4098 Artist::Contour(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
4099 Artist::Polar(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
4100 Artist::Hexbin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4101 Artist::Waterfall(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
4102 };
4103 label.map(|l| LegendEntry { label: l.to_string(), color, swatch })
4104 })
4105 .collect();
4106
4107 legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
4108 }
4109
4110 fn data_to_pixel(
4120 &self,
4121 x: f64,
4122 y: f64,
4123 plot_area: &Rect,
4124 xmin: f64,
4125 xmax: f64,
4126 ymin: f64,
4127 ymax: f64,
4128 ) -> Point {
4129 let tx = self.xscale.transform(x, xmin, xmax);
4130 let ty = self.yscale.transform(y, ymin, ymax);
4131 Point::new(
4132 plot_area.x + tx * plot_area.width,
4133 plot_area.y + (1.0 - ty) * plot_area.height, )
4135 }
4136}
4137
4138fn format_waterfall_value(v: f64) -> String {
4148 if v == v.trunc() && v.abs() < 1e15 {
4149 format!("{}", v as i64)
4150 } else {
4151 let s = format!("{:.2}", v);
4152 s.trim_end_matches('0').trim_end_matches('.').to_string()
4153 }
4154}
4155
4156#[cfg(test)]
4161mod tests {
4162 use super::*;
4163
4164 #[test]
4165 fn new_axes_has_defaults() {
4166 let ax = Axes::new();
4167 assert!(ax.artists.is_empty());
4168 assert!(ax.title.is_none());
4169 assert!(ax.xlabel.is_none());
4170 assert!(ax.ylabel.is_none());
4171 assert!(ax.xlim.is_none());
4172 assert!(ax.ylim.is_none());
4173 assert!(!ax.show_legend);
4174 assert_eq!(ax.color_index, 0);
4175 }
4176
4177 #[test]
4178 fn plot_creates_line_artist() {
4179 let mut ax = Axes::new();
4180 let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
4181 assert!(result.is_ok());
4182 assert_eq!(ax.artists.len(), 1);
4183 assert!(matches!(&ax.artists[0], Artist::Line(_)));
4184 assert_eq!(ax.color_index, 1);
4185 }
4186
4187 #[test]
4188 fn plot_length_mismatch() {
4189 let mut ax = Axes::new();
4190 let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
4191 assert!(matches!(
4192 result,
4193 Err(PlotError::SeriesLengthMismatch { expected: 2, got: 1 })
4194 ));
4195 }
4196
4197 #[test]
4198 fn plot_empty_data() {
4199 let mut ax = Axes::new();
4200 let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
4201 assert!(matches!(result, Err(PlotError::EmptyData)));
4202 }
4203
4204 #[test]
4205 fn scatter_creates_artist() {
4206 let mut ax = Axes::new();
4207 let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
4208 assert!(result.is_ok());
4209 assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
4210 }
4211
4212 #[test]
4213 fn bar_creates_artist() {
4214 let mut ax = Axes::new();
4215 let cats: &[&str] = &["a", "b", "c"];
4216 let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
4217 assert!(result.is_ok());
4218 match &ax.artists[0] {
4219 Artist::Bar(a) => {
4220 assert!(!a.horizontal);
4221 assert_eq!(a.categories.len(), 3);
4222 }
4223 _ => panic!("expected Bar artist"),
4224 }
4225 }
4226
4227 #[test]
4228 fn barh_creates_horizontal_artist() {
4229 let mut ax = Axes::new();
4230 let cats: &[&str] = &["x", "y"];
4231 let result = ax.barh(cats, vec![5.0, 10.0]);
4232 assert!(result.is_ok());
4233 match &ax.artists[0] {
4234 Artist::Bar(a) => assert!(a.horizontal),
4235 _ => panic!("expected Bar artist"),
4236 }
4237 }
4238
4239 #[test]
4240 fn hist_computes_bins() {
4241 let mut ax = Axes::new();
4242 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
4243 let result = ax.hist(data, 5);
4244 assert!(result.is_ok());
4245 match &ax.artists[0] {
4246 Artist::Histogram(a) => {
4247 assert_eq!(a.bin_edges.len(), 6); assert_eq!(a.counts.len(), 5);
4249 let total: f64 = a.counts.iter().sum();
4251 assert_eq!(total, 10.0);
4252 }
4253 _ => panic!("expected Hist artist"),
4254 }
4255 }
4256
4257 #[test]
4258 fn hist_single_value() {
4259 let mut ax = Axes::new();
4260 let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
4261 assert!(result.is_ok());
4262 match &ax.artists[0] {
4263 Artist::Histogram(a) => {
4264 let total: f64 = a.counts.iter().sum();
4265 assert_eq!(total, 3.0);
4266 }
4267 _ => panic!("expected Hist artist"),
4268 }
4269 }
4270
4271 #[test]
4272 fn hist_empty_data() {
4273 let mut ax = Axes::new();
4274 let result = ax.hist(Vec::<f64>::new(), 10);
4275 assert!(matches!(result, Err(PlotError::EmptyData)));
4276 }
4277
4278 #[test]
4279 fn fill_between_creates_artist() {
4280 let mut ax = Axes::new();
4281 let result = ax.fill_between(
4282 vec![1.0, 2.0, 3.0],
4283 vec![1.0, 2.0, 1.0],
4284 vec![0.0, 0.0, 0.0],
4285 );
4286 assert!(result.is_ok());
4287 assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
4288 }
4289
4290 #[test]
4291 fn fill_between_length_mismatch() {
4292 let mut ax = Axes::new();
4293 let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
4294 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
4295 }
4296
4297 #[test]
4298 fn configuration_methods_return_self() {
4299 let mut ax = Axes::new();
4300 ax.set_title("Test")
4301 .set_xlabel("X")
4302 .set_ylabel("Y")
4303 .set_xlim(0.0, 10.0)
4304 .set_ylim(-1.0, 1.0)
4305 .set_xscale(Scale::Linear)
4306 .set_yscale(Scale::Log10)
4307 .grid(true)
4308 .legend();
4309
4310 assert_eq!(ax.title.as_deref(), Some("Test"));
4311 assert_eq!(ax.xlabel.as_deref(), Some("X"));
4312 assert_eq!(ax.ylabel.as_deref(), Some("Y"));
4313 assert_eq!(ax.xlim, Some((0.0, 10.0)));
4314 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
4315 assert_eq!(ax.show_grid, Some(true));
4316 assert!(ax.show_legend);
4317 }
4318
4319 #[test]
4320 fn color_cycle_advances() {
4321 let mut ax = Axes::new();
4322 for _ in 0..12 {
4323 ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4324 }
4325 assert_eq!(ax.color_index, 12);
4326 match (&ax.artists[0], &ax.artists[10]) {
4329 (Artist::Line(a), Artist::Line(b)) => {
4330 assert_eq!(a.color, b.color);
4331 }
4332 _ => panic!("expected Line artists"),
4333 }
4334 }
4335
4336 #[test]
4337 fn data_to_pixel_linear() {
4338 let ax = Axes::new();
4339 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4340
4341 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4343 assert!((p.x - 100.0).abs() < 1e-10);
4344 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);
4348 assert!((p.x - 500.0).abs() < 1e-10);
4349 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);
4353 assert!((p.x - 300.0).abs() < 1e-10);
4354 assert!((p.y - 200.0).abs() < 1e-10);
4355 }
4356
4357 #[test]
4358 fn compute_data_limits_no_artists() {
4359 let ax = Axes::new();
4360 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4361 assert!(xmin < xmax);
4363 assert!(ymin < ymax);
4364 }
4365
4366 #[test]
4367 fn compute_data_limits_with_user_override() {
4368 let mut ax = Axes::new();
4369 ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
4370 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4371 assert!((xmin - (-5.0)).abs() < f64::EPSILON);
4372 assert!((xmax - 5.0).abs() < f64::EPSILON);
4373 assert!((ymin - 0.0).abs() < f64::EPSILON);
4374 assert!((ymax - 100.0).abs() < f64::EPSILON);
4375 }
4376
4377 #[test]
4378 fn compute_data_limits_from_line_data() {
4379 let mut ax = Axes::new();
4380 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4381 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4382 assert!(xmin < 1.0);
4384 assert!(xmax > 10.0);
4385 assert!(ymin < 2.0);
4386 assert!(ymax > 8.0);
4387 }
4388 #[test]
4391 fn step_creates_artist() {
4392 let mut ax = Axes::new();
4393 let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4394 assert!(result.is_ok());
4395 assert!(matches!(&ax.artists[0], Artist::Step(_)));
4396 }
4397
4398 #[test]
4399 fn step_length_mismatch() {
4400 let mut ax = Axes::new();
4401 let result = ax.step(vec![1.0, 2.0], vec![1.0]);
4402 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
4403 }
4404
4405 #[test]
4406 fn step_empty_data() {
4407 let mut ax = Axes::new();
4408 let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
4409 assert!(matches!(result, Err(PlotError::EmptyData)));
4410 }
4411
4412 #[test]
4413 fn step_default_where() {
4414 let mut ax = Axes::new();
4415 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4416 match &ax.artists[0] {
4417 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
4418 _ => panic!("expected Step"),
4419 }
4420 }
4421
4422 #[test]
4423 fn step_color_cycle() {
4424 let mut ax = Axes::new();
4425 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4426 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4427 let c0 = ax.artists[0].color();
4428 let c1 = ax.artists[1].color();
4429 assert_ne!(c0, c1);
4430 }
4431
4432 #[test]
4433 fn step_builder_chaining() {
4434 let mut ax = Axes::new();
4435 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4436 .unwrap()
4437 .color(Color::TAB_RED)
4438 .width(3.0)
4439 .where_step(StepWhere::Post)
4440 .label("steps")
4441 .alpha(0.5);
4442 match &ax.artists[0] {
4443 Artist::Step(a) => {
4444 assert_eq!(a.color, Color::TAB_RED);
4445 assert!((a.width - 3.0).abs() < 1e-12);
4446 assert!(matches!(a.where_step, StepWhere::Post));
4447 assert_eq!(a.label.as_deref(), Some("steps"));
4448 assert!((a.alpha - 0.5).abs() < 1e-12);
4449 }
4450 _ => panic!("expected Step"),
4451 }
4452 }
4453
4454 #[test]
4455 fn step_data_bounds() {
4456 let mut ax = Axes::new();
4457 ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4458 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4459 assert!(xmin < 1.0);
4460 assert!(xmax > 10.0);
4461 assert!(ymin < 2.0);
4462 assert!(ymax > 8.0);
4463 }
4464
4465 #[test]
4466 fn step_legend_label() {
4467 let mut ax = Axes::new();
4468 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
4469 assert_eq!(ax.artists[0].label(), Some("S"));
4470 }
4471
4472 #[test]
4473 fn step_default_alpha() {
4474 let mut ax = Axes::new();
4475 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4476 match &ax.artists[0] {
4477 Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4478 _ => panic!("expected Step"),
4479 }
4480 }
4481
4482 #[test]
4483 fn step_default_width() {
4484 let mut ax = Axes::new();
4485 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4486 match &ax.artists[0] {
4487 Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
4488 _ => panic!("expected Step"),
4489 }
4490 }
4491
4492 #[test]
4493 fn step_mid_mode() {
4494 let mut ax = Axes::new();
4495 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4496 .unwrap()
4497 .where_step(StepWhere::Mid);
4498 match &ax.artists[0] {
4499 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
4500 _ => panic!("expected Step"),
4501 }
4502 }
4503
4504 #[test]
4507 fn stem_creates_artist() {
4508 let mut ax = Axes::new();
4509 let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
4510 assert!(result.is_ok());
4511 assert!(matches!(&ax.artists[0], Artist::Stem(_)));
4512 }
4513
4514 #[test]
4515 fn stem_length_mismatch() {
4516 let mut ax = Axes::new();
4517 let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
4518 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
4519 }
4520
4521 #[test]
4522 fn stem_empty_data() {
4523 let mut ax = Axes::new();
4524 let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
4525 assert!(matches!(result, Err(PlotError::EmptyData)));
4526 }
4527
4528 #[test]
4529 fn stem_default_baseline() {
4530 let mut ax = Axes::new();
4531 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4532 match &ax.artists[0] {
4533 Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
4534 _ => panic!("expected Stem"),
4535 }
4536 }
4537
4538 #[test]
4539 fn stem_default_marker_size() {
4540 let mut ax = Axes::new();
4541 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4542 match &ax.artists[0] {
4543 Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
4544 _ => panic!("expected Stem"),
4545 }
4546 }
4547
4548 #[test]
4549 fn stem_builder_chaining() {
4550 let mut ax = Axes::new();
4551 ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
4552 .unwrap()
4553 .color(Color::TAB_GREEN)
4554 .baseline(1.0)
4555 .marker_size(8.0)
4556 .width(2.0)
4557 .label("stems")
4558 .alpha(0.7);
4559 match &ax.artists[0] {
4560 Artist::Stem(a) => {
4561 assert_eq!(a.color, Color::TAB_GREEN);
4562 assert!((a.baseline - 1.0).abs() < 1e-12);
4563 assert!((a.marker_size - 8.0).abs() < 1e-12);
4564 assert!((a.line_width - 2.0).abs() < 1e-12);
4565 assert_eq!(a.label.as_deref(), Some("stems"));
4566 assert!((a.alpha - 0.7).abs() < 1e-12);
4567 }
4568 _ => panic!("expected Stem"),
4569 }
4570 }
4571
4572 #[test]
4573 fn stem_data_bounds_include_baseline() {
4574 let mut ax = Axes::new();
4575 ax.stem(vec![1.0, 5.0], vec![2.0, 8.0]).unwrap().baseline(-5.0);
4576 let (_xmin, _xmax, ymin, _ymax) = ax.compute_data_limits();
4577 assert!(ymin < -5.0);
4578 }
4579
4580 #[test]
4581 fn stem_legend_label() {
4582 let mut ax = Axes::new();
4583 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
4584 assert_eq!(ax.artists[0].label(), Some("L"));
4585 }
4586
4587 #[test]
4588 fn stem_color_cycle() {
4589 let mut ax = Axes::new();
4590 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4591 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4592 let c0 = ax.artists[0].color();
4593 let c1 = ax.artists[1].color();
4594 assert_ne!(c0, c1);
4595 }
4596
4597 #[test]
4598 fn stem_alpha_default() {
4599 let mut ax = Axes::new();
4600 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
4601 match &ax.artists[0] {
4602 Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
4603 _ => panic!("expected Stem"),
4604 }
4605 }
4606
4607 #[test]
4608 fn stem_negative_baseline() {
4609 let mut ax = Axes::new();
4610 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().baseline(-3.0);
4611 match &ax.artists[0] {
4612 Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
4613 _ => panic!("expected Stem"),
4614 }
4615 }
4616
4617 #[test]
4620 fn text_creates_annotation() {
4621 let mut ax = Axes::new();
4622 ax.text(1.0, 2.0, "hello");
4623 assert_eq!(ax.texts.len(), 1);
4624 assert_eq!(ax.texts[0].text, "hello");
4625 assert!((ax.texts[0].x - 1.0).abs() < f64::EPSILON);
4626 assert!((ax.texts[0].y - 2.0).abs() < f64::EPSILON);
4627 }
4628
4629 #[test]
4630 fn text_default_alignment() {
4631 let mut ax = Axes::new();
4632 ax.text(0.0, 0.0, "test");
4633 assert_eq!(ax.texts[0].ha, HAlign::Left);
4634 assert_eq!(ax.texts[0].va, VAlign::Baseline);
4635 assert!((ax.texts[0].rotation - 0.0).abs() < f64::EPSILON);
4636 }
4637
4638 #[test]
4639 fn text_builder_chaining() {
4640 let mut ax = Axes::new();
4641 ax.text(1.0, 2.0, "styled")
4642 .fontsize(14.0)
4643 .color(Color::TAB_RED)
4644 .ha(HAlign::Center)
4645 .va(VAlign::Top)
4646 .rotation(45.0);
4647 let t = &ax.texts[0];
4648 assert_eq!(t.fontsize, Some(14.0));
4649 assert_eq!(t.color, Some(Color::TAB_RED));
4650 assert_eq!(t.ha, HAlign::Center);
4651 assert_eq!(t.va, VAlign::Top);
4652 assert!((t.rotation - 45.0).abs() < f64::EPSILON);
4653 }
4654
4655 #[test]
4656 fn text_does_not_affect_autoscale() {
4657 let mut ax = Axes::new();
4658 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4659 let limits_before = ax.compute_data_limits();
4660 ax.text(100.0, 100.0, "far away");
4662 let limits_after = ax.compute_data_limits();
4663 assert_eq!(limits_before, limits_after);
4664 }
4665
4666 #[test]
4667 fn multiple_texts() {
4668 let mut ax = Axes::new();
4669 ax.text(1.0, 1.0, "first");
4670 ax.text(2.0, 2.0, "second");
4671 ax.text(3.0, 3.0, "third");
4672 assert_eq!(ax.texts.len(), 3);
4673 assert_eq!(ax.texts[0].text, "first");
4674 assert_eq!(ax.texts[1].text, "second");
4675 assert_eq!(ax.texts[2].text, "third");
4676 }
4677
4678 #[test]
4681 fn annotate_creates_annotation() {
4682 let mut ax = Axes::new();
4683 ax.annotate("peak", (1.0, 2.0), (3.0, 4.0));
4684 assert_eq!(ax.annotations.len(), 1);
4685 assert_eq!(ax.annotations[0].text, "peak");
4686 assert_eq!(ax.annotations[0].xy, (1.0, 2.0));
4687 assert_eq!(ax.annotations[0].xytext, (3.0, 4.0));
4688 }
4689
4690 #[test]
4691 fn annotate_default_no_arrow() {
4692 let mut ax = Axes::new();
4693 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4694 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::None);
4695 }
4696
4697 #[test]
4698 fn annotate_default_alignment() {
4699 let mut ax = Axes::new();
4700 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4701 assert_eq!(ax.annotations[0].ha, HAlign::Center);
4702 assert_eq!(ax.annotations[0].va, VAlign::Bottom);
4703 }
4704
4705 #[test]
4706 fn annotate_with_arrow() {
4707 let mut ax = Axes::new();
4708 ax.annotate("peak", (1.0, 1.0), (2.0, 2.0))
4709 .arrowstyle(ArrowStyle::Simple);
4710 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Simple);
4711 }
4712
4713 #[test]
4714 fn annotate_with_fancy_arrow() {
4715 let mut ax = Axes::new();
4716 ax.annotate("label", (0.0, 0.0), (1.0, 1.0))
4717 .arrowstyle(ArrowStyle::Fancy);
4718 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Fancy);
4719 }
4720
4721 #[test]
4722 fn annotate_builder_chaining() {
4723 let mut ax = Axes::new();
4724 ax.annotate("note", (1.0, 2.0), (3.0, 4.0))
4725 .fontsize(12.0)
4726 .color(Color::TAB_BLUE)
4727 .ha(HAlign::Right)
4728 .va(VAlign::Top)
4729 .arrowstyle(ArrowStyle::Fancy)
4730 .arrow_color(Color::TAB_RED);
4731 let a = &ax.annotations[0];
4732 assert_eq!(a.fontsize, Some(12.0));
4733 assert_eq!(a.color, Some(Color::TAB_BLUE));
4734 assert_eq!(a.ha, HAlign::Right);
4735 assert_eq!(a.va, VAlign::Top);
4736 assert_eq!(a.arrowstyle, ArrowStyle::Fancy);
4737 assert_eq!(a.arrow_color, Some(Color::TAB_RED));
4738 }
4739
4740 #[test]
4741 fn annotate_does_not_affect_autoscale() {
4742 let mut ax = Axes::new();
4743 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4744 let limits_before = ax.compute_data_limits();
4745 ax.annotate("far", (100.0, 100.0), (200.0, 200.0));
4747 let limits_after = ax.compute_data_limits();
4748 assert_eq!(limits_before, limits_after);
4749 }
4750
4751 #[test]
4752 fn multiple_annotations() {
4753 let mut ax = Axes::new();
4754 ax.annotate("a", (0.0, 0.0), (1.0, 1.0));
4755 ax.annotate("b", (2.0, 2.0), (3.0, 3.0));
4756 assert_eq!(ax.annotations.len(), 2);
4757 }
4758
4759 #[test]
4760 fn text_at_plot_boundary() {
4761 let mut ax = Axes::new();
4762 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4763 ax.text(0.0, 0.0, "origin");
4765 ax.text(10.0, 10.0, "corner");
4766 assert_eq!(ax.texts.len(), 2);
4767 }
4768
4769 #[test]
4770 fn overlapping_annotations() {
4771 let mut ax = Axes::new();
4772 ax.annotate("one", (5.0, 5.0), (6.0, 6.0));
4774 ax.annotate("two", (5.0, 5.0), (6.0, 6.0));
4775 ax.text(6.0, 6.0, "three");
4776 assert_eq!(ax.annotations.len(), 2);
4777 assert_eq!(ax.texts.len(), 1);
4778 }
4779
4780 #[test]
4781 fn new_axes_has_empty_annotations() {
4782 let ax = Axes::new();
4783 assert!(ax.texts.is_empty());
4784 assert!(ax.annotations.is_empty());
4785 }
4786
4787 #[test]
4788 fn text_pixel_placement() {
4789 let ax = Axes::new();
4791 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4792 let pt = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4794 assert!((pt.x - 300.0).abs() < 1e-10);
4795 assert!((pt.y - 200.0).abs() < 1e-10);
4796 }
4797
4798 #[test]
4799 fn annotation_pixel_placement() {
4800 let ax = Axes::new();
4802 let plot_area = Rect::new(0.0, 0.0, 100.0, 100.0);
4803 let target = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4804 let text_pos = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4805 assert!((target.x - 0.0).abs() < 1e-10);
4807 assert!((target.y - 100.0).abs() < 1e-10);
4808 assert!((text_pos.x - 50.0).abs() < 1e-10);
4810 assert!((text_pos.y - 50.0).abs() < 1e-10);
4811 }
4812
4813 #[test]
4816 fn set_xlim_overrides_autoscale() {
4817 let mut ax = Axes::new();
4818 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4819 ax.set_xlim(2.0, 8.0);
4820 let (xmin, xmax, _, _) = ax.compute_data_limits();
4821 assert!((xmin - 2.0).abs() < f64::EPSILON);
4822 assert!((xmax - 8.0).abs() < f64::EPSILON);
4823 }
4824
4825 #[test]
4826 fn set_ylim_overrides_autoscale() {
4827 let mut ax = Axes::new();
4828 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4829 ax.set_ylim(-5.0, 15.0);
4830 let (_, _, ymin, ymax) = ax.compute_data_limits();
4831 assert!((ymin - (-5.0)).abs() < f64::EPSILON);
4832 assert!((ymax - 15.0).abs() < f64::EPSILON);
4833 }
4834
4835 #[test]
4836 fn set_xlim_with_min_greater_than_max() {
4837 let mut ax = Axes::new();
4838 ax.set_xlim(10.0, 2.0);
4839 let (xmin, xmax, _, _) = ax.compute_data_limits();
4840 assert!((xmin - 10.0).abs() < f64::EPSILON);
4842 assert!((xmax - 2.0).abs() < f64::EPSILON);
4843 }
4844
4845 #[test]
4846 fn invert_xaxis_swaps_limits() {
4847 let mut ax = Axes::new();
4848 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4849 ax.set_xlim(0.0, 10.0);
4850 ax.invert_xaxis();
4851 let (xmin, xmax, _, _) = ax.compute_data_limits();
4852 assert!((xmin - 10.0).abs() < f64::EPSILON);
4854 assert!((xmax - 0.0).abs() < f64::EPSILON);
4855 }
4856
4857 #[test]
4858 fn invert_yaxis_swaps_limits() {
4859 let mut ax = Axes::new();
4860 ax.set_ylim(0.0, 100.0);
4861 ax.invert_yaxis();
4862 let (_, _, ymin, ymax) = ax.compute_data_limits();
4863 assert!((ymin - 100.0).abs() < f64::EPSILON);
4864 assert!((ymax - 0.0).abs() < f64::EPSILON);
4865 }
4866
4867 #[test]
4868 fn custom_xticks_appear_in_output() {
4869 let mut ax = Axes::new();
4870 ax.set_xticks(&[1.0, 2.0, 3.0]);
4871 let ticks = ax.resolve_xticks(0.0, 10.0);
4872 assert_eq!(ticks.len(), 3);
4873 assert!((ticks[0].value - 1.0).abs() < f64::EPSILON);
4874 assert!((ticks[1].value - 2.0).abs() < f64::EPSILON);
4875 assert!((ticks[2].value - 3.0).abs() < f64::EPSILON);
4876 }
4877
4878 #[test]
4879 fn custom_yticks_appear_in_output() {
4880 let mut ax = Axes::new();
4881 ax.set_yticks(&[0.0, 50.0, 100.0]);
4882 let ticks = ax.resolve_yticks(0.0, 100.0);
4883 assert_eq!(ticks.len(), 3);
4884 assert!((ticks[0].value - 0.0).abs() < f64::EPSILON);
4885 assert!((ticks[1].value - 50.0).abs() < f64::EPSILON);
4886 assert!((ticks[2].value - 100.0).abs() < f64::EPSILON);
4887 }
4888
4889 #[test]
4890 fn custom_tick_labels_override_default_format() {
4891 let mut ax = Axes::new();
4892 ax.set_xticks(&[0.0, 3.125, 6.25]);
4893 ax.set_xticklabels(&["0", "pi", "2pi"]);
4894 let ticks = ax.resolve_xticks(0.0, 7.0);
4895 assert_eq!(ticks.len(), 3);
4896 assert_eq!(ticks[0].label, "0");
4897 assert_eq!(ticks[1].label, "pi");
4898 assert_eq!(ticks[2].label, "2pi");
4899 }
4900
4901 #[test]
4902 fn custom_tick_labels_partial_match() {
4903 let mut ax = Axes::new();
4905 ax.set_xticks(&[1.0, 2.0, 3.0]);
4906 ax.set_xticklabels(&["one"]);
4907 let ticks = ax.resolve_xticks(0.0, 5.0);
4908 assert_eq!(ticks[0].label, "one");
4909 assert_eq!(ticks[1].label, "2");
4910 assert_eq!(ticks[2].label, "3");
4911 }
4912
4913 #[test]
4914 fn empty_custom_ticks() {
4915 let mut ax = Axes::new();
4916 ax.set_xticks(&[]);
4917 let ticks = ax.resolve_xticks(0.0, 10.0);
4918 assert!(ticks.is_empty());
4919 }
4920
4921 #[test]
4922 fn grid_visibility_toggle() {
4923 let mut ax = Axes::new();
4924 assert!(ax.show_grid.is_none());
4925 ax.grid(true);
4926 assert_eq!(ax.show_grid, Some(true));
4927 ax.grid(false);
4928 assert_eq!(ax.show_grid, Some(false));
4929 }
4930
4931 #[test]
4932 fn grid_axis_setting() {
4933 let mut ax = Axes::new();
4934 assert_eq!(ax.grid_axis, GridAxis::Both);
4935 ax.grid_axis("x");
4936 assert_eq!(ax.grid_axis, GridAxis::X);
4937 ax.grid_axis("y");
4938 assert_eq!(ax.grid_axis, GridAxis::Y);
4939 ax.grid_axis("both");
4940 assert_eq!(ax.grid_axis, GridAxis::Both);
4941 }
4942
4943 #[test]
4944 fn grid_alpha_setting() {
4945 let mut ax = Axes::new();
4946 ax.grid_alpha(0.5);
4947 assert!((ax.grid_alpha.unwrap() - 0.5).abs() < f64::EPSILON);
4948 }
4949
4950 #[test]
4951 fn grid_alpha_clamps() {
4952 let mut ax = Axes::new();
4953 ax.grid_alpha(2.0);
4954 assert!((ax.grid_alpha.unwrap() - 1.0).abs() < f64::EPSILON);
4955 ax.grid_alpha(-0.5);
4956 assert!((ax.grid_alpha.unwrap() - 0.0).abs() < f64::EPSILON);
4957 }
4958
4959 #[test]
4960 fn grid_style_setting() {
4961 let mut ax = Axes::new();
4962 ax.grid_style(crate::theme::LineStyle::Dashed);
4963 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dashed));
4964 }
4965
4966 #[test]
4967 fn tick_rotation_setting() {
4968 let mut ax = Axes::new();
4969 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
4970 ax.tick_params_x_rotation(45.0);
4971 assert!((ax.xtick_rotation - 45.0).abs() < f64::EPSILON);
4972 ax.tick_params_y_rotation(-30.0);
4973 assert!((ax.ytick_rotation - (-30.0)).abs() < f64::EPSILON);
4974 }
4975
4976 #[test]
4977 fn axis_control_chaining() {
4978 let mut ax = Axes::new();
4979 ax.set_xlim(0.0, 10.0)
4980 .set_ylim(-1.0, 1.0)
4981 .invert_xaxis()
4982 .grid(true)
4983 .grid_axis("y")
4984 .grid_alpha(0.3)
4985 .grid_style(crate::theme::LineStyle::Dotted)
4986 .set_xticks(&[0.0, 5.0, 10.0])
4987 .set_xticklabels(&["start", "mid", "end"])
4988 .tick_params_x_rotation(90.0);
4989
4990 assert_eq!(ax.xlim, Some((0.0, 10.0)));
4991 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
4992 assert!(ax.x_inverted);
4993 assert_eq!(ax.show_grid, Some(true));
4994 assert_eq!(ax.grid_axis, GridAxis::Y);
4995 assert!((ax.grid_alpha.unwrap() - 0.3).abs() < f64::EPSILON);
4996 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dotted));
4997 assert_eq!(ax.custom_xticks.as_ref().unwrap().len(), 3);
4998 assert_eq!(ax.custom_xticklabels.as_ref().unwrap().len(), 3);
4999 assert!((ax.xtick_rotation - 90.0).abs() < f64::EPSILON);
5000 }
5001
5002 #[test]
5003 fn new_axes_has_axis_control_defaults() {
5004 let ax = Axes::new();
5005 assert_eq!(ax.grid_axis, GridAxis::Both);
5006 assert!(ax.grid_alpha.is_none());
5007 assert!(ax.grid_style.is_none());
5008 assert!(!ax.x_inverted);
5009 assert!(!ax.y_inverted);
5010 assert!(ax.custom_xticks.is_none());
5011 assert!(ax.custom_yticks.is_none());
5012 assert!(ax.custom_xticklabels.is_none());
5013 assert!(ax.custom_yticklabels.is_none());
5014 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
5015 assert!((ax.ytick_rotation - 0.0).abs() < f64::EPSILON);
5016 }
5017
5018 #[test]
5019 fn autoscale_not_broken_without_user_limits() {
5020 let mut ax = Axes::new();
5021 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
5022 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5023 assert!(xmin < 1.0);
5025 assert!(xmax > 10.0);
5026 assert!(ymin < 2.0);
5027 assert!(ymax > 8.0);
5028 }
5029
5030 #[test]
5031 fn resolve_xticks_auto_when_not_set() {
5032 let ax = Axes::new();
5033 let ticks = ax.resolve_xticks(0.0, 10.0);
5034 assert!(!ticks.is_empty());
5036 }
5037
5038 #[test]
5039 fn resolve_yticks_auto_when_not_set() {
5040 let ax = Axes::new();
5041 let ticks = ax.resolve_yticks(0.0, 100.0);
5042 assert!(!ticks.is_empty());
5043 }
5044
5045 #[test]
5048 fn data_to_pixel_log10() {
5049 let mut ax = Axes::new();
5050 ax.set_xscale(Scale::Log10);
5051 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
5052
5053 let p = ax.data_to_pixel(1.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5055 assert!((p.x - 100.0).abs() < 1e-6, "log10(1)=0 should be left edge, got {}", p.x);
5056
5057 let p = ax.data_to_pixel(1000.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5059 assert!((p.x - 500.0).abs() < 1e-6, "log10(1000)=3 should be right edge, got {}", p.x);
5060
5061 let p = ax.data_to_pixel(10.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5063 let expected_x = 100.0 + 400.0 / 3.0;
5064 assert!((p.x - expected_x).abs() < 1e-6, "log10(10)=1/3 of range, expected {}, got {}", expected_x, p.x);
5065
5066 let p = ax.data_to_pixel(100.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
5068 let expected_x = 100.0 + 400.0 * 2.0 / 3.0;
5069 assert!((p.x - expected_x).abs() < 1e-6, "log10(100)=2/3 of range, expected {}, got {}", expected_x, p.x);
5070 }
5071
5072 #[test]
5073 fn data_to_pixel_log10_y() {
5074 let mut ax = Axes::new();
5075 ax.set_yscale(Scale::Log10);
5076 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5077
5078 let p = ax.data_to_pixel(0.0, 10.0, &plot_area, 0.0, 10.0, 1.0, 100.0);
5080 assert!((p.y - 150.0).abs() < 1e-6, "log10(10) should be vertical center, got {}", p.y);
5083 }
5084
5085 #[test]
5086 fn compute_data_limits_log_clamps_positive() {
5087 let mut ax = Axes::new();
5088 ax.set_xscale(Scale::Log10);
5089 ax.set_yscale(Scale::Log10);
5090 ax.plot(vec![0.1, 1.0, 10.0], vec![1.0, 10.0, 100.0]).unwrap();
5091 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5092 assert!(xmin > 0.0, "log x-min must be positive, got {}", xmin);
5093 assert!(xmax > xmin, "log x-max must be > x-min");
5094 assert!(ymin > 0.0, "log y-min must be positive, got {}", ymin);
5095 assert!(ymax > ymin, "log y-max must be > y-min");
5096 }
5097
5098 #[test]
5099 fn compute_data_limits_log_with_zeros() {
5100 let mut ax = Axes::new();
5102 ax.set_xscale(Scale::Log10);
5103 ax.plot(vec![0.0, 1.0, 10.0], vec![1.0, 2.0, 3.0]).unwrap();
5104 let (xmin, xmax, _ymin, _ymax) = ax.compute_data_limits();
5105 assert!(xmin > 0.0, "log x-min should be clamped positive, got {}", xmin);
5106 assert!(xmax > xmin);
5107 }
5108
5109 #[test]
5110 fn data_to_pixel_symlog() {
5111 let mut ax = Axes::new();
5112 ax.set_xscale(Scale::SymLog { linthresh: 1.0 });
5113 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
5114
5115 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, -100.0, 100.0, 0.0, 1.0);
5117 assert!((p.x - 200.0).abs() < 1e-6, "symlog(0) should be center for symmetric range, got {}", p.x);
5118 }
5119
5120 #[test]
5121 fn set_scale_methods_return_self() {
5122 let mut ax = Axes::new();
5123 ax.set_xscale(Scale::Log10)
5124 .set_yscale(Scale::Log10)
5125 .set_title("Log plot");
5126 assert!(matches!(ax.xscale, Scale::Log10));
5127 assert!(matches!(ax.yscale, Scale::Log10));
5128 assert_eq!(ax.title.as_deref(), Some("Log plot"));
5129 }
5130
5131 #[test]
5132 fn compute_data_limits_log_no_artists() {
5133 let mut ax = Axes::new();
5134 ax.set_xscale(Scale::Log10);
5135 ax.set_yscale(Scale::Log10);
5136 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
5137 assert!(xmin > 0.0, "default log x-min must be positive");
5138 assert!(xmax > xmin);
5139 assert!(ymin > 0.0, "default log y-min must be positive");
5140 assert!(ymax > ymin);
5141 }
5142
5143 #[test]
5144 fn data_to_pixel_log10_very_large_range() {
5145 let mut ax = Axes::new();
5146 ax.set_xscale(Scale::Log10);
5147 let plot_area = Rect::new(0.0, 0.0, 1000.0, 100.0);
5148
5149 let p_lo = ax.data_to_pixel(1e-5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5151 let p_hi = ax.data_to_pixel(1e5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
5152 assert!((p_lo.x - 0.0).abs() < 1e-6);
5153 assert!((p_hi.x - 1000.0).abs() < 1e-6);
5154 }
5155
5156 #[test]
5161 fn new_axes_has_no_twin_fields() {
5162 let ax = Axes::new();
5163 assert!(!ax.is_twin());
5164 assert_eq!(ax.twin_side(), None);
5165 }
5166
5167 #[test]
5172 fn colorbar_attaches_to_axes() {
5173 let mut ax = Axes::new();
5174 let cb = ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5175 cb.set_label("Test");
5176 assert!(ax.colorbar.is_some());
5177 assert_eq!(ax.colorbar.as_ref().unwrap().label.as_deref(), Some("Test"));
5178 }
5179
5180 #[test]
5181 fn colorbar_replaces_previous() {
5182 let mut ax = Axes::new();
5183 ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
5184 ax.colorbar(crate::colormap::Colormap::Plasma, -10.0, 10.0);
5185 let cb = ax.colorbar.as_ref().unwrap();
5186 assert_eq!(cb.cmap, crate::colormap::Colormap::Plasma);
5187 assert!((cb.vmin - (-10.0)).abs() < f64::EPSILON);
5188 assert!((cb.vmax - 10.0).abs() < f64::EPSILON);
5189 }
5190
5191 #[test]
5192 fn heatmap_auto_colorbar() {
5193 let mut ax = Axes::new();
5194 ax.heatmap(vec![vec![1.0, 2.0], vec![3.0, 4.0]])
5195 .unwrap()
5196 .colorbar(true);
5197 let auto_cb = ax.auto_colorbar_from_artists();
5198 assert!(auto_cb.is_some());
5199 let cb = auto_cb.unwrap();
5200 assert!((cb.vmin - 1.0).abs() < f64::EPSILON);
5201 assert!((cb.vmax - 4.0).abs() < f64::EPSILON);
5202 }
5203
5204 #[test]
5205 fn heatmap_no_auto_colorbar_by_default() {
5206 let mut ax = Axes::new();
5207 ax.heatmap(vec![vec![1.0, 2.0]]).unwrap();
5208 let auto_cb = ax.auto_colorbar_from_artists();
5209 assert!(auto_cb.is_none());
5210 }
5211
5212 #[test]
5213 fn new_axes_has_no_colorbar() {
5214 let ax = Axes::new();
5215 assert!(ax.colorbar.is_none());
5216 }
5217}