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 };
225 self.artists.push(Artist::Line(artist));
226 match self.artists.last_mut().expect("just pushed") {
227 Artist::Line(a) => Ok(a),
228 _ => unreachable!(),
229 }
230 }
231
232 pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
241 where
242 X: IntoSeries,
243 Y: IntoSeries,
244 {
245 let xs = x.into_series();
246 let ys = y.into_series();
247 if xs.len() != ys.len() {
248 return Err(PlotError::SeriesLengthMismatch {
249 expected: xs.len(),
250 got: ys.len(),
251 });
252 }
253 if xs.is_empty() {
254 return Err(PlotError::EmptyData);
255 }
256 let color = Color::TABLEAU_10[self.color_index % 10];
257 self.color_index += 1;
258 let artist = ScatterArtist {
259 x: xs,
260 y: ys,
261 color,
262 marker: Marker::Circle,
263 size: 6.0,
264 label: None,
265 alpha: 0.8,
266 colors: None,
267 c: None,
268 cmap: None,
269 };
270 self.artists.push(Artist::Scatter(artist));
271 match self.artists.last_mut().expect("just pushed") {
272 Artist::Scatter(a) => Ok(a),
273 _ => unreachable!(),
274 }
275 }
276
277 pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
287 where
288 C: IntoCategories,
289 H: IntoSeries,
290 {
291 let cats = categories.into_categories();
292 let vals = heights.into_series();
293 if cats.len() != vals.len() {
294 return Err(PlotError::SeriesLengthMismatch {
295 expected: cats.len(),
296 got: vals.len(),
297 });
298 }
299 if cats.is_empty() {
300 return Err(PlotError::EmptyData);
301 }
302 let color = Color::TABLEAU_10[self.color_index % 10];
303 self.color_index += 1;
304 let artist = BarArtist {
305 categories: cats,
306 heights: vals,
307 color,
308 horizontal: false,
309 bar_width: 0.8,
310 label: None,
311 alpha: 1.0,
312 bottom: None,
313 offset: None,
314 };
315 self.artists.push(Artist::Bar(artist));
316 match self.artists.last_mut().expect("just pushed") {
317 Artist::Bar(a) => Ok(a),
318 _ => unreachable!(),
319 }
320 }
321
322 pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
331 where
332 C: IntoCategories,
333 W: IntoSeries,
334 {
335 let cats = categories.into_categories();
336 let vals = widths.into_series();
337 if cats.len() != vals.len() {
338 return Err(PlotError::SeriesLengthMismatch {
339 expected: cats.len(),
340 got: vals.len(),
341 });
342 }
343 if cats.is_empty() {
344 return Err(PlotError::EmptyData);
345 }
346 let color = Color::TABLEAU_10[self.color_index % 10];
347 self.color_index += 1;
348 let artist = BarArtist {
349 categories: cats,
350 heights: vals,
351 color,
352 horizontal: true,
353 bar_width: 0.8,
354 label: None,
355 alpha: 1.0,
356 bottom: None,
357 offset: None,
358 };
359 self.artists.push(Artist::Bar(artist));
360 match self.artists.last_mut().expect("just pushed") {
361 Artist::Bar(a) => Ok(a),
362 _ => unreachable!(),
363 }
364 }
365
366 pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
376 where
377 D: IntoSeries,
378 {
379 let series = data.into_series();
380 if series.is_empty() {
381 return Err(PlotError::EmptyData);
382 }
383 let bins = bins.max(1);
384
385 let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
387
388 let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
390 (data_min - 0.5, data_max + 0.5)
391 } else {
392 (data_min, data_max)
393 };
394
395 let bin_width = (hi - lo) / bins as f64;
396
397 let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
399 *edges.last_mut().expect("edges is non-empty") = hi;
401
402 let mut counts = vec![0.0f64; bins];
404 for &v in &series.data {
405 if !v.is_finite() {
406 continue;
407 }
408 let idx = if v >= hi {
410 bins - 1
412 } else {
413 let raw = ((v - lo) / bin_width) as usize;
414 raw.min(bins - 1)
415 };
416 counts[idx] += 1.0;
417 }
418
419 let color = Color::TABLEAU_10[self.color_index % 10];
420 self.color_index += 1;
421 let artist = HistArtist {
422 data: series,
423 bins,
424 bin_edges: edges,
425 counts,
426 color,
427 label: None,
428 alpha: 0.85,
429 density: false,
430 };
431
432
433
434
435
436
437
438 self.artists.push(Artist::Histogram(artist));
439 match self.artists.last_mut().expect("just pushed") {
440 Artist::Histogram(a) => Ok(a),
441 _ => unreachable!(),
442 }
443 }
444
445 pub fn fill_between<X, Y1, Y2>(
454 &mut self,
455 x: X,
456 y1: Y1,
457 y2: Y2,
458 ) -> Result<&mut FillBetweenArtist>
459 where
460 X: IntoSeries,
461 Y1: IntoSeries,
462 Y2: IntoSeries,
463 {
464 let xs = x.into_series();
465 let y1s = y1.into_series();
466 let y2s = y2.into_series();
467 if xs.len() != y1s.len() {
468 return Err(PlotError::SeriesLengthMismatch {
469 expected: xs.len(),
470 got: y1s.len(),
471 });
472 }
473 if xs.len() != y2s.len() {
474 return Err(PlotError::SeriesLengthMismatch {
475 expected: xs.len(),
476 got: y2s.len(),
477 });
478 }
479 if xs.is_empty() {
480 return Err(PlotError::EmptyData);
481 }
482 let color = Color::TABLEAU_10[self.color_index % 10];
483 self.color_index += 1;
484 let artist = FillBetweenArtist {
485 x: xs,
486 y1: y1s,
487 y2: y2s,
488 color,
489 label: None,
490 alpha: 0.3,
491 };
492 self.artists.push(Artist::FillBetween(artist));
493 match self.artists.last_mut().expect("just pushed") {
494 Artist::FillBetween(a) => Ok(a),
495 _ => unreachable!(),
496 }
497 }
498
499 pub fn step<X: IntoSeries, Y: IntoSeries>(
508 &mut self,
509 x: X,
510 y: Y,
511 ) -> Result<&mut StepArtist> {
512 let xs = x.into_series();
513 let ys = y.into_series();
514 if xs.len() != ys.len() {
515 return Err(PlotError::SeriesLengthMismatch {
516 expected: xs.len(),
517 got: ys.len(),
518 });
519 }
520 if xs.is_empty() {
521 return Err(PlotError::EmptyData);
522 }
523 let color = Color::TABLEAU_10[self.color_index % 10];
524 self.color_index += 1;
525 let artist = StepArtist {
526 x: xs,
527 y: ys,
528 color,
529 width: 1.5,
530 where_step: StepWhere::Pre,
531 label: None,
532 alpha: 1.0,
533 };
534 self.artists.push(Artist::Step(artist));
535 match self.artists.last_mut().expect("just pushed") {
536 Artist::Step(a) => Ok(a),
537 _ => unreachable!(),
538 }
539 }
540
541 pub fn stem<X: IntoSeries, Y: IntoSeries>(
550 &mut self,
551 x: X,
552 y: Y,
553 ) -> Result<&mut StemArtist> {
554 let xs = x.into_series();
555 let ys = y.into_series();
556 if xs.len() != ys.len() {
557 return Err(PlotError::SeriesLengthMismatch {
558 expected: xs.len(),
559 got: ys.len(),
560 });
561 }
562 if xs.is_empty() {
563 return Err(PlotError::EmptyData);
564 }
565 let color = Color::TABLEAU_10[self.color_index % 10];
566 self.color_index += 1;
567 let artist = StemArtist {
568 x: xs,
569 y: ys,
570 color,
571 line_width: 1.5,
572 marker_size: 6.0,
573 baseline: 0.0,
574 label: None,
575 alpha: 1.0,
576 };
577 self.artists.push(Artist::Stem(artist));
578 match self.artists.last_mut().expect("just pushed") {
579 Artist::Stem(a) => Ok(a),
580 _ => unreachable!(),
581 }
582 }
583
584 pub fn boxplot(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut BoxPlotArtist> {
593 use crate::charts::boxplot::compute_stats;
594 if datasets.is_empty() {
595 return Err(PlotError::EmptyData);
596 }
597 let color = Color::TABLEAU_10[self.color_index % 10];
598 self.color_index += 1;
599 let factor = 1.5;
600 let stats: Vec<_> = datasets.iter().map(|d| compute_stats(d, factor)).collect();
601 let labels: Vec<String> = (0..datasets.len()).map(|i| format!("{}", i + 1)).collect();
602 let artist = BoxPlotArtist {
603 stats,
604 labels,
605 color,
606 label: None,
607 alpha: 1.0,
608 box_width: 0.5,
609 show_outliers: true,
610 whisker_iq_factor: factor,
611 raw_data: datasets,
612 };
613 self.artists.push(Artist::BoxPlot(artist));
614 match self.artists.last_mut().expect("just pushed") {
615 Artist::BoxPlot(a) => Ok(a),
616 _ => unreachable!(),
617 }
618 }
619
620 pub fn errorbar<X: IntoSeries, Y: IntoSeries>(
631 &mut self,
632 x: X,
633 y: Y,
634 ) -> Result<ErrorBarArtist> {
635 let xs = x.into_series();
636 let ys = y.into_series();
637 if xs.len() != ys.len() {
638 return Err(PlotError::SeriesLengthMismatch {
639 expected: xs.len(),
640 got: ys.len(),
641 });
642 }
643 if xs.is_empty() {
644 return Err(PlotError::EmptyData);
645 }
646 let color = Color::TABLEAU_10[self.color_index % 10];
647 self.color_index += 1;
648 Ok(ErrorBarArtist {
649 x: xs,
650 y: ys,
651 xerr: None,
652 yerr: None,
653 color,
654 label: None,
655 cap_size: 4.0,
656 line_width: 1.0,
657 })
658 }
659
660 pub fn add_errorbar(&mut self, artist: ErrorBarArtist) {
665 self.artists.push(Artist::ErrorBar(artist));
666 }
667
668 pub fn heatmap(&mut self, data: Vec<Vec<f64>>) -> Result<&mut HeatmapArtist> {
677 if data.is_empty() {
678 return Err(PlotError::EmptyData);
679 }
680 let color = Color::TABLEAU_10[self.color_index % 10];
681 self.color_index += 1;
682 let artist = HeatmapArtist {
683 data,
684 x_labels: None,
685 y_labels: None,
686 cmap: crate::colormap::Colormap::Viridis,
687 vmin: None,
688 vmax: None,
689 show_values: false,
690 color,
691 label: None,
692 show_colorbar: false,
693 };
694 self.artists.push(Artist::Heatmap(artist));
695 match self.artists.last_mut().expect("just pushed") {
696 Artist::Heatmap(a) => Ok(a),
697 _ => unreachable!(),
698 }
699 }
700
701 pub fn pie<S: IntoSeries>(&mut self, sizes: S) -> Result<&mut PieArtist> {
712 let series = sizes.into_series();
713 if series.is_empty() {
714 return Err(PlotError::EmptyData);
715 }
716 let color = Color::TABLEAU_10[self.color_index % 10];
717 self.color_index += 1;
718 let artist = PieArtist {
719 sizes: series.data,
720 labels: None,
721 colors: None,
722 explode: None,
723 autopct: false,
724 start_angle: 90.0,
725 radius: 1.0,
726 label: None,
727 color,
728 };
729 self.artists.push(Artist::Pie(artist));
730 match self.artists.last_mut().expect("just pushed") {
731 Artist::Pie(a) => Ok(a),
732 _ => unreachable!(),
733 }
734 }
735}
736
737impl Axes {
742 pub fn set_title(&mut self, title: &str) -> &mut Self {
744 self.title = Some(title.to_string());
745 self
746 }
747
748 pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
750 self.xlabel = Some(label.to_string());
751 self
752 }
753
754 pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
756 self.ylabel = Some(label.to_string());
757 self
758 }
759
760 pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
762 self.xlim = Some((min, max));
763 self
764 }
765
766 pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
768 self.ylim = Some((min, max));
769 self
770 }
771
772 pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
774 self.xscale = scale;
775 self
776 }
777
778 pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
780 self.yscale = scale;
781 self
782 }
783
784 pub fn grid(&mut self, show: bool) -> &mut Self {
786 self.show_grid = Some(show);
787 self
788 }
789
790 pub fn legend(&mut self) -> &mut Self {
792 self.show_legend = true;
793 self
794 }
795
796 pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
798 self.legend_loc = loc;
799 self
800 }
801
802 pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
804 self.theme_override = Some(theme);
805 self
806 }
807
808 pub fn grid_axis(&mut self, axis: &str) -> &mut Self {
813 self.grid_axis = match axis {
814 "x" => GridAxis::X,
815 "y" => GridAxis::Y,
816 _ => GridAxis::Both,
817 };
818 self
819 }
820
821 pub fn grid_alpha(&mut self, alpha: f64) -> &mut Self {
823 self.grid_alpha = Some(alpha.clamp(0.0, 1.0));
824 self
825 }
826
827 pub fn grid_style(&mut self, style: LineStyle) -> &mut Self {
829 self.grid_style = Some(style);
830 self
831 }
832
833 pub fn invert_xaxis(&mut self) -> &mut Self {
835 self.x_inverted = true;
836 self
837 }
838
839 pub fn invert_yaxis(&mut self) -> &mut Self {
841 self.y_inverted = true;
842 self
843 }
844
845 pub fn set_xticks(&mut self, ticks: &[f64]) -> &mut Self {
849 self.custom_xticks = Some(ticks.to_vec());
850 self
851 }
852
853 pub fn set_yticks(&mut self, ticks: &[f64]) -> &mut Self {
857 self.custom_yticks = Some(ticks.to_vec());
858 self
859 }
860
861 pub fn set_xticklabels(&mut self, labels: &[&str]) -> &mut Self {
867 self.custom_xticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
868 self
869 }
870
871 pub fn set_yticklabels(&mut self, labels: &[&str]) -> &mut Self {
877 self.custom_yticklabels = Some(labels.iter().map(|s| s.to_string()).collect());
878 self
879 }
880
881 pub fn tick_params_x_rotation(&mut self, degrees: f64) -> &mut Self {
883 self.xtick_rotation = degrees;
884 self
885 }
886
887 pub fn tick_params_y_rotation(&mut self, degrees: f64) -> &mut Self {
889 self.ytick_rotation = degrees;
890 self
891 }
892
893 pub fn text(&mut self, x: f64, y: f64, text: &str) -> &mut TextAnnotation {
901 self.texts.push(TextAnnotation {
902 text: text.to_string(),
903 x,
904 y,
905 fontsize: None,
906 color: None,
907 ha: HAlign::Left,
908 va: VAlign::Baseline,
909 rotation: 0.0,
910 });
911 self.texts.last_mut().expect("just pushed")
912 }
913
914 pub fn annotate(&mut self, text: &str, xy: (f64, f64), xytext: (f64, f64)) -> &mut Annotation {
923 self.annotations.push(Annotation {
924 text: text.to_string(),
925 xy,
926 xytext,
927 fontsize: None,
928 color: None,
929 ha: HAlign::Center,
930 va: VAlign::Bottom,
931 arrowstyle: ArrowStyle::None,
932 arrow_color: None,
933 });
934 self.annotations.last_mut().expect("just pushed")
935 }
936}
937
938impl Axes {
943 pub fn colorbar(&mut self, cmap: crate::colormap::Colormap, vmin: f64, vmax: f64) -> &mut Colorbar {
945 self.colorbar = Some(Colorbar::new(cmap, vmin, vmax));
946 self.colorbar.as_mut().expect("just set")
947 }
948}
949
950impl Axes {
955 pub fn contour(
957 &mut self,
958 x: &[f64],
959 y: &[f64],
960 z: Vec<Vec<f64>>,
961 ) -> Result<&mut ContourArtist> {
962 if x.is_empty() || y.is_empty() || z.is_empty() {
963 return Err(PlotError::EmptyData);
964 }
965 let color = Color::TABLEAU_10[self.color_index % 10];
966 self.color_index += 1;
967 let artist = ContourArtist {
968 x: x.to_vec(),
969 y: y.to_vec(),
970 z,
971 levels: None,
972 filled: false,
973 cmap: crate::colormap::Colormap::Viridis,
974 colors: None,
975 linewidths: 1.0,
976 label: None,
977 color,
978 num_levels: 10,
979 };
980 self.artists.push(Artist::Contour(artist));
981 match self.artists.last_mut().expect("just pushed") {
982 Artist::Contour(a) => Ok(a),
983 _ => unreachable!(),
984 }
985 }
986
987 pub fn contourf(
989 &mut self,
990 x: &[f64],
991 y: &[f64],
992 z: Vec<Vec<f64>>,
993 ) -> Result<&mut ContourArtist> {
994 if x.is_empty() || y.is_empty() || z.is_empty() {
995 return Err(PlotError::EmptyData);
996 }
997 let color = Color::TABLEAU_10[self.color_index % 10];
998 self.color_index += 1;
999 let artist = ContourArtist {
1000 x: x.to_vec(),
1001 y: y.to_vec(),
1002 z,
1003 levels: None,
1004 filled: true,
1005 cmap: crate::colormap::Colormap::Viridis,
1006 colors: None,
1007 linewidths: 1.0,
1008 label: None,
1009 color,
1010 num_levels: 10,
1011 };
1012 self.artists.push(Artist::Contour(artist));
1013 match self.artists.last_mut().expect("just pushed") {
1014 Artist::Contour(a) => Ok(a),
1015 _ => unreachable!(),
1016 }
1017 }
1018
1019 pub fn violin(&mut self, datasets: Vec<Vec<f64>>) -> Result<&mut ViolinArtist> {
1021 if datasets.is_empty() {
1022 return Err(PlotError::EmptyData);
1023 }
1024 let color = Color::TABLEAU_10[self.color_index % 10];
1025 self.color_index += 1;
1026 let artist = ViolinArtist {
1027 datasets,
1028 positions: None,
1029 widths: 0.7,
1030 show_median: true,
1031 show_quartiles: true,
1032 color,
1033 alpha: 0.7,
1034 label: None,
1035 bw_method: 0.0,
1036 };
1037 self.artists.push(Artist::Violin(artist));
1038 match self.artists.last_mut().expect("just pushed") {
1039 Artist::Violin(a) => Ok(a),
1040 _ => unreachable!(),
1041 }
1042 }
1043
1044 pub fn bar_group<C: IntoCategories>(
1049 &mut self,
1050 categories: C,
1051 series: &[(&str, Vec<f64>)],
1052 ) -> Result<()> {
1053 let cat_labels: Vec<String> = categories.into_categories().labels.iter().map(|s| s.to_string()).collect();
1054 let n_series = series.len();
1055 if n_series == 0 {
1056 return Err(PlotError::EmptyData);
1057 }
1058 let total_width = 0.8;
1059 let bar_width = total_width / n_series as f64;
1060
1061 for (si, (label, heights)) in series.iter().enumerate() {
1062 let offset_val = (si as f64 - (n_series as f64 - 1.0) / 2.0) * bar_width;
1063 let offsets: Vec<f64> = vec![offset_val; heights.len()];
1064 let artist_ref = self.bar(cat_labels.clone(), heights.as_slice())?;
1065 artist_ref.bar_width(bar_width);
1066 artist_ref.offset(offsets);
1067 artist_ref.label(label);
1068 }
1069 Ok(())
1070 }
1071}
1072
1073#[allow(clippy::too_many_arguments)]
1078impl Axes {
1079 pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
1095 self.render_primary(renderer, bounds, fig_theme, false);
1096 }
1097
1098 pub(crate) fn render_primary(
1099 &self,
1100 renderer: &mut impl Renderer,
1101 bounds: Rect,
1102 fig_theme: &Theme,
1103 suppress_legend: bool,
1104 ) {
1105 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1106
1107 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1109
1110 let xticks = self.resolve_xticks(xmin, xmax);
1112 let yticks = self.resolve_yticks(ymin, ymax);
1113
1114 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1116 layout_config.has_title = self.title.is_some();
1117 layout_config.has_xlabel = self.xlabel.is_some();
1118 layout_config.has_ylabel = self.ylabel.is_some();
1119 layout_config.has_legend = self.show_legend;
1120
1121 let layout_result = layout::compute_layout(&layout_config);
1122
1123 let full_plot_area = Rect::new(
1124 bounds.x + layout_result.plot_area.x,
1125 bounds.y + layout_result.plot_area.y,
1126 layout_result.plot_area.width,
1127 layout_result.plot_area.height,
1128 );
1129
1130 let effective_colorbar = if self.colorbar.is_some() {
1132 self.colorbar.clone()
1133 } else {
1134 self.auto_colorbar_from_artists()
1135 };
1136
1137 let (plot_area, colorbar_rect) = if effective_colorbar.is_some() {
1139 let cb_width = (full_plot_area.width * colorbar::COLORBAR_WIDTH_FRACTION).max(30.0);
1140 let shrunk = Rect::new(
1141 full_plot_area.x,
1142 full_plot_area.y,
1143 full_plot_area.width - cb_width - colorbar::COLORBAR_GAP,
1144 full_plot_area.height,
1145 );
1146 let cb_rect = Rect::new(
1147 full_plot_area.x + full_plot_area.width - cb_width,
1148 full_plot_area.y,
1149 cb_width,
1150 full_plot_area.height,
1151 );
1152 (shrunk, Some(cb_rect))
1153 } else {
1154 (full_plot_area, None)
1155 };
1156
1157 let bg_path = Path::rect(plot_area);
1159 renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
1160
1161 if self.show_grid.unwrap_or(theme.show_grid) {
1163 self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1164 }
1165
1166 let clip_path = Path::rect(plot_area);
1168 renderer.push_clip(&clip_path, Affine::IDENTITY);
1169 for artist in &self.artists {
1170 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1171 }
1172 renderer.pop_clip();
1173
1174 let x_minor = if matches!(self.xscale, Scale::Log10) {
1176 ticks::generate_log_minor_ticks(xmin, xmax)
1177 } else {
1178 Vec::new()
1179 };
1180 let y_minor = if matches!(self.yscale, Scale::Log10) {
1181 ticks::generate_log_minor_ticks(ymin, ymax)
1182 } else {
1183 Vec::new()
1184 };
1185
1186 self.draw_spines(renderer, &plot_area, theme);
1188
1189 self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
1191 if !x_minor.is_empty() || !y_minor.is_empty() {
1192 self.draw_minor_ticks(renderer, &plot_area, &x_minor, &y_minor, xmin, xmax, ymin, ymax, theme);
1193 }
1194
1195 self.draw_labels(renderer, &plot_area, &bounds, theme);
1197
1198 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1200
1201 if self.show_legend && !suppress_legend {
1203 self.draw_legend(renderer, &plot_area, theme);
1204 }
1205
1206 if let (Some(ref cb), Some(ref cb_rect)) = (&effective_colorbar, &colorbar_rect) {
1208 colorbar::draw_colorbar(renderer, cb, cb_rect, theme);
1209 }
1210 }
1211
1212 fn auto_colorbar_from_artists(&self) -> Option<Colorbar> {
1213 for artist in &self.artists {
1214 if let Artist::Heatmap(a) = artist {
1215 if a.show_colorbar {
1216 return Some(Colorbar::new(
1217 a.cmap,
1218 a.effective_vmin(),
1219 a.effective_vmax(),
1220 ));
1221 }
1222 }
1223 }
1224 None
1225 }
1226
1227 pub(crate) fn render_twin(
1229 &self,
1230 renderer: &mut impl Renderer,
1231 plot_area: Rect,
1232 bounds: Rect,
1233 fig_theme: &Theme,
1234 ) {
1235 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
1236 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
1237 let yticks = self.resolve_yticks(ymin, ymax);
1238 let xticks = self.resolve_xticks(xmin, xmax);
1239
1240 let clip_path = Path::rect(plot_area);
1241 renderer.push_clip(&clip_path, Affine::IDENTITY);
1242 for artist in &self.artists {
1243 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
1244 }
1245 renderer.pop_clip();
1246
1247 let side = self.twin_side.unwrap_or(TwinSide::Right);
1248 match side {
1249 TwinSide::Right => {
1250 let paint = Paint::new(theme.spine_color);
1251 let stroke = Stroke::new(theme.spine_width);
1252 let mut p = Path::new();
1253 p.move_to(plot_area.right(), plot_area.y);
1254 p.line_to(plot_area.right(), plot_area.bottom());
1255 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1256 self.draw_ticks_right(renderer, &plot_area, &yticks, ymin, ymax, theme);
1257 self.draw_ylabel_right(renderer, &plot_area, &bounds, theme);
1258 }
1259 TwinSide::Top => {
1260 let paint = Paint::new(theme.spine_color);
1261 let stroke = Stroke::new(theme.spine_width);
1262 let mut p = Path::new();
1263 p.move_to(plot_area.x, plot_area.y);
1264 p.line_to(plot_area.right(), plot_area.y);
1265 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1266 self.draw_ticks_top(renderer, &plot_area, &xticks, xmin, xmax, theme);
1267 self.draw_xlabel_top(renderer, &plot_area, theme);
1268 }
1269 }
1270
1271 self.draw_annotations(renderer, &plot_area, xmin, xmax, ymin, ymax, theme);
1272 }
1273
1274 pub(crate) fn compute_plot_area(&self, bounds: &Rect) -> Rect {
1276 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
1277 layout_config.has_title = self.title.is_some();
1278 layout_config.has_xlabel = self.xlabel.is_some();
1279 layout_config.has_ylabel = self.ylabel.is_some();
1280 layout_config.has_legend = self.show_legend;
1281 let layout_result = layout::compute_layout(&layout_config);
1282 Rect::new(
1283 bounds.x + layout_result.plot_area.x,
1284 bounds.y + layout_result.plot_area.y,
1285 layout_result.plot_area.width,
1286 layout_result.plot_area.height,
1287 )
1288 }
1289
1290 pub fn collect_legend_entries(&self) -> Vec<LegendEntry> {
1292 self.artists
1293 .iter()
1294 .filter_map(|a| {
1295 let (label, color, swatch) = match a {
1296 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1297 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1298 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1299 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1300 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1301 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1302 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1303 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1304 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1305 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1306 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1307 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1308 Artist::Contour(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
1309 };
1310 label.map(|l| LegendEntry {
1311 label: l.to_string(),
1312 color,
1313 swatch,
1314 })
1315 })
1316 .collect()
1317 }
1318
1319 fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
1326 let mut x_lo = f64::INFINITY;
1327 let mut x_hi = f64::NEG_INFINITY;
1328 let mut y_lo = f64::INFINITY;
1329 let mut y_hi = f64::NEG_INFINITY;
1330
1331 for artist in &self.artists {
1332 match artist {
1333 Artist::Line(a) => {
1334 if let Some((lo, hi)) = a.x.bounds() {
1335 x_lo = x_lo.min(lo);
1336 x_hi = x_hi.max(hi);
1337 }
1338 if let Some((lo, hi)) = a.y.bounds() {
1339 y_lo = y_lo.min(lo);
1340 y_hi = y_hi.max(hi);
1341 }
1342 }
1343 Artist::Scatter(a) => {
1344 if let Some((lo, hi)) = a.x.bounds() {
1345 x_lo = x_lo.min(lo);
1346 x_hi = x_hi.max(hi);
1347 }
1348 if let Some((lo, hi)) = a.y.bounds() {
1349 y_lo = y_lo.min(lo);
1350 y_hi = y_hi.max(hi);
1351 }
1352 }
1353 Artist::Bar(a) => {
1354 let n = a.categories.len() as f64;
1355 if a.horizontal {
1356 y_lo = 0.0_f64.min(y_lo);
1358 y_hi = n.max(y_hi);
1359 x_lo = 0.0_f64.min(x_lo);
1360 if let Some((lo, hi)) = a.heights.bounds() {
1361 x_lo = x_lo.min(lo.min(0.0));
1362 x_hi = x_hi.max(hi);
1363 }
1364 } else {
1365 x_lo = 0.0_f64.min(x_lo);
1367 x_hi = n.max(x_hi);
1368 y_lo = 0.0_f64.min(y_lo);
1369 if let Some((lo, hi)) = a.heights.bounds() {
1370 y_lo = y_lo.min(lo.min(0.0));
1371 y_hi = y_hi.max(hi);
1372 }
1373 }
1374 }
1375 Artist::Histogram(a) => {
1376 if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
1377 x_lo = x_lo.min(first);
1378 x_hi = x_hi.max(last);
1379 }
1380 y_lo = 0.0_f64.min(y_lo);
1381 let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
1382 y_hi = y_hi.max(max_count);
1383 }
1384 Artist::FillBetween(a) => {
1385 if let Some((lo, hi)) = a.x.bounds() {
1386 x_lo = x_lo.min(lo);
1387 x_hi = x_hi.max(hi);
1388 }
1389 if let Some((lo, hi)) = a.y1.bounds() {
1390 y_lo = y_lo.min(lo);
1391 y_hi = y_hi.max(hi);
1392 }
1393 if let Some((lo, hi)) = a.y2.bounds() {
1394 y_lo = y_lo.min(lo);
1395 y_hi = y_hi.max(hi);
1396 }
1397 }
1398 Artist::Step(a) => {
1399 if let Some((lo, hi)) = a.x.bounds() {
1400 x_lo = x_lo.min(lo);
1401 x_hi = x_hi.max(hi);
1402 }
1403 if let Some((lo, hi)) = a.y.bounds() {
1404 y_lo = y_lo.min(lo);
1405 y_hi = y_hi.max(hi);
1406 }
1407 }
1408 Artist::Stem(a) => {
1409 if let Some((lo, hi)) = a.x.bounds() {
1410 x_lo = x_lo.min(lo);
1411 x_hi = x_hi.max(hi);
1412 }
1413 if let Some((lo, hi)) = a.y.bounds() {
1414 y_lo = y_lo.min(lo.min(a.baseline));
1415 y_hi = y_hi.max(hi.max(a.baseline));
1416 }
1417 }
1418 Artist::BoxPlot(a) => {
1419 let n = a.stats.len() as f64;
1420 x_lo = 0.0_f64.min(x_lo);
1421 x_hi = n.max(x_hi);
1422 for s in &a.stats {
1423 y_lo = y_lo.min(s.whisker_low);
1424 y_hi = y_hi.max(s.whisker_high);
1425 for &o in &s.outliers {
1426 y_lo = y_lo.min(o);
1427 y_hi = y_hi.max(o);
1428 }
1429 }
1430 }
1431 Artist::ErrorBar(a) => {
1432 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1433 x_lo = x_lo.min(bxlo);
1434 x_hi = x_hi.max(bxhi);
1435 y_lo = y_lo.min(bylo);
1436 y_hi = y_hi.max(byhi);
1437 }
1438 Artist::Heatmap(a) => {
1439 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1440 x_lo = x_lo.min(bxlo);
1441 x_hi = x_hi.max(bxhi);
1442 y_lo = y_lo.min(bylo);
1443 y_hi = y_hi.max(byhi);
1444 }
1445 Artist::Pie(a) => {
1446 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1449 x_lo = x_lo.min(bxlo);
1450 x_hi = x_hi.max(bxhi);
1451 y_lo = y_lo.min(bylo);
1452 y_hi = y_hi.max(byhi);
1453 }
1454 Artist::Violin(a) => {
1455 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1456 x_lo = x_lo.min(bxlo);
1457 x_hi = x_hi.max(bxhi);
1458 y_lo = y_lo.min(bylo);
1459 y_hi = y_hi.max(byhi);
1460 }
1461 Artist::Contour(a) => {
1462 let (bxlo, bxhi, bylo, byhi) = a.data_bounds();
1463 x_lo = x_lo.min(bxlo);
1464 x_hi = x_hi.max(bxhi);
1465 y_lo = y_lo.min(bylo);
1466 y_hi = y_hi.max(byhi);
1467 }
1468 }
1469 }
1470
1471 if !x_lo.is_finite() || !x_hi.is_finite() {
1473 x_lo = if self.xscale.requires_positive() { 1.0 } else { 0.0 };
1474 x_hi = if self.xscale.requires_positive() { 10.0 } else { 1.0 };
1475 }
1476 if !y_lo.is_finite() || !y_hi.is_finite() {
1477 y_lo = if self.yscale.requires_positive() { 1.0 } else { 0.0 };
1478 y_hi = if self.yscale.requires_positive() { 10.0 } else { 1.0 };
1479 }
1480
1481 if self.xscale.requires_positive() {
1483 if x_lo <= 0.0 {
1484 x_lo = if x_hi > 0.0 { x_hi * 1e-4 } else { 1.0 };
1486 }
1487 if x_hi <= x_lo {
1488 x_hi = x_lo * 10.0;
1489 }
1490 }
1491 if self.yscale.requires_positive() {
1492 if y_lo <= 0.0 {
1493 y_lo = if y_hi > 0.0 { y_hi * 1e-4 } else { 1.0 };
1494 }
1495 if y_hi <= y_lo {
1496 y_hi = y_lo * 10.0;
1497 }
1498 }
1499
1500 if (x_hi - x_lo).abs() < f64::EPSILON {
1502 x_lo -= 0.5;
1503 x_hi += 0.5;
1504 }
1505 if (y_hi - y_lo).abs() < f64::EPSILON {
1506 y_lo -= 0.5;
1507 y_hi += 0.5;
1508 }
1509
1510 let (x_pad_lo, x_pad_hi) = if self.xscale.requires_positive() {
1513 let factor = 1.0 + AUTOSCALE_PAD;
1515 (x_lo / factor, x_hi * factor)
1516 } else {
1517 let pad = (x_hi - x_lo) * AUTOSCALE_PAD;
1518 (x_lo - pad, x_hi + pad)
1519 };
1520 let (y_pad_lo, y_pad_hi) = if self.yscale.requires_positive() {
1521 let factor = 1.0 + AUTOSCALE_PAD;
1522 (y_lo / factor, y_hi * factor)
1523 } else {
1524 let pad = (y_hi - y_lo) * AUTOSCALE_PAD;
1525 (y_lo - pad, y_hi + pad)
1526 };
1527 x_lo = x_pad_lo;
1528 x_hi = x_pad_hi;
1529 y_lo = y_pad_lo;
1530 y_hi = y_pad_hi;
1531
1532 if let Some((lo, hi)) = self.xlim {
1534 x_lo = lo;
1535 x_hi = hi;
1536 }
1537 if let Some((lo, hi)) = self.ylim {
1538 y_lo = lo;
1539 y_hi = hi;
1540 }
1541
1542 if self.x_inverted {
1544 std::mem::swap(&mut x_lo, &mut x_hi);
1545 }
1546 if self.y_inverted {
1547 std::mem::swap(&mut y_lo, &mut y_hi);
1548 }
1549
1550 (x_lo, x_hi, y_lo, y_hi)
1551 }
1552
1553 fn resolve_xticks(&self, xmin: f64, xmax: f64) -> Vec<ticks::Tick> {
1560 if let Some(ref positions) = self.custom_xticks {
1561 let labels = self.custom_xticklabels.as_ref();
1562 positions
1563 .iter()
1564 .enumerate()
1565 .map(|(i, &v)| ticks::Tick {
1566 value: v,
1567 label: labels
1568 .and_then(|l| l.get(i))
1569 .cloned()
1570 .unwrap_or_else(|| ticks::format_tick_value(v)),
1571 })
1572 .collect()
1573 } else {
1574 let (lo, hi) = if xmin <= xmax { (xmin, xmax) } else { (xmax, xmin) };
1575 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.xscale)
1576 }
1577 }
1578
1579 fn resolve_yticks(&self, ymin: f64, ymax: f64) -> Vec<ticks::Tick> {
1582 if let Some(ref positions) = self.custom_yticks {
1583 let labels = self.custom_yticklabels.as_ref();
1584 positions
1585 .iter()
1586 .enumerate()
1587 .map(|(i, &v)| ticks::Tick {
1588 value: v,
1589 label: labels
1590 .and_then(|l| l.get(i))
1591 .cloned()
1592 .unwrap_or_else(|| ticks::format_tick_value(v)),
1593 })
1594 .collect()
1595 } else {
1596 let (lo, hi) = if ymin <= ymax { (ymin, ymax) } else { (ymax, ymin) };
1597 ticks::generate_ticks(lo, hi, DEFAULT_TICK_COUNT, &self.yscale)
1598 }
1599 }
1600
1601 fn draw_grid(
1607 &self,
1608 renderer: &mut impl Renderer,
1609 plot_area: &Rect,
1610 xticks: &[ticks::Tick],
1611 yticks: &[ticks::Tick],
1612 xmin: f64,
1613 xmax: f64,
1614 ymin: f64,
1615 ymax: f64,
1616 theme: &Theme,
1617 ) {
1618 let grid_color = if let Some(alpha) = self.grid_alpha {
1620 theme.grid_color.with_alpha((alpha * 255.0) as u8)
1621 } else {
1622 theme.grid_color
1623 };
1624 let paint = Paint::new(grid_color);
1625
1626 let mut stroke = Stroke::new(theme.grid_width);
1628 if let Some(style) = self.grid_style {
1629 stroke = match style {
1630 LineStyle::Solid => stroke,
1631 LineStyle::Dashed => stroke.with_dash(DashPattern {
1632 dashes: vec![6.0, 4.0],
1633 offset: 0.0,
1634 }),
1635 LineStyle::Dotted => stroke.with_dash(DashPattern {
1636 dashes: vec![2.0, 2.0],
1637 offset: 0.0,
1638 }),
1639 LineStyle::DashDot => stroke.with_dash(DashPattern {
1640 dashes: vec![6.0, 3.0, 2.0, 3.0],
1641 offset: 0.0,
1642 }),
1643 };
1644 }
1645
1646 let draw_x = matches!(self.grid_axis, GridAxis::X | GridAxis::Both);
1647 let draw_y = matches!(self.grid_axis, GridAxis::Y | GridAxis::Both);
1648
1649 if draw_x {
1651 for tick in xticks {
1652 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1653 let mut path = Path::new();
1654 path.move_to(pt.x, plot_area.y);
1655 path.line_to(pt.x, plot_area.bottom());
1656 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1657 }
1658 }
1659
1660 if draw_y {
1662 for tick in yticks {
1663 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1664 let mut path = Path::new();
1665 path.move_to(plot_area.x, pt.y);
1666 path.line_to(plot_area.right(), pt.y);
1667 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1668 }
1669 }
1670 }
1671
1672 fn draw_artist(
1678 &self,
1679 renderer: &mut impl Renderer,
1680 artist: &Artist,
1681 plot_area: &Rect,
1682 xmin: f64,
1683 xmax: f64,
1684 ymin: f64,
1685 ymax: f64,
1686 theme: &Theme,
1687 ) {
1688 match artist {
1689 Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1690 Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1691 Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1692 Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1693 Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1694 Artist::Step(a) => self.draw_step(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1695 Artist::Stem(a) => self.draw_stem(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1696 Artist::BoxPlot(a) => self.draw_boxplot(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1697 Artist::ErrorBar(a) => self.draw_errorbar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1698 Artist::Heatmap(a) => self.draw_heatmap(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1699 Artist::Pie(a) => self.draw_pie(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1700 Artist::Violin(a) => self.draw_violin(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
1701 Artist::Contour(a) => self.draw_contour(renderer, a, plot_area, xmin, xmax, ymin, ymax),
1702 }
1703 }
1704
1705 fn draw_line(
1707 &self,
1708 renderer: &mut impl Renderer,
1709 artist: &LineArtist,
1710 plot_area: &Rect,
1711 xmin: f64,
1712 xmax: f64,
1713 ymin: f64,
1714 ymax: f64,
1715 ) {
1716 if artist.x.is_empty() {
1717 return;
1718 }
1719
1720 let mut path = Path::new();
1721 let first = self.data_to_pixel(
1722 artist.x.data[0],
1723 artist.y.data[0],
1724 plot_area,
1725 xmin, xmax, ymin, ymax,
1726 );
1727 path.move_to(first.x, first.y);
1728
1729 for i in 1..artist.x.len() {
1730 let pt = self.data_to_pixel(
1731 artist.x.data[i],
1732 artist.y.data[i],
1733 plot_area,
1734 xmin, xmax, ymin, ymax,
1735 );
1736 path.line_to(pt.x, pt.y);
1737 }
1738
1739 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1740 let paint = Paint::new(color);
1741 let mut stroke = Stroke::new(artist.width);
1742
1743 match artist.style {
1745 crate::theme::LineStyle::Solid => {}
1746 crate::theme::LineStyle::Dashed => {
1747 stroke = stroke.with_dash(DashPattern {
1748 dashes: vec![6.0, 4.0],
1749 offset: 0.0,
1750 });
1751 }
1752 crate::theme::LineStyle::Dotted => {
1753 stroke = stroke.with_dash(DashPattern {
1754 dashes: vec![2.0, 2.0],
1755 offset: 0.0,
1756 });
1757 }
1758 crate::theme::LineStyle::DashDot => {
1759 stroke = stroke.with_dash(DashPattern {
1760 dashes: vec![6.0, 3.0, 2.0, 3.0],
1761 offset: 0.0,
1762 });
1763 }
1764 }
1765
1766 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
1767 }
1768
1769 fn draw_scatter(
1771 &self,
1772 renderer: &mut impl Renderer,
1773 artist: &ScatterArtist,
1774 plot_area: &Rect,
1775 xmin: f64,
1776 xmax: f64,
1777 ymin: f64,
1778 ymax: f64,
1779 theme: &Theme,
1780 ) {
1781 let alpha_byte = (artist.alpha * 255.0) as u8;
1782
1783 let cmap_colors: Option<Vec<Color>> = match (&artist.c, &artist.cmap) {
1785 (Some(c_vals), Some(cmap)) if !c_vals.is_empty() => Some(cmap.map_values(c_vals)),
1786 _ => None,
1787 };
1788
1789 let default_color = artist.color.with_alpha(alpha_byte);
1790 let default_paint = Paint::new(default_color);
1791 let radius = artist.size / 2.0;
1792
1793 for i in 0..artist.x.len() {
1794 let pt = self.data_to_pixel(
1795 artist.x.data[i],
1796 artist.y.data[i],
1797 plot_area,
1798 xmin, xmax, ymin, ymax,
1799 );
1800
1801 let paint = if let Some(ref cc) = cmap_colors {
1803 Paint::new(cc[i].with_alpha(alpha_byte))
1804 } else if let Some(ref cs) = artist.colors {
1805 Paint::new(cs[i].with_alpha(alpha_byte))
1806 } else {
1807 default_paint
1808 };
1809
1810 let marker_path = match artist.marker {
1811 Marker::Circle | Marker::Point => Path::circle(pt, radius),
1812 Marker::Square => {
1813 Path::rect(Rect::new(pt.x - radius, pt.y - radius, radius * 2.0, radius * 2.0))
1814 }
1815 Marker::Diamond => {
1816 let mut p = Path::new();
1817 p.move_to(pt.x, pt.y - radius);
1818 p.line_to(pt.x + radius, pt.y);
1819 p.line_to(pt.x, pt.y + radius);
1820 p.line_to(pt.x - radius, pt.y);
1821 p.close();
1822 p
1823 }
1824 Marker::Triangle => {
1825 let mut p = Path::new();
1826 let h = radius * 1.1547; p.move_to(pt.x, pt.y - radius);
1828 p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
1829 p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
1830 p.close();
1831 p
1832 }
1833 Marker::Plus => {
1834 let mut p = Path::new();
1836 p.move_to(pt.x - radius, pt.y);
1837 p.line_to(pt.x + radius, pt.y);
1838 p.move_to(pt.x, pt.y - radius);
1839 p.line_to(pt.x, pt.y + radius);
1840 let stroke = Stroke::new(theme.line_width.max(1.0));
1841 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1842 continue;
1843 }
1844 Marker::Cross => {
1845 let mut p = Path::new();
1846 let d = radius * 0.707; p.move_to(pt.x - d, pt.y - d);
1848 p.line_to(pt.x + d, pt.y + d);
1849 p.move_to(pt.x + d, pt.y - d);
1850 p.line_to(pt.x - d, pt.y + d);
1851 let stroke = Stroke::new(theme.line_width.max(1.0));
1852 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1853 continue;
1854 }
1855 Marker::Star => {
1856 let mut p = Path::new();
1858 let inner = radius * 0.382;
1859 for j in 0..10 {
1860 let angle = std::f64::consts::FRAC_PI_2
1861 + j as f64 * std::f64::consts::PI / 5.0;
1862 let r = if j % 2 == 0 { radius } else { inner };
1863 let sx = pt.x + r * angle.cos();
1864 let sy = pt.y - r * angle.sin();
1865 if j == 0 {
1866 p.move_to(sx, sy);
1867 } else {
1868 p.line_to(sx, sy);
1869 }
1870 }
1871 p.close();
1872 p
1873 }
1874 };
1875
1876 renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
1877 }
1878 }
1879
1880 fn draw_bar(
1882 &self,
1883 renderer: &mut impl Renderer,
1884 artist: &BarArtist,
1885 plot_area: &Rect,
1886 xmin: f64,
1887 xmax: f64,
1888 ymin: f64,
1889 ymax: f64,
1890 ) {
1891 let n = artist.categories.len();
1892 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1893 let paint = Paint::new(color);
1894
1895 if artist.horizontal {
1896 let cat_range = ymax - ymin;
1898 let cat_step = cat_range / n as f64;
1899 let bar_half = cat_step * artist.bar_width * 0.5;
1900
1901 for i in 0..n {
1902 let base_center = ymin + (i as f64 + 0.5) * cat_step;
1903 let cat_center = if let Some(ref off) = artist.offset {
1904 base_center + if i < off.len() { off[i] } else { 0.0 }
1905 } else {
1906 base_center
1907 };
1908 let value = artist.heights.data[i];
1909 let base = if let Some(ref bot) = artist.bottom {
1910 if i < bot.len() { bot[i] } else { 0.0 }
1911 } else {
1912 0.0
1913 };
1914
1915 let left_val = base.min(base + value);
1916 let right_val = base.max(base + value);
1917
1918 let p_left = self.data_to_pixel(left_val, cat_center - bar_half, plot_area, xmin, xmax, ymin, ymax);
1919 let p_right = self.data_to_pixel(right_val, cat_center + bar_half, plot_area, xmin, xmax, ymin, ymax);
1920
1921 let rect = Rect::from_points(p_left, p_right);
1922 let bar_path = Path::rect(rect);
1923 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1924 }
1925 } else {
1926 let cat_range = xmax - xmin;
1928 let cat_step = cat_range / n as f64;
1929 let bar_half = cat_step * artist.bar_width * 0.5;
1930
1931 for i in 0..n {
1932 let base_center = xmin + (i as f64 + 0.5) * cat_step;
1933 let cat_center = if let Some(ref off) = artist.offset {
1934 base_center + if i < off.len() { off[i] } else { 0.0 }
1935 } else {
1936 base_center
1937 };
1938 let value = artist.heights.data[i];
1939 let base = if let Some(ref bot) = artist.bottom {
1940 if i < bot.len() { bot[i] } else { 0.0 }
1941 } else {
1942 0.0
1943 };
1944
1945 let bottom_val = base.min(base + value);
1946 let top_val = base.max(base + value);
1947
1948 let p_bl = self.data_to_pixel(cat_center - bar_half, bottom_val, plot_area, xmin, xmax, ymin, ymax);
1949 let p_tr = self.data_to_pixel(cat_center + bar_half, top_val, plot_area, xmin, xmax, ymin, ymax);
1950
1951 let rect = Rect::from_points(p_bl, p_tr);
1952 let bar_path = Path::rect(rect);
1953 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1954 }
1955 }
1956 }
1957
1958 fn draw_hist(
1960 &self,
1961 renderer: &mut impl Renderer,
1962 artist: &HistArtist,
1963 plot_area: &Rect,
1964 xmin: f64,
1965 xmax: f64,
1966 ymin: f64,
1967 ymax: f64,
1968 ) {
1969 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1970 let paint = Paint::new(color);
1971 let stroke_paint = Paint::new(Color::WHITE);
1972 let stroke = Stroke::new(0.5);
1973
1974 for i in 0..artist.counts.len() {
1975 let left = artist.bin_edges[i];
1976 let right = artist.bin_edges[i + 1];
1977 let height = artist.counts[i];
1978
1979 if height <= 0.0 {
1980 continue;
1981 }
1982
1983 let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
1984 let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
1985
1986 let rect = Rect::from_points(p_bl, p_tr);
1987 let bar_path = Path::rect(rect);
1988 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1989 renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
1991 }
1992 }
1993
1994 fn draw_fill_between(
1997 &self,
1998 renderer: &mut impl Renderer,
1999 artist: &FillBetweenArtist,
2000 plot_area: &Rect,
2001 xmin: f64,
2002 xmax: f64,
2003 ymin: f64,
2004 ymax: f64,
2005 ) {
2006 if artist.x.is_empty() {
2007 return;
2008 }
2009
2010 let n = artist.x.len();
2011 let mut path = Path::new();
2012
2013 let first = self.data_to_pixel(
2015 artist.x.data[0],
2016 artist.y1.data[0],
2017 plot_area,
2018 xmin, xmax, ymin, ymax,
2019 );
2020 path.move_to(first.x, first.y);
2021 for i in 1..n {
2022 let pt = self.data_to_pixel(
2023 artist.x.data[i],
2024 artist.y1.data[i],
2025 plot_area,
2026 xmin, xmax, ymin, ymax,
2027 );
2028 path.line_to(pt.x, pt.y);
2029 }
2030
2031 for i in (0..n).rev() {
2033 let pt = self.data_to_pixel(
2034 artist.x.data[i],
2035 artist.y2.data[i],
2036 plot_area,
2037 xmin, xmax, ymin, ymax,
2038 );
2039 path.line_to(pt.x, pt.y);
2040 }
2041 path.close();
2042
2043 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2044 let paint = Paint::new(color);
2045 renderer.fill_path(&path, &paint, Affine::IDENTITY);
2046 }
2047
2048
2049 fn draw_spines(
2055 &self,
2056 renderer: &mut impl Renderer,
2057 plot_area: &Rect,
2058 theme: &Theme,
2059 ) {
2060 let paint = Paint::new(theme.spine_color);
2061 let stroke = Stroke::new(theme.spine_width);
2062
2063 if theme.show_bottom_spine {
2065 let mut p = Path::new();
2066 p.move_to(plot_area.x, plot_area.bottom());
2067 p.line_to(plot_area.right(), plot_area.bottom());
2068 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2069 }
2070 if theme.show_left_spine {
2072 let mut p = Path::new();
2073 p.move_to(plot_area.x, plot_area.y);
2074 p.line_to(plot_area.x, plot_area.bottom());
2075 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2076 }
2077 if theme.show_top_spine {
2079 let mut p = Path::new();
2080 p.move_to(plot_area.x, plot_area.y);
2081 p.line_to(plot_area.right(), plot_area.y);
2082 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2083 }
2084 if theme.show_right_spine {
2086 let mut p = Path::new();
2087 p.move_to(plot_area.right(), plot_area.y);
2088 p.line_to(plot_area.right(), plot_area.bottom());
2089 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
2090 }
2091 }
2092
2093 fn draw_ticks(
2099 &self,
2100 renderer: &mut impl Renderer,
2101 plot_area: &Rect,
2102 xticks: &[ticks::Tick],
2103 yticks: &[ticks::Tick],
2104 xmin: f64,
2105 xmax: f64,
2106 ymin: f64,
2107 ymax: f64,
2108 theme: &Theme,
2109 ) {
2110 let tick_paint = Paint::new(theme.tick_color);
2111 let tick_stroke = Stroke::new(1.0);
2112 let tick_len = theme.tick_length;
2113
2114 let x_label_style = TextStyle {
2115 size: theme.tick_label_size,
2116 color: theme.text_color,
2117 weight: FontWeight::Normal,
2118 family: theme.font_family.clone(),
2119 halign: if self.xtick_rotation.abs() > 1.0 {
2120 HAlign::Right
2121 } else {
2122 HAlign::Center
2123 },
2124 valign: VAlign::Top,
2125 };
2126
2127 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2129
2130 let x_rot_rad = -self.xtick_rotation.to_radians();
2132
2133 for tick in xticks {
2135 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
2136 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2138 continue;
2139 }
2140 let x = pt.x;
2141 let y_base = plot_area.bottom();
2142
2143 let (y_start, y_end) = if outward {
2145 (y_base, y_base + tick_len)
2146 } else {
2147 (y_base - tick_len, y_base)
2148 };
2149 let mut tp = Path::new();
2150 tp.move_to(x, y_start);
2151 tp.line_to(x, y_end);
2152 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2153
2154 let label_y = if outward {
2156 y_base + tick_len + 2.0
2157 } else {
2158 y_base + 2.0
2159 };
2160 let label_pos = Point::new(x, label_y);
2161 let transform = if self.xtick_rotation.abs() > 0.01 {
2162 let rotate = Affine::rotate(x_rot_rad);
2163 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2164 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2165 from_origin * rotate * to_origin
2166 } else {
2167 Affine::IDENTITY
2168 };
2169 renderer.draw_text(
2170 &tick.label,
2171 label_pos,
2172 &x_label_style,
2173 transform,
2174 );
2175 }
2176
2177 let y_label_style = TextStyle {
2179 halign: HAlign::Right,
2180 valign: VAlign::Middle,
2181 ..x_label_style.clone()
2182 };
2183
2184 let y_rot_rad = -self.ytick_rotation.to_radians();
2186
2187 for tick in yticks {
2188 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
2189 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2191 continue;
2192 }
2193 let y = pt.y;
2194 let x_base = plot_area.x;
2195
2196 let (x_start, x_end) = if outward {
2198 (x_base - tick_len, x_base)
2199 } else {
2200 (x_base, x_base + tick_len)
2201 };
2202 let mut tp = Path::new();
2203 tp.move_to(x_start, y);
2204 tp.line_to(x_end, y);
2205 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2206
2207 let label_x = if outward {
2209 x_base - tick_len - 3.0
2210 } else {
2211 x_base - 3.0
2212 };
2213 let label_pos = Point::new(label_x, y);
2214 let transform = if self.ytick_rotation.abs() > 0.01 {
2215 let rotate = Affine::rotate(y_rot_rad);
2216 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2217 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2218 from_origin * rotate * to_origin
2219 } else {
2220 Affine::IDENTITY
2221 };
2222 renderer.draw_text(
2223 &tick.label,
2224 label_pos,
2225 &y_label_style,
2226 transform,
2227 );
2228 }
2229 }
2230
2231 fn draw_minor_ticks(
2237 &self,
2238 renderer: &mut impl Renderer,
2239 plot_area: &Rect,
2240 x_minor: &[f64],
2241 y_minor: &[f64],
2242 xmin: f64,
2243 xmax: f64,
2244 ymin: f64,
2245 ymax: f64,
2246 theme: &Theme,
2247 ) {
2248 let tick_paint = Paint::new(theme.tick_color);
2249 let tick_stroke = Stroke::new(0.5);
2250 let tick_len = theme.tick_length * 0.5;
2252 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2253
2254 for &val in x_minor {
2256 let pt = self.data_to_pixel(val, ymin, plot_area, xmin, xmax, ymin, ymax);
2257 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2258 continue;
2259 }
2260 let x = pt.x;
2261 let y_base = plot_area.bottom();
2262 let (y_start, y_end) = if outward {
2263 (y_base, y_base + tick_len)
2264 } else {
2265 (y_base - tick_len, y_base)
2266 };
2267 let mut tp = Path::new();
2268 tp.move_to(x, y_start);
2269 tp.line_to(x, y_end);
2270 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2271 }
2272
2273 for &val in y_minor {
2275 let pt = self.data_to_pixel(xmin, val, plot_area, xmin, xmax, ymin, ymax);
2276 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2277 continue;
2278 }
2279 let y = pt.y;
2280 let x_base = plot_area.x;
2281 let (x_start, x_end) = if outward {
2282 (x_base - tick_len, x_base)
2283 } else {
2284 (x_base, x_base + tick_len)
2285 };
2286 let mut tp = Path::new();
2287 tp.move_to(x_start, y);
2288 tp.line_to(x_end, y);
2289 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2290 }
2291 }
2292
2293 fn draw_ticks_right(
2295 &self,
2296 renderer: &mut impl Renderer,
2297 plot_area: &Rect,
2298 yticks: &[ticks::Tick],
2299 ymin: f64,
2300 ymax: f64,
2301 theme: &Theme,
2302 ) {
2303 let tick_paint = Paint::new(theme.tick_color);
2304 let tick_stroke = Stroke::new(1.0);
2305 let tick_len = theme.tick_length;
2306 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2307
2308 let y_label_style = TextStyle {
2309 size: theme.tick_label_size,
2310 color: theme.text_color,
2311 weight: FontWeight::Normal,
2312 family: theme.font_family.clone(),
2313 halign: HAlign::Left,
2314 valign: VAlign::Middle,
2315 };
2316
2317 let y_rot_rad = -self.ytick_rotation.to_radians();
2318
2319 for tick in yticks {
2320 let pt = self.data_to_pixel(0.0, tick.value, plot_area, 0.0, 1.0, ymin, ymax);
2321 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
2322 continue;
2323 }
2324 let y = pt.y;
2325 let x_base = plot_area.right();
2326 let (x_start, x_end) = if outward {
2327 (x_base, x_base + tick_len)
2328 } else {
2329 (x_base - tick_len, x_base)
2330 };
2331 let mut tp = Path::new();
2332 tp.move_to(x_start, y);
2333 tp.line_to(x_end, y);
2334 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2335
2336 let label_x = if outward { x_base + tick_len + 3.0 } else { x_base + 3.0 };
2337 let label_pos = Point::new(label_x, y);
2338 let transform = if self.ytick_rotation.abs() > 0.01 {
2339 let rotate = Affine::rotate(y_rot_rad);
2340 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2341 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2342 from_origin * rotate * to_origin
2343 } else {
2344 Affine::IDENTITY
2345 };
2346 renderer.draw_text(&tick.label, label_pos, &y_label_style, transform);
2347 }
2348 }
2349
2350 fn draw_ylabel_right(
2352 &self,
2353 renderer: &mut impl Renderer,
2354 plot_area: &Rect,
2355 bounds: &Rect,
2356 theme: &Theme,
2357 ) {
2358 if let Some(ylabel) = &self.ylabel {
2359 let style = TextStyle {
2360 size: theme.axis_label_size,
2361 color: theme.text_color,
2362 weight: FontWeight::Normal,
2363 family: theme.font_family.clone(),
2364 halign: HAlign::Center,
2365 valign: VAlign::Top,
2366 };
2367 let x = bounds.right() - 4.0;
2368 let y = plot_area.y + plot_area.height / 2.0;
2369 let rotate = Affine::rotate(std::f64::consts::FRAC_PI_2);
2370 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2371 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2372 let transform = translate_to * rotate * translate_back;
2373 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2374 }
2375 }
2376
2377 fn draw_ticks_top(
2379 &self,
2380 renderer: &mut impl Renderer,
2381 plot_area: &Rect,
2382 xticks: &[ticks::Tick],
2383 xmin: f64,
2384 xmax: f64,
2385 theme: &Theme,
2386 ) {
2387 let tick_paint = Paint::new(theme.tick_color);
2388 let tick_stroke = Stroke::new(1.0);
2389 let tick_len = theme.tick_length;
2390 let outward = matches!(theme.tick_direction, TickDirection::Outward);
2391
2392 let x_label_style = TextStyle {
2393 size: theme.tick_label_size,
2394 color: theme.text_color,
2395 weight: FontWeight::Normal,
2396 family: theme.font_family.clone(),
2397 halign: if self.xtick_rotation.abs() > 1.0 { HAlign::Left } else { HAlign::Center },
2398 valign: VAlign::Bottom,
2399 };
2400
2401 let x_rot_rad = -self.xtick_rotation.to_radians();
2402
2403 for tick in xticks {
2404 let pt = self.data_to_pixel(tick.value, 0.0, plot_area, xmin, xmax, 0.0, 1.0);
2405 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
2406 continue;
2407 }
2408 let x = pt.x;
2409 let y_base = plot_area.y;
2410 let (y_start, y_end) = if outward {
2411 (y_base - tick_len, y_base)
2412 } else {
2413 (y_base, y_base + tick_len)
2414 };
2415 let mut tp = Path::new();
2416 tp.move_to(x, y_start);
2417 tp.line_to(x, y_end);
2418 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
2419
2420 let label_y = if outward { y_base - tick_len - 2.0 } else { y_base - 2.0 };
2421 let label_pos = Point::new(x, label_y);
2422 let transform = if self.xtick_rotation.abs() > 0.01 {
2423 let rotate = Affine::rotate(x_rot_rad);
2424 let to_origin = Affine::translate(kurbo::Vec2::new(-label_pos.x, -label_pos.y));
2425 let from_origin = Affine::translate(kurbo::Vec2::new(label_pos.x, label_pos.y));
2426 from_origin * rotate * to_origin
2427 } else {
2428 Affine::IDENTITY
2429 };
2430 renderer.draw_text(&tick.label, label_pos, &x_label_style, transform);
2431 }
2432 }
2433
2434 fn draw_xlabel_top(
2436 &self,
2437 renderer: &mut impl Renderer,
2438 plot_area: &Rect,
2439 theme: &Theme,
2440 ) {
2441 if let Some(xlabel) = &self.xlabel {
2442 let style = TextStyle {
2443 size: theme.axis_label_size,
2444 color: theme.text_color,
2445 weight: FontWeight::Normal,
2446 family: theme.font_family.clone(),
2447 halign: HAlign::Center,
2448 valign: VAlign::Bottom,
2449 };
2450 let x = plot_area.x + plot_area.width / 2.0;
2451 let y = plot_area.y - theme.tick_length - theme.tick_label_size - 8.0;
2452 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2453 }
2454 }
2455
2456 fn draw_labels(
2462 &self,
2463 renderer: &mut impl Renderer,
2464 plot_area: &Rect,
2465 bounds: &Rect,
2466 theme: &Theme,
2467 ) {
2468 if let Some(title) = &self.title {
2470 let style = TextStyle {
2471 size: theme.title_size,
2472 color: theme.text_color,
2473 weight: theme.title_weight,
2474 family: theme.font_family.clone(),
2475 halign: HAlign::Center,
2476 valign: VAlign::Bottom,
2477 };
2478 let x = plot_area.x + plot_area.width / 2.0;
2479 let y = plot_area.y - 10.0;
2480 renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
2481 }
2482
2483 if let Some(xlabel) = &self.xlabel {
2485 let style = TextStyle {
2486 size: theme.axis_label_size,
2487 color: theme.text_color,
2488 weight: FontWeight::Normal,
2489 family: theme.font_family.clone(),
2490 halign: HAlign::Center,
2491 valign: VAlign::Top,
2492 };
2493 let x = plot_area.x + plot_area.width / 2.0;
2494 let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
2496 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
2497 }
2498
2499 if let Some(ylabel) = &self.ylabel {
2501 let style = TextStyle {
2502 size: theme.axis_label_size,
2503 color: theme.text_color,
2504 weight: FontWeight::Normal,
2505 family: theme.font_family.clone(),
2506 halign: HAlign::Center,
2507 valign: VAlign::Bottom,
2508 };
2509 let x = bounds.x + 4.0;
2510 let y = plot_area.y + plot_area.height / 2.0;
2511 let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
2513 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
2514 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
2515 let transform = translate_to * rotate * translate_back;
2516 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
2517 }
2518 }
2519
2520 fn draw_boxplot(
2528 &self,
2529 renderer: &mut impl Renderer,
2530 artist: &BoxPlotArtist,
2531 plot_area: &Rect,
2532 xmin: f64,
2533 xmax: f64,
2534 ymin: f64,
2535 ymax: f64,
2536 ) {
2537 let n = artist.stats.len();
2538 if n == 0 {
2539 return;
2540 }
2541
2542 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2543 let stroke_color = Color::BLACK.with_alpha((artist.alpha * 255.0) as u8);
2544 let paint = Paint::new(stroke_color);
2545 let thin = Stroke::new(1.0);
2546 let thick = Stroke::new(2.0);
2547 let hair = Stroke::new(0.5);
2548
2549 for (i, stats) in artist.stats.iter().enumerate() {
2550 let cx = i as f64 + 0.5;
2551 let half = artist.box_width / 2.0;
2552 let left = cx - half;
2553 let right = cx + half;
2554
2555 let tl = self.data_to_pixel(left, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2557 let br = self.data_to_pixel(right, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2558 let box_rect_path = {
2559 let mut p = Path::new();
2560 p.move_to(tl.x, tl.y);
2561 p.line_to(br.x, tl.y);
2562 p.line_to(br.x, br.y);
2563 p.line_to(tl.x, br.y);
2564 p.close();
2565 p
2566 };
2567 renderer.fill_path(&box_rect_path, &Paint::new(fill_color), Affine::IDENTITY);
2568 renderer.stroke_path(&box_rect_path, &paint, &thin, Affine::IDENTITY);
2569
2570 let ml = self.data_to_pixel(left, stats.median, plot_area, xmin, xmax, ymin, ymax);
2572 let mr = self.data_to_pixel(right, stats.median, plot_area, xmin, xmax, ymin, ymax);
2573 let mut median_path = Path::new();
2574 median_path.move_to(ml.x, ml.y);
2575 median_path.line_to(mr.x, mr.y);
2576 renderer.stroke_path(&median_path, &paint, &thick, Affine::IDENTITY);
2577
2578 let wl_bottom = self.data_to_pixel(cx, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2580 let wl_top = self.data_to_pixel(cx, stats.q1, plot_area, xmin, xmax, ymin, ymax);
2581 let mut wl_path = Path::new();
2582 wl_path.move_to(wl_top.x, wl_top.y);
2583 wl_path.line_to(wl_bottom.x, wl_bottom.y);
2584 renderer.stroke_path(&wl_path, &paint, &thin, Affine::IDENTITY);
2585
2586 let cap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2588 let cap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_low, plot_area, xmin, xmax, ymin, ymax);
2589 let mut cap_path = Path::new();
2590 cap_path.move_to(cap_left.x, cap_left.y);
2591 cap_path.line_to(cap_right.x, cap_right.y);
2592 renderer.stroke_path(&cap_path, &paint, &thin, Affine::IDENTITY);
2593
2594 let wu_bottom = self.data_to_pixel(cx, stats.q3, plot_area, xmin, xmax, ymin, ymax);
2596 let wu_top = self.data_to_pixel(cx, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2597 let mut wu_path = Path::new();
2598 wu_path.move_to(wu_bottom.x, wu_bottom.y);
2599 wu_path.line_to(wu_top.x, wu_top.y);
2600 renderer.stroke_path(&wu_path, &paint, &thin, Affine::IDENTITY);
2601
2602 let ucap_left = self.data_to_pixel(cx - half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2604 let ucap_right = self.data_to_pixel(cx + half * 0.5, stats.whisker_high, plot_area, xmin, xmax, ymin, ymax);
2605 let mut ucap_path = Path::new();
2606 ucap_path.move_to(ucap_left.x, ucap_left.y);
2607 ucap_path.line_to(ucap_right.x, ucap_right.y);
2608 renderer.stroke_path(&ucap_path, &paint, &thin, Affine::IDENTITY);
2609
2610 if artist.show_outliers {
2612 let r = 3.0;
2613 for &val in &stats.outliers {
2614 let pt = self.data_to_pixel(cx, val, plot_area, xmin, xmax, ymin, ymax);
2615 let mut dot = Path::new();
2616 for seg in 0..8 {
2617 let angle = std::f64::consts::TAU * seg as f64 / 8.0;
2618 let dx = r * angle.cos();
2619 let dy = r * angle.sin();
2620 if seg == 0 {
2621 dot.move_to(pt.x + dx, pt.y + dy);
2622 } else {
2623 dot.line_to(pt.x + dx, pt.y + dy);
2624 }
2625 }
2626 dot.close();
2627 renderer.fill_path(&dot, &Paint::new(fill_color), Affine::IDENTITY);
2628 renderer.stroke_path(&dot, &paint, &hair, Affine::IDENTITY);
2629 }
2630 }
2631 }
2632 }
2633
2634 fn draw_step(
2636 &self,
2637 renderer: &mut impl Renderer,
2638 artist: &StepArtist,
2639 plot_area: &Rect,
2640 xmin: f64,
2641 xmax: f64,
2642 ymin: f64,
2643 ymax: f64,
2644 ) {
2645 if artist.x.len() < 2 {
2646 return;
2647 }
2648 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
2649 let paint = Paint::new(color);
2650 let stroke = Stroke::new(artist.width);
2651
2652 let mut path = Path::new();
2653 let first = self.data_to_pixel(
2654 artist.x.data[0], artist.y.data[0],
2655 plot_area, xmin, xmax, ymin, ymax,
2656 );
2657 path.move_to(first.x, first.y);
2658
2659 for i in 1..artist.x.len() {
2660 let prev = self.data_to_pixel(
2661 artist.x.data[i - 1], artist.y.data[i - 1],
2662 plot_area, xmin, xmax, ymin, ymax,
2663 );
2664 let cur = self.data_to_pixel(
2665 artist.x.data[i], artist.y.data[i],
2666 plot_area, xmin, xmax, ymin, ymax,
2667 );
2668 match artist.where_step {
2669 StepWhere::Pre => {
2670 path.line_to(prev.x, cur.y);
2671 path.line_to(cur.x, cur.y);
2672 }
2673 StepWhere::Post => {
2674 path.line_to(cur.x, prev.y);
2675 path.line_to(cur.x, cur.y);
2676 }
2677 StepWhere::Mid => {
2678 let mid_x = (prev.x + cur.x) / 2.0;
2679 path.line_to(mid_x, prev.y);
2680 path.line_to(mid_x, cur.y);
2681 path.line_to(cur.x, cur.y);
2682 }
2683 }
2684 }
2685 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
2686 }
2687
2688 fn draw_stem(
2690 &self,
2691 renderer: &mut impl Renderer,
2692 artist: &StemArtist,
2693 plot_area: &Rect,
2694 xmin: f64,
2695 xmax: f64,
2696 ymin: f64,
2697 ymax: f64,
2698 ) {
2699 if artist.x.is_empty() {
2700 return;
2701 }
2702 let alpha_byte = (artist.alpha * 255.0) as u8;
2703 let color = artist.color.with_alpha(alpha_byte);
2704 let paint = Paint::new(color);
2705 let stroke = Stroke::new(artist.line_width);
2706 let radius = artist.marker_size / 2.0;
2707
2708 let bl_left = self.data_to_pixel(
2710 artist.x.data[0], artist.baseline,
2711 plot_area, xmin, xmax, ymin, ymax,
2712 );
2713 let bl_right = self.data_to_pixel(
2714 *artist.x.data.last().unwrap(), artist.baseline,
2715 plot_area, xmin, xmax, ymin, ymax,
2716 );
2717 let mut bl_path = Path::new();
2718 bl_path.move_to(bl_left.x, bl_left.y);
2719 bl_path.line_to(bl_right.x, bl_right.y);
2720 let bl_paint = Paint::new(Color::BLACK.with_alpha(alpha_byte));
2721 let bl_stroke = Stroke::new(0.8);
2722 renderer.stroke_path(&bl_path, &bl_paint, &bl_stroke, Affine::IDENTITY);
2723
2724 for i in 0..artist.x.len() {
2726 let base = self.data_to_pixel(
2727 artist.x.data[i], artist.baseline,
2728 plot_area, xmin, xmax, ymin, ymax,
2729 );
2730 let tip = self.data_to_pixel(
2731 artist.x.data[i], artist.y.data[i],
2732 plot_area, xmin, xmax, ymin, ymax,
2733 );
2734 let mut stem_path = Path::new();
2735 stem_path.move_to(base.x, base.y);
2736 stem_path.line_to(tip.x, tip.y);
2737 renderer.stroke_path(&stem_path, &paint, &stroke, Affine::IDENTITY);
2738 let marker = Path::circle(tip, radius);
2739 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
2740 }
2741 }
2742
2743 fn draw_errorbar(
2746 &self,
2747 renderer: &mut impl Renderer,
2748 artist: &ErrorBarArtist,
2749 plot_area: &Rect,
2750 xmin: f64,
2751 xmax: f64,
2752 ymin: f64,
2753 ymax: f64,
2754 ) {
2755 if artist.x.is_empty() {
2756 return;
2757 }
2758 let paint = Paint::new(artist.color);
2759 let stroke = Stroke::new(artist.line_width);
2760 let marker_radius = 3.0;
2761
2762 let mut line_path = Path::new();
2764 let first = self.data_to_pixel(
2765 artist.x.data[0], artist.y.data[0],
2766 plot_area, xmin, xmax, ymin, ymax,
2767 );
2768 line_path.move_to(first.x, first.y);
2769 for i in 1..artist.x.len() {
2770 let pt = self.data_to_pixel(
2771 artist.x.data[i], artist.y.data[i],
2772 plot_area, xmin, xmax, ymin, ymax,
2773 );
2774 line_path.line_to(pt.x, pt.y);
2775 }
2776 renderer.stroke_path(&line_path, &paint, &stroke, Affine::IDENTITY);
2777
2778 for i in 0..artist.x.len() {
2780 let xv = artist.x.data[i];
2781 let yv = artist.y.data[i];
2782 let center = self.data_to_pixel(xv, yv, plot_area, xmin, xmax, ymin, ymax);
2783
2784 let marker = Path::circle(center, marker_radius);
2786 renderer.fill_path(&marker, &paint, Affine::IDENTITY);
2787
2788 if let Some(ref yerr) = artist.yerr {
2790 let (lo, hi) = match yerr {
2791 ErrorBarData::Symmetric(e) => (yv - e[i], yv + e[i]),
2792 ErrorBarData::Asymmetric { low, high } => (yv - low[i], yv + high[i]),
2793 };
2794 let pt_lo = self.data_to_pixel(xv, lo, plot_area, xmin, xmax, ymin, ymax);
2795 let pt_hi = self.data_to_pixel(xv, hi, plot_area, xmin, xmax, ymin, ymax);
2796
2797 let mut bar = Path::new();
2799 bar.move_to(pt_lo.x, pt_lo.y);
2800 bar.line_to(pt_hi.x, pt_hi.y);
2801 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
2802
2803 if artist.cap_size > 0.0 {
2805 let half_cap = artist.cap_size / 2.0;
2806 let mut cap_lo = Path::new();
2807 cap_lo.move_to(pt_lo.x - half_cap, pt_lo.y);
2808 cap_lo.line_to(pt_lo.x + half_cap, pt_lo.y);
2809 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
2810
2811 let mut cap_hi = Path::new();
2812 cap_hi.move_to(pt_hi.x - half_cap, pt_hi.y);
2813 cap_hi.line_to(pt_hi.x + half_cap, pt_hi.y);
2814 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
2815 }
2816 }
2817
2818 if let Some(ref xerr) = artist.xerr {
2820 let (lo, hi) = match xerr {
2821 ErrorBarData::Symmetric(e) => (xv - e[i], xv + e[i]),
2822 ErrorBarData::Asymmetric { low, high } => (xv - low[i], xv + high[i]),
2823 };
2824 let pt_lo = self.data_to_pixel(lo, yv, plot_area, xmin, xmax, ymin, ymax);
2825 let pt_hi = self.data_to_pixel(hi, yv, plot_area, xmin, xmax, ymin, ymax);
2826
2827 let mut bar = Path::new();
2829 bar.move_to(pt_lo.x, pt_lo.y);
2830 bar.line_to(pt_hi.x, pt_hi.y);
2831 renderer.stroke_path(&bar, &paint, &stroke, Affine::IDENTITY);
2832
2833 if artist.cap_size > 0.0 {
2835 let half_cap = artist.cap_size / 2.0;
2836 let mut cap_lo = Path::new();
2837 cap_lo.move_to(pt_lo.x, pt_lo.y - half_cap);
2838 cap_lo.line_to(pt_lo.x, pt_lo.y + half_cap);
2839 renderer.stroke_path(&cap_lo, &paint, &stroke, Affine::IDENTITY);
2840
2841 let mut cap_hi = Path::new();
2842 cap_hi.move_to(pt_hi.x, pt_hi.y - half_cap);
2843 cap_hi.line_to(pt_hi.x, pt_hi.y + half_cap);
2844 renderer.stroke_path(&cap_hi, &paint, &stroke, Affine::IDENTITY);
2845 }
2846 }
2847 }
2848 }
2849
2850 fn draw_heatmap(
2853 &self,
2854 renderer: &mut impl Renderer,
2855 artist: &HeatmapArtist,
2856 plot_area: &Rect,
2857 xmin: f64,
2858 xmax: f64,
2859 ymin: f64,
2860 ymax: f64,
2861 ) {
2862 let nrows = artist.data.len();
2863 if nrows == 0 {
2864 return;
2865 }
2866 let ncols = artist.data[0].len();
2867 if ncols == 0 {
2868 return;
2869 }
2870
2871 let vmin = artist.effective_vmin();
2872 let vmax = artist.effective_vmax();
2873
2874 let text_style = TextStyle {
2875 size: 10.0,
2876 color: Color::BLACK,
2877 weight: FontWeight::Normal,
2878 family: None,
2879 halign: HAlign::Center,
2880 valign: VAlign::Middle,
2881 };
2882
2883 for row in 0..nrows {
2884 for col in 0..ncols {
2885 let val = artist.data[row][col];
2886 let cell_color = artist.cmap.map_value(val, vmin, vmax);
2887
2888 let p_bl = self.data_to_pixel(
2890 col as f64, row as f64,
2891 plot_area, xmin, xmax, ymin, ymax,
2892 );
2893 let p_tr = self.data_to_pixel(
2894 (col + 1) as f64, (row + 1) as f64,
2895 plot_area, xmin, xmax, ymin, ymax,
2896 );
2897 let rect = Rect::from_points(p_bl, p_tr);
2898 let cell_path = Path::rect(rect);
2899 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
2900
2901 if artist.show_values {
2903 let cx = (p_bl.x + p_tr.x) / 2.0;
2904 let cy = (p_bl.y + p_tr.y) / 2.0;
2905 let label = format!("{val:.1}");
2906 renderer.draw_text(
2907 &label,
2908 Point::new(cx, cy),
2909 &text_style,
2910 Affine::IDENTITY,
2911 );
2912 }
2913 }
2914 }
2915 }
2916
2917 fn draw_annotations(
2923 &self,
2924 renderer: &mut impl Renderer,
2925 plot_area: &Rect,
2926 xmin: f64,
2927 xmax: f64,
2928 ymin: f64,
2929 ymax: f64,
2930 theme: &Theme,
2931 ) {
2932 for ta in &self.texts {
2934 let pt = self.data_to_pixel(ta.x, ta.y, plot_area, xmin, xmax, ymin, ymax);
2935 let size = ta.fontsize.unwrap_or(theme.axis_label_size);
2936 let color = ta.color.unwrap_or(theme.text_color);
2937 let style = TextStyle {
2938 size,
2939 color,
2940 weight: FontWeight::Normal,
2941 family: theme.font_family.clone(),
2942 halign: ta.ha,
2943 valign: ta.va,
2944 };
2945 if ta.rotation.abs() < f64::EPSILON {
2946 renderer.draw_text(&ta.text, pt, &style, Affine::IDENTITY);
2947 } else {
2948 let angle_rad = -ta.rotation.to_radians();
2949 let rotate = Affine::rotate(angle_rad);
2950 let translate_to = Affine::translate(kurbo::Vec2::new(pt.x, pt.y));
2951 let translate_back = Affine::translate(kurbo::Vec2::new(-pt.x, -pt.y));
2952 let transform = translate_to * rotate * translate_back;
2953 renderer.draw_text(&ta.text, pt, &style, transform);
2954 }
2955 }
2956
2957 for ann in &self.annotations {
2959 let text_pt = self.data_to_pixel(
2960 ann.xytext.0, ann.xytext.1,
2961 plot_area, xmin, xmax, ymin, ymax,
2962 );
2963 let target_pt = self.data_to_pixel(
2964 ann.xy.0, ann.xy.1,
2965 plot_area, xmin, xmax, ymin, ymax,
2966 );
2967
2968 let size = ann.fontsize.unwrap_or(theme.axis_label_size);
2969 let color = ann.color.unwrap_or(theme.text_color);
2970 let style = TextStyle {
2971 size,
2972 color,
2973 weight: FontWeight::Normal,
2974 family: theme.font_family.clone(),
2975 halign: ann.ha,
2976 valign: ann.va,
2977 };
2978 renderer.draw_text(&ann.text, text_pt, &style, Affine::IDENTITY);
2979
2980 if ann.arrowstyle != ArrowStyle::None {
2982 let arrow_col = ann.arrow_color.unwrap_or(color);
2983 self.draw_annotation_arrow(
2984 renderer, text_pt, target_pt, arrow_col, &ann.arrowstyle,
2985 );
2986 }
2987 }
2988 }
2989
2990 fn draw_annotation_arrow(
2992 &self,
2993 renderer: &mut impl Renderer,
2994 from: Point,
2995 to: Point,
2996 color: Color,
2997 style: &ArrowStyle,
2998 ) {
2999 let paint = Paint::new(color);
3000 let stroke = Stroke::new(1.0);
3001
3002 let dx = to.x - from.x;
3004 let dy = to.y - from.y;
3005 let len = (dx * dx + dy * dy).sqrt();
3006 if len < 1e-6 {
3007 return;
3008 }
3009
3010 let ux = dx / len;
3012 let uy = dy / len;
3013
3014 let mut line = Path::new();
3016 line.move_to(from.x, from.y);
3017 line.line_to(to.x, to.y);
3018 renderer.stroke_path(&line, &paint, &stroke, Affine::IDENTITY);
3019
3020 let head_len = match style {
3022 ArrowStyle::None => return,
3023 ArrowStyle::Simple => 8.0,
3024 ArrowStyle::Fancy => 12.0,
3025 };
3026 let head_half_width = match style {
3027 ArrowStyle::None => return,
3028 ArrowStyle::Simple => 3.0,
3029 ArrowStyle::Fancy => 5.0,
3030 };
3031
3032 let px = -uy;
3034 let py = ux;
3035
3036 let base_x = to.x - ux * head_len;
3038 let base_y = to.y - uy * head_len;
3039
3040 let left_x = base_x + px * head_half_width;
3041 let left_y = base_y + py * head_half_width;
3042 let right_x = base_x - px * head_half_width;
3043 let right_y = base_y - py * head_half_width;
3044
3045 let mut arrow = Path::new();
3046 arrow.move_to(to.x, to.y);
3047 arrow.line_to(left_x, left_y);
3048 arrow.line_to(right_x, right_y);
3049 arrow.close();
3050 renderer.fill_path(&arrow, &paint, Affine::IDENTITY);
3051 }
3052
3053 fn draw_pie(
3059 &self,
3060 renderer: &mut impl Renderer,
3061 artist: &PieArtist,
3062 plot_area: &Rect,
3063 xmin: f64,
3064 xmax: f64,
3065 ymin: f64,
3066 ymax: f64,
3067 theme: &Theme,
3068 ) {
3069 let n = artist.sizes.len();
3070 if n == 0 {
3071 return;
3072 }
3073
3074 let total: f64 = artist.sizes.iter().copied().filter(|v| v.is_finite() && *v > 0.0).sum();
3076 if total <= 0.0 {
3077 return;
3078 }
3079 let fractions: Vec<f64> = artist.sizes.iter().map(|&s| {
3080 if s.is_finite() && s > 0.0 { s / total } else { 0.0 }
3081 }).collect();
3082
3083 let center_px = self.data_to_pixel(0.0, 0.0, plot_area, xmin, xmax, ymin, ymax);
3085
3086 let edge_px = self.data_to_pixel(artist.radius, 0.0, plot_area, xmin, xmax, ymin, ymax);
3088 let radius_px = (edge_px.x - center_px.x).abs();
3089
3090 let start_rad = artist.start_angle.to_radians();
3091 let mut current_angle = start_rad;
3092
3093 let pct_style = TextStyle {
3094 size: 10.0,
3095 color: Color::BLACK,
3096 weight: FontWeight::Normal,
3097 family: None,
3098 halign: HAlign::Center,
3099 valign: VAlign::Middle,
3100 };
3101 let label_style = TextStyle {
3102 size: 11.0,
3103 color: theme.tick_color,
3104 weight: FontWeight::Normal,
3105 family: None,
3106 halign: HAlign::Center,
3107 valign: VAlign::Middle,
3108 };
3109
3110 for i in 0..n {
3111 let frac = fractions[i];
3112 if frac <= 0.0 {
3113 current_angle += frac * std::f64::consts::TAU;
3114 continue;
3115 }
3116
3117 let sweep = frac * std::f64::consts::TAU;
3118 let mid_angle = current_angle + sweep / 2.0;
3119
3120 let wedge_color = if let Some(ref colors) = artist.colors {
3122 colors[i % colors.len()]
3123 } else {
3124 Color::TABLEAU_10[i % 10]
3125 };
3126
3127 let explode_frac = artist.explode.as_ref().map(|e| {
3129 if i < e.len() { e[i] } else { 0.0 }
3130 }).unwrap_or(0.0);
3131 let offset_x = explode_frac * radius_px * mid_angle.cos();
3132 let offset_y = explode_frac * radius_px * (-mid_angle.sin()); let cx = center_px.x + offset_x;
3135 let cy = center_px.y + offset_y;
3136
3137 let mut path = Path::new();
3140 path.move_to(cx, cy);
3141
3142 let arc_start_x = cx + radius_px * current_angle.cos();
3143 let arc_start_y = cy - radius_px * current_angle.sin();
3144 path.line_to(arc_start_x, arc_start_y);
3145
3146 let max_sub = std::f64::consts::FRAC_PI_2;
3148 let num_segments = (sweep / max_sub).ceil() as usize;
3149 let seg_sweep = sweep / num_segments as f64;
3150 let mut seg_start = current_angle;
3151
3152 for _ in 0..num_segments {
3153 let seg_end = seg_start + seg_sweep;
3154 let half = seg_sweep / 2.0;
3156 let alpha = (4.0 / 3.0) * (half / 2.0).tan();
3157
3158 let p0x = cx + radius_px * seg_start.cos();
3159 let p0y = cy - radius_px * seg_start.sin();
3160 let p3x = cx + radius_px * seg_end.cos();
3161 let p3y = cy - radius_px * seg_end.sin();
3162
3163 let t0x = -seg_start.sin();
3165 let t0y = -seg_start.cos(); let t1x = -seg_end.sin();
3168 let t1y = -seg_end.cos(); let cp1x = p0x + alpha * radius_px * t0x;
3171 let cp1y = p0y + alpha * radius_px * t0y;
3172 let cp2x = p3x - alpha * radius_px * t1x;
3173 let cp2y = p3y - alpha * radius_px * t1y;
3174
3175 let _ = path.curve_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
3176 seg_start = seg_end;
3177 }
3178
3179 path.close();
3180
3181 let paint = Paint::new(wedge_color);
3182 renderer.fill_path(&path, &paint, Affine::IDENTITY);
3183
3184 let outline_paint = Paint::new(Color::WHITE);
3186 let outline_stroke = Stroke::new(1.5);
3187 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
3188
3189 if artist.autopct {
3191 let pct_r = radius_px * 0.6;
3192 let pct_x = cx + pct_r * mid_angle.cos();
3193 let pct_y = cy - pct_r * mid_angle.sin();
3194 let pct_text = format!("{:.1}%", frac * 100.0);
3195 renderer.draw_text(
3196 &pct_text,
3197 Point::new(pct_x, pct_y),
3198 &pct_style,
3199 Affine::IDENTITY,
3200 );
3201 }
3202
3203 if let Some(ref labels) = artist.labels {
3205 if i < labels.len() {
3206 let label_r = radius_px * 1.15;
3207 let lx = cx + label_r * mid_angle.cos();
3208 let ly = cy - label_r * mid_angle.sin();
3209 renderer.draw_text(
3210 &labels[i],
3211 Point::new(lx, ly),
3212 &label_style,
3213 Affine::IDENTITY,
3214 );
3215 }
3216 }
3217
3218 current_angle += sweep;
3219 }
3220 }
3221
3222 fn draw_contour(
3224 &self,
3225 renderer: &mut impl Renderer,
3226 artist: &ContourArtist,
3227 plot_area: &Rect,
3228 xmin: f64,
3229 xmax: f64,
3230 ymin: f64,
3231 ymax: f64,
3232 ) {
3233 let nx = artist.x.len();
3234 let ny = artist.y.len();
3235 if nx < 2 || ny < 2 || artist.z.len() < 2 {
3236 return;
3237 }
3238
3239 let levels = artist.effective_levels();
3240 let (zmin, zmax) = artist.z_bounds();
3241
3242 if artist.filled {
3243 let avgs = artist.cell_averages();
3244 for (j, row) in avgs.iter().enumerate() {
3245 for (i, &avg) in row.iter().enumerate() {
3246 if !avg.is_finite() {
3247 continue;
3248 }
3249 let cell_color = if let Some(ref colors) = artist.colors {
3250 let idx = levels
3251 .iter()
3252 .position(|&l| avg < l)
3253 .unwrap_or(levels.len())
3254 .saturating_sub(1);
3255 colors[idx % colors.len()]
3256 } else {
3257 artist.cmap.map_value(avg, zmin, zmax)
3258 };
3259 let p_bl = self.data_to_pixel(
3260 artist.x[i], artist.y[j],
3261 plot_area, xmin, xmax, ymin, ymax,
3262 );
3263 let p_tr = self.data_to_pixel(
3264 artist.x[i + 1], artist.y[j + 1],
3265 plot_area, xmin, xmax, ymin, ymax,
3266 );
3267 let rect = Rect::from_points(p_bl, p_tr);
3268 let cell_path = Path::rect(rect);
3269 renderer.fill_path(&cell_path, &Paint::new(cell_color), Affine::IDENTITY);
3270 }
3271 }
3272 }
3273
3274 if !artist.filled {
3275 for (li, &level) in levels.iter().enumerate() {
3276 let segments = artist.marching_squares(level);
3277 if segments.is_empty() {
3278 continue;
3279 }
3280 let line_color = if let Some(ref colors) = artist.colors {
3281 colors[li % colors.len()]
3282 } else {
3283 artist.cmap.map_value(level, zmin, zmax)
3284 };
3285 let paint = Paint::new(line_color);
3286 let stroke = Stroke::new(artist.linewidths);
3287 for (sx0, sy0, sx1, sy1) in &segments {
3288 let p0 = self.data_to_pixel(*sx0, *sy0, plot_area, xmin, xmax, ymin, ymax);
3289 let p1 = self.data_to_pixel(*sx1, *sy1, plot_area, xmin, xmax, ymin, ymax);
3290 let mut path = Path::new();
3291 path.move_to(p0.x, p0.y);
3292 path.line_to(p1.x, p1.y);
3293 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
3294 }
3295 }
3296 }
3297 }
3298
3299 fn draw_violin(
3301 &self,
3302 renderer: &mut impl Renderer,
3303 artist: &ViolinArtist,
3304 plot_area: &Rect,
3305 xmin: f64,
3306 xmax: f64,
3307 ymin: f64,
3308 ymax: f64,
3309 theme: &Theme,
3310 ) {
3311 use crate::charts::violin::{gaussian_kde, silverman_bandwidth};
3312
3313 let fill_color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
3314 let fill_paint = Paint::new(fill_color);
3315 let outline_paint = Paint::new(artist.color);
3316 let outline_stroke = Stroke::new(1.0);
3317
3318 for (di, data) in artist.datasets.iter().enumerate() {
3319 let mut sorted: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
3320 if sorted.is_empty() {
3321 continue;
3322 }
3323 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
3324
3325 let pos = artist.positions.as_ref()
3326 .and_then(|p| p.get(di).copied())
3327 .unwrap_or(di as f64 + 1.0);
3328
3329 let bw = if artist.bw_method > 0.0 {
3330 artist.bw_method
3331 } else {
3332 silverman_bandwidth(&sorted)
3333 };
3334
3335 let data_min = sorted[0];
3336 let data_max = sorted[sorted.len() - 1];
3337 let n_eval = 100;
3338 let eval_points: Vec<f64> = (0..n_eval)
3339 .map(|i| data_min + (data_max - data_min) * i as f64 / (n_eval - 1) as f64)
3340 .collect();
3341 let densities = gaussian_kde(&sorted, bw, &eval_points);
3342
3343 let max_density = densities.iter().copied().fold(0.0_f64, f64::max);
3344 if max_density <= 0.0 {
3345 continue;
3346 }
3347
3348 let half_width = artist.widths * 0.5;
3349
3350 let mut path = Path::new();
3352 let first_y = eval_points[0];
3354 let first_w = densities[0] / max_density * half_width;
3355 let fp = self.data_to_pixel(pos + first_w, first_y, plot_area, xmin, xmax, ymin, ymax);
3356 path.move_to(fp.x, fp.y);
3357 for i in 1..n_eval {
3358 let w = densities[i] / max_density * half_width;
3359 let p = self.data_to_pixel(pos + w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
3360 path.line_to(p.x, p.y);
3361 }
3362 for i in (0..n_eval).rev() {
3364 let w = densities[i] / max_density * half_width;
3365 let p = self.data_to_pixel(pos - w, eval_points[i], plot_area, xmin, xmax, ymin, ymax);
3366 path.line_to(p.x, p.y);
3367 }
3368 path.close();
3369
3370 renderer.fill_path(&path, &fill_paint, Affine::IDENTITY);
3371 renderer.stroke_path(&path, &outline_paint, &outline_stroke, Affine::IDENTITY);
3372
3373 let n = sorted.len();
3375 if artist.show_median {
3376 let median = if n % 2 == 0 {
3377 (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
3378 } else {
3379 sorted[n / 2]
3380 };
3381 let med_density = gaussian_kde(&sorted, bw, &[median])[0];
3382 let med_w = med_density / max_density * half_width;
3383 let p1 = self.data_to_pixel(pos - med_w, median, plot_area, xmin, xmax, ymin, ymax);
3384 let p2 = self.data_to_pixel(pos + med_w, median, plot_area, xmin, xmax, ymin, ymax);
3385 let mut mp = Path::new();
3386 mp.move_to(p1.x, p1.y);
3387 mp.line_to(p2.x, p2.y);
3388 let med_paint = Paint::new(theme.text_color);
3389 let med_stroke = Stroke::new(2.0);
3390 renderer.stroke_path(&mp, &med_paint, &med_stroke, Affine::IDENTITY);
3391 }
3392
3393 if artist.show_quartiles && n >= 4 {
3394 let q1 = sorted[n / 4];
3395 let q3 = sorted[3 * n / 4];
3396 for q in [q1, q3] {
3397 let q_density = gaussian_kde(&sorted, bw, &[q])[0];
3398 let q_w = q_density / max_density * half_width;
3399 let p1 = self.data_to_pixel(pos - q_w, q, plot_area, xmin, xmax, ymin, ymax);
3400 let p2 = self.data_to_pixel(pos + q_w, q, plot_area, xmin, xmax, ymin, ymax);
3401 let mut qp = Path::new();
3402 qp.move_to(p1.x, p1.y);
3403 qp.line_to(p2.x, p2.y);
3404 let q_stroke = Stroke::new(1.0).with_dash(DashPattern { dashes: vec![4.0, 2.0], offset: 0.0 });
3405 renderer.stroke_path(&qp, &Paint::new(theme.text_color), &q_stroke, Affine::IDENTITY);
3406 }
3407 }
3408 }
3409 }
3410
3411 fn draw_legend(
3412 &self,
3413 renderer: &mut impl Renderer,
3414 plot_area: &Rect,
3415 theme: &Theme,
3416 ) {
3417 let entries: Vec<LegendEntry> = self
3420 .artists
3421 .iter()
3422 .filter_map(|a| {
3423 let (label, color, swatch) = match a {
3424 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
3425 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3426 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3427 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3428 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3429 Artist::Step(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
3430 Artist::Stem(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3431 Artist::BoxPlot(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3432 Artist::ErrorBar(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
3433 Artist::Heatmap(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3434 Artist::Pie(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3435 Artist::Violin(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
3436 Artist::Contour(a) => (a.label.as_deref(), a.color, if a.filled { SwatchKind::Filled } else { SwatchKind::Line }),
3437 };
3438 label.map(|l| LegendEntry { label: l.to_string(), color, swatch })
3439 })
3440 .collect();
3441
3442 legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
3443 }
3444
3445 fn data_to_pixel(
3455 &self,
3456 x: f64,
3457 y: f64,
3458 plot_area: &Rect,
3459 xmin: f64,
3460 xmax: f64,
3461 ymin: f64,
3462 ymax: f64,
3463 ) -> Point {
3464 let tx = self.xscale.transform(x, xmin, xmax);
3465 let ty = self.yscale.transform(y, ymin, ymax);
3466 Point::new(
3467 plot_area.x + tx * plot_area.width,
3468 plot_area.y + (1.0 - ty) * plot_area.height, )
3470 }
3471}
3472
3473#[cfg(test)]
3478mod tests {
3479 use super::*;
3480
3481 #[test]
3482 fn new_axes_has_defaults() {
3483 let ax = Axes::new();
3484 assert!(ax.artists.is_empty());
3485 assert!(ax.title.is_none());
3486 assert!(ax.xlabel.is_none());
3487 assert!(ax.ylabel.is_none());
3488 assert!(ax.xlim.is_none());
3489 assert!(ax.ylim.is_none());
3490 assert!(!ax.show_legend);
3491 assert_eq!(ax.color_index, 0);
3492 }
3493
3494 #[test]
3495 fn plot_creates_line_artist() {
3496 let mut ax = Axes::new();
3497 let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
3498 assert!(result.is_ok());
3499 assert_eq!(ax.artists.len(), 1);
3500 assert!(matches!(&ax.artists[0], Artist::Line(_)));
3501 assert_eq!(ax.color_index, 1);
3502 }
3503
3504 #[test]
3505 fn plot_length_mismatch() {
3506 let mut ax = Axes::new();
3507 let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
3508 assert!(matches!(
3509 result,
3510 Err(PlotError::SeriesLengthMismatch { expected: 2, got: 1 })
3511 ));
3512 }
3513
3514 #[test]
3515 fn plot_empty_data() {
3516 let mut ax = Axes::new();
3517 let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
3518 assert!(matches!(result, Err(PlotError::EmptyData)));
3519 }
3520
3521 #[test]
3522 fn scatter_creates_artist() {
3523 let mut ax = Axes::new();
3524 let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
3525 assert!(result.is_ok());
3526 assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
3527 }
3528
3529 #[test]
3530 fn bar_creates_artist() {
3531 let mut ax = Axes::new();
3532 let cats: &[&str] = &["a", "b", "c"];
3533 let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
3534 assert!(result.is_ok());
3535 match &ax.artists[0] {
3536 Artist::Bar(a) => {
3537 assert!(!a.horizontal);
3538 assert_eq!(a.categories.len(), 3);
3539 }
3540 _ => panic!("expected Bar artist"),
3541 }
3542 }
3543
3544 #[test]
3545 fn barh_creates_horizontal_artist() {
3546 let mut ax = Axes::new();
3547 let cats: &[&str] = &["x", "y"];
3548 let result = ax.barh(cats, vec![5.0, 10.0]);
3549 assert!(result.is_ok());
3550 match &ax.artists[0] {
3551 Artist::Bar(a) => assert!(a.horizontal),
3552 _ => panic!("expected Bar artist"),
3553 }
3554 }
3555
3556 #[test]
3557 fn hist_computes_bins() {
3558 let mut ax = Axes::new();
3559 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
3560 let result = ax.hist(data, 5);
3561 assert!(result.is_ok());
3562 match &ax.artists[0] {
3563 Artist::Histogram(a) => {
3564 assert_eq!(a.bin_edges.len(), 6); assert_eq!(a.counts.len(), 5);
3566 let total: f64 = a.counts.iter().sum();
3568 assert_eq!(total, 10.0);
3569 }
3570 _ => panic!("expected Hist artist"),
3571 }
3572 }
3573
3574 #[test]
3575 fn hist_single_value() {
3576 let mut ax = Axes::new();
3577 let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
3578 assert!(result.is_ok());
3579 match &ax.artists[0] {
3580 Artist::Histogram(a) => {
3581 let total: f64 = a.counts.iter().sum();
3582 assert_eq!(total, 3.0);
3583 }
3584 _ => panic!("expected Hist artist"),
3585 }
3586 }
3587
3588 #[test]
3589 fn hist_empty_data() {
3590 let mut ax = Axes::new();
3591 let result = ax.hist(Vec::<f64>::new(), 10);
3592 assert!(matches!(result, Err(PlotError::EmptyData)));
3593 }
3594
3595 #[test]
3596 fn fill_between_creates_artist() {
3597 let mut ax = Axes::new();
3598 let result = ax.fill_between(
3599 vec![1.0, 2.0, 3.0],
3600 vec![1.0, 2.0, 1.0],
3601 vec![0.0, 0.0, 0.0],
3602 );
3603 assert!(result.is_ok());
3604 assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
3605 }
3606
3607 #[test]
3608 fn fill_between_length_mismatch() {
3609 let mut ax = Axes::new();
3610 let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
3611 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
3612 }
3613
3614 #[test]
3615 fn configuration_methods_return_self() {
3616 let mut ax = Axes::new();
3617 ax.set_title("Test")
3618 .set_xlabel("X")
3619 .set_ylabel("Y")
3620 .set_xlim(0.0, 10.0)
3621 .set_ylim(-1.0, 1.0)
3622 .set_xscale(Scale::Linear)
3623 .set_yscale(Scale::Log10)
3624 .grid(true)
3625 .legend();
3626
3627 assert_eq!(ax.title.as_deref(), Some("Test"));
3628 assert_eq!(ax.xlabel.as_deref(), Some("X"));
3629 assert_eq!(ax.ylabel.as_deref(), Some("Y"));
3630 assert_eq!(ax.xlim, Some((0.0, 10.0)));
3631 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
3632 assert_eq!(ax.show_grid, Some(true));
3633 assert!(ax.show_legend);
3634 }
3635
3636 #[test]
3637 fn color_cycle_advances() {
3638 let mut ax = Axes::new();
3639 for _ in 0..12 {
3640 ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3641 }
3642 assert_eq!(ax.color_index, 12);
3643 match (&ax.artists[0], &ax.artists[10]) {
3646 (Artist::Line(a), Artist::Line(b)) => {
3647 assert_eq!(a.color, b.color);
3648 }
3649 _ => panic!("expected Line artists"),
3650 }
3651 }
3652
3653 #[test]
3654 fn data_to_pixel_linear() {
3655 let ax = Axes::new();
3656 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
3657
3658 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
3660 assert!((p.x - 100.0).abs() < 1e-10);
3661 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);
3665 assert!((p.x - 500.0).abs() < 1e-10);
3666 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);
3670 assert!((p.x - 300.0).abs() < 1e-10);
3671 assert!((p.y - 200.0).abs() < 1e-10);
3672 }
3673
3674 #[test]
3675 fn compute_data_limits_no_artists() {
3676 let ax = Axes::new();
3677 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
3678 assert!(xmin < xmax);
3680 assert!(ymin < ymax);
3681 }
3682
3683 #[test]
3684 fn compute_data_limits_with_user_override() {
3685 let mut ax = Axes::new();
3686 ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
3687 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
3688 assert!((xmin - (-5.0)).abs() < f64::EPSILON);
3689 assert!((xmax - 5.0).abs() < f64::EPSILON);
3690 assert!((ymin - 0.0).abs() < f64::EPSILON);
3691 assert!((ymax - 100.0).abs() < f64::EPSILON);
3692 }
3693
3694 #[test]
3695 fn compute_data_limits_from_line_data() {
3696 let mut ax = Axes::new();
3697 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
3698 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
3699 assert!(xmin < 1.0);
3701 assert!(xmax > 10.0);
3702 assert!(ymin < 2.0);
3703 assert!(ymax > 8.0);
3704 }
3705 #[test]
3708 fn step_creates_artist() {
3709 let mut ax = Axes::new();
3710 let result = ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
3711 assert!(result.is_ok());
3712 assert!(matches!(&ax.artists[0], Artist::Step(_)));
3713 }
3714
3715 #[test]
3716 fn step_length_mismatch() {
3717 let mut ax = Axes::new();
3718 let result = ax.step(vec![1.0, 2.0], vec![1.0]);
3719 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
3720 }
3721
3722 #[test]
3723 fn step_empty_data() {
3724 let mut ax = Axes::new();
3725 let result = ax.step(Vec::<f64>::new(), Vec::<f64>::new());
3726 assert!(matches!(result, Err(PlotError::EmptyData)));
3727 }
3728
3729 #[test]
3730 fn step_default_where() {
3731 let mut ax = Axes::new();
3732 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3733 match &ax.artists[0] {
3734 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Pre)),
3735 _ => panic!("expected Step"),
3736 }
3737 }
3738
3739 #[test]
3740 fn step_color_cycle() {
3741 let mut ax = Axes::new();
3742 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3743 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3744 let c0 = ax.artists[0].color();
3745 let c1 = ax.artists[1].color();
3746 assert_ne!(c0, c1);
3747 }
3748
3749 #[test]
3750 fn step_builder_chaining() {
3751 let mut ax = Axes::new();
3752 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
3753 .unwrap()
3754 .color(Color::TAB_RED)
3755 .width(3.0)
3756 .where_step(StepWhere::Post)
3757 .label("steps")
3758 .alpha(0.5);
3759 match &ax.artists[0] {
3760 Artist::Step(a) => {
3761 assert_eq!(a.color, Color::TAB_RED);
3762 assert!((a.width - 3.0).abs() < 1e-12);
3763 assert!(matches!(a.where_step, StepWhere::Post));
3764 assert_eq!(a.label.as_deref(), Some("steps"));
3765 assert!((a.alpha - 0.5).abs() < 1e-12);
3766 }
3767 _ => panic!("expected Step"),
3768 }
3769 }
3770
3771 #[test]
3772 fn step_data_bounds() {
3773 let mut ax = Axes::new();
3774 ax.step(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
3775 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
3776 assert!(xmin < 1.0);
3777 assert!(xmax > 10.0);
3778 assert!(ymin < 2.0);
3779 assert!(ymax > 8.0);
3780 }
3781
3782 #[test]
3783 fn step_legend_label() {
3784 let mut ax = Axes::new();
3785 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("S");
3786 assert_eq!(ax.artists[0].label(), Some("S"));
3787 }
3788
3789 #[test]
3790 fn step_default_alpha() {
3791 let mut ax = Axes::new();
3792 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3793 match &ax.artists[0] {
3794 Artist::Step(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
3795 _ => panic!("expected Step"),
3796 }
3797 }
3798
3799 #[test]
3800 fn step_default_width() {
3801 let mut ax = Axes::new();
3802 ax.step(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3803 match &ax.artists[0] {
3804 Artist::Step(a) => assert!((a.width - 1.5).abs() < 1e-12),
3805 _ => panic!("expected Step"),
3806 }
3807 }
3808
3809 #[test]
3810 fn step_mid_mode() {
3811 let mut ax = Axes::new();
3812 ax.step(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
3813 .unwrap()
3814 .where_step(StepWhere::Mid);
3815 match &ax.artists[0] {
3816 Artist::Step(a) => assert!(matches!(a.where_step, StepWhere::Mid)),
3817 _ => panic!("expected Step"),
3818 }
3819 }
3820
3821 #[test]
3824 fn stem_creates_artist() {
3825 let mut ax = Axes::new();
3826 let result = ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0]);
3827 assert!(result.is_ok());
3828 assert!(matches!(&ax.artists[0], Artist::Stem(_)));
3829 }
3830
3831 #[test]
3832 fn stem_length_mismatch() {
3833 let mut ax = Axes::new();
3834 let result = ax.stem(vec![1.0, 2.0], vec![1.0]);
3835 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
3836 }
3837
3838 #[test]
3839 fn stem_empty_data() {
3840 let mut ax = Axes::new();
3841 let result = ax.stem(Vec::<f64>::new(), Vec::<f64>::new());
3842 assert!(matches!(result, Err(PlotError::EmptyData)));
3843 }
3844
3845 #[test]
3846 fn stem_default_baseline() {
3847 let mut ax = Axes::new();
3848 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3849 match &ax.artists[0] {
3850 Artist::Stem(a) => assert!((a.baseline - 0.0).abs() < 1e-12),
3851 _ => panic!("expected Stem"),
3852 }
3853 }
3854
3855 #[test]
3856 fn stem_default_marker_size() {
3857 let mut ax = Axes::new();
3858 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3859 match &ax.artists[0] {
3860 Artist::Stem(a) => assert!((a.marker_size - 6.0).abs() < 1e-12),
3861 _ => panic!("expected Stem"),
3862 }
3863 }
3864
3865 #[test]
3866 fn stem_builder_chaining() {
3867 let mut ax = Axes::new();
3868 ax.stem(vec![1.0, 2.0, 3.0], vec![1.0, 3.0, 2.0])
3869 .unwrap()
3870 .color(Color::TAB_GREEN)
3871 .baseline(1.0)
3872 .marker_size(8.0)
3873 .width(2.0)
3874 .label("stems")
3875 .alpha(0.7);
3876 match &ax.artists[0] {
3877 Artist::Stem(a) => {
3878 assert_eq!(a.color, Color::TAB_GREEN);
3879 assert!((a.baseline - 1.0).abs() < 1e-12);
3880 assert!((a.marker_size - 8.0).abs() < 1e-12);
3881 assert!((a.line_width - 2.0).abs() < 1e-12);
3882 assert_eq!(a.label.as_deref(), Some("stems"));
3883 assert!((a.alpha - 0.7).abs() < 1e-12);
3884 }
3885 _ => panic!("expected Stem"),
3886 }
3887 }
3888
3889 #[test]
3890 fn stem_data_bounds_include_baseline() {
3891 let mut ax = Axes::new();
3892 ax.stem(vec![1.0, 5.0], vec![2.0, 8.0]).unwrap().baseline(-5.0);
3893 let (_xmin, _xmax, ymin, _ymax) = ax.compute_data_limits();
3894 assert!(ymin < -5.0);
3895 }
3896
3897 #[test]
3898 fn stem_legend_label() {
3899 let mut ax = Axes::new();
3900 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().label("L");
3901 assert_eq!(ax.artists[0].label(), Some("L"));
3902 }
3903
3904 #[test]
3905 fn stem_color_cycle() {
3906 let mut ax = Axes::new();
3907 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3908 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3909 let c0 = ax.artists[0].color();
3910 let c1 = ax.artists[1].color();
3911 assert_ne!(c0, c1);
3912 }
3913
3914 #[test]
3915 fn stem_alpha_default() {
3916 let mut ax = Axes::new();
3917 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
3918 match &ax.artists[0] {
3919 Artist::Stem(a) => assert!((a.alpha - 1.0).abs() < 1e-12),
3920 _ => panic!("expected Stem"),
3921 }
3922 }
3923
3924 #[test]
3925 fn stem_negative_baseline() {
3926 let mut ax = Axes::new();
3927 ax.stem(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap().baseline(-3.0);
3928 match &ax.artists[0] {
3929 Artist::Stem(a) => assert!((a.baseline - (-3.0)).abs() < 1e-12),
3930 _ => panic!("expected Stem"),
3931 }
3932 }
3933
3934 #[test]
3937 fn text_creates_annotation() {
3938 let mut ax = Axes::new();
3939 ax.text(1.0, 2.0, "hello");
3940 assert_eq!(ax.texts.len(), 1);
3941 assert_eq!(ax.texts[0].text, "hello");
3942 assert!((ax.texts[0].x - 1.0).abs() < f64::EPSILON);
3943 assert!((ax.texts[0].y - 2.0).abs() < f64::EPSILON);
3944 }
3945
3946 #[test]
3947 fn text_default_alignment() {
3948 let mut ax = Axes::new();
3949 ax.text(0.0, 0.0, "test");
3950 assert_eq!(ax.texts[0].ha, HAlign::Left);
3951 assert_eq!(ax.texts[0].va, VAlign::Baseline);
3952 assert!((ax.texts[0].rotation - 0.0).abs() < f64::EPSILON);
3953 }
3954
3955 #[test]
3956 fn text_builder_chaining() {
3957 let mut ax = Axes::new();
3958 ax.text(1.0, 2.0, "styled")
3959 .fontsize(14.0)
3960 .color(Color::TAB_RED)
3961 .ha(HAlign::Center)
3962 .va(VAlign::Top)
3963 .rotation(45.0);
3964 let t = &ax.texts[0];
3965 assert_eq!(t.fontsize, Some(14.0));
3966 assert_eq!(t.color, Some(Color::TAB_RED));
3967 assert_eq!(t.ha, HAlign::Center);
3968 assert_eq!(t.va, VAlign::Top);
3969 assert!((t.rotation - 45.0).abs() < f64::EPSILON);
3970 }
3971
3972 #[test]
3973 fn text_does_not_affect_autoscale() {
3974 let mut ax = Axes::new();
3975 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
3976 let limits_before = ax.compute_data_limits();
3977 ax.text(100.0, 100.0, "far away");
3979 let limits_after = ax.compute_data_limits();
3980 assert_eq!(limits_before, limits_after);
3981 }
3982
3983 #[test]
3984 fn multiple_texts() {
3985 let mut ax = Axes::new();
3986 ax.text(1.0, 1.0, "first");
3987 ax.text(2.0, 2.0, "second");
3988 ax.text(3.0, 3.0, "third");
3989 assert_eq!(ax.texts.len(), 3);
3990 assert_eq!(ax.texts[0].text, "first");
3991 assert_eq!(ax.texts[1].text, "second");
3992 assert_eq!(ax.texts[2].text, "third");
3993 }
3994
3995 #[test]
3998 fn annotate_creates_annotation() {
3999 let mut ax = Axes::new();
4000 ax.annotate("peak", (1.0, 2.0), (3.0, 4.0));
4001 assert_eq!(ax.annotations.len(), 1);
4002 assert_eq!(ax.annotations[0].text, "peak");
4003 assert_eq!(ax.annotations[0].xy, (1.0, 2.0));
4004 assert_eq!(ax.annotations[0].xytext, (3.0, 4.0));
4005 }
4006
4007 #[test]
4008 fn annotate_default_no_arrow() {
4009 let mut ax = Axes::new();
4010 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4011 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::None);
4012 }
4013
4014 #[test]
4015 fn annotate_default_alignment() {
4016 let mut ax = Axes::new();
4017 ax.annotate("label", (0.0, 0.0), (1.0, 1.0));
4018 assert_eq!(ax.annotations[0].ha, HAlign::Center);
4019 assert_eq!(ax.annotations[0].va, VAlign::Bottom);
4020 }
4021
4022 #[test]
4023 fn annotate_with_arrow() {
4024 let mut ax = Axes::new();
4025 ax.annotate("peak", (1.0, 1.0), (2.0, 2.0))
4026 .arrowstyle(ArrowStyle::Simple);
4027 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Simple);
4028 }
4029
4030 #[test]
4031 fn annotate_with_fancy_arrow() {
4032 let mut ax = Axes::new();
4033 ax.annotate("label", (0.0, 0.0), (1.0, 1.0))
4034 .arrowstyle(ArrowStyle::Fancy);
4035 assert_eq!(ax.annotations[0].arrowstyle, ArrowStyle::Fancy);
4036 }
4037
4038 #[test]
4039 fn annotate_builder_chaining() {
4040 let mut ax = Axes::new();
4041 ax.annotate("note", (1.0, 2.0), (3.0, 4.0))
4042 .fontsize(12.0)
4043 .color(Color::TAB_BLUE)
4044 .ha(HAlign::Right)
4045 .va(VAlign::Top)
4046 .arrowstyle(ArrowStyle::Fancy)
4047 .arrow_color(Color::TAB_RED);
4048 let a = &ax.annotations[0];
4049 assert_eq!(a.fontsize, Some(12.0));
4050 assert_eq!(a.color, Some(Color::TAB_BLUE));
4051 assert_eq!(a.ha, HAlign::Right);
4052 assert_eq!(a.va, VAlign::Top);
4053 assert_eq!(a.arrowstyle, ArrowStyle::Fancy);
4054 assert_eq!(a.arrow_color, Some(Color::TAB_RED));
4055 }
4056
4057 #[test]
4058 fn annotate_does_not_affect_autoscale() {
4059 let mut ax = Axes::new();
4060 ax.plot(vec![0.0, 1.0], vec![0.0, 1.0]).unwrap();
4061 let limits_before = ax.compute_data_limits();
4062 ax.annotate("far", (100.0, 100.0), (200.0, 200.0));
4064 let limits_after = ax.compute_data_limits();
4065 assert_eq!(limits_before, limits_after);
4066 }
4067
4068 #[test]
4069 fn multiple_annotations() {
4070 let mut ax = Axes::new();
4071 ax.annotate("a", (0.0, 0.0), (1.0, 1.0));
4072 ax.annotate("b", (2.0, 2.0), (3.0, 3.0));
4073 assert_eq!(ax.annotations.len(), 2);
4074 }
4075
4076 #[test]
4077 fn text_at_plot_boundary() {
4078 let mut ax = Axes::new();
4079 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4080 ax.text(0.0, 0.0, "origin");
4082 ax.text(10.0, 10.0, "corner");
4083 assert_eq!(ax.texts.len(), 2);
4084 }
4085
4086 #[test]
4087 fn overlapping_annotations() {
4088 let mut ax = Axes::new();
4089 ax.annotate("one", (5.0, 5.0), (6.0, 6.0));
4091 ax.annotate("two", (5.0, 5.0), (6.0, 6.0));
4092 ax.text(6.0, 6.0, "three");
4093 assert_eq!(ax.annotations.len(), 2);
4094 assert_eq!(ax.texts.len(), 1);
4095 }
4096
4097 #[test]
4098 fn new_axes_has_empty_annotations() {
4099 let ax = Axes::new();
4100 assert!(ax.texts.is_empty());
4101 assert!(ax.annotations.is_empty());
4102 }
4103
4104 #[test]
4105 fn text_pixel_placement() {
4106 let ax = Axes::new();
4108 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4109 let pt = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4111 assert!((pt.x - 300.0).abs() < 1e-10);
4112 assert!((pt.y - 200.0).abs() < 1e-10);
4113 }
4114
4115 #[test]
4116 fn annotation_pixel_placement() {
4117 let ax = Axes::new();
4119 let plot_area = Rect::new(0.0, 0.0, 100.0, 100.0);
4120 let target = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4121 let text_pos = ax.data_to_pixel(5.0, 5.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
4122 assert!((target.x - 0.0).abs() < 1e-10);
4124 assert!((target.y - 100.0).abs() < 1e-10);
4125 assert!((text_pos.x - 50.0).abs() < 1e-10);
4127 assert!((text_pos.y - 50.0).abs() < 1e-10);
4128 }
4129
4130 #[test]
4133 fn set_xlim_overrides_autoscale() {
4134 let mut ax = Axes::new();
4135 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4136 ax.set_xlim(2.0, 8.0);
4137 let (xmin, xmax, _, _) = ax.compute_data_limits();
4138 assert!((xmin - 2.0).abs() < f64::EPSILON);
4139 assert!((xmax - 8.0).abs() < f64::EPSILON);
4140 }
4141
4142 #[test]
4143 fn set_ylim_overrides_autoscale() {
4144 let mut ax = Axes::new();
4145 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4146 ax.set_ylim(-5.0, 15.0);
4147 let (_, _, ymin, ymax) = ax.compute_data_limits();
4148 assert!((ymin - (-5.0)).abs() < f64::EPSILON);
4149 assert!((ymax - 15.0).abs() < f64::EPSILON);
4150 }
4151
4152 #[test]
4153 fn set_xlim_with_min_greater_than_max() {
4154 let mut ax = Axes::new();
4155 ax.set_xlim(10.0, 2.0);
4156 let (xmin, xmax, _, _) = ax.compute_data_limits();
4157 assert!((xmin - 10.0).abs() < f64::EPSILON);
4159 assert!((xmax - 2.0).abs() < f64::EPSILON);
4160 }
4161
4162 #[test]
4163 fn invert_xaxis_swaps_limits() {
4164 let mut ax = Axes::new();
4165 ax.plot(vec![0.0, 10.0], vec![0.0, 10.0]).unwrap();
4166 ax.set_xlim(0.0, 10.0);
4167 ax.invert_xaxis();
4168 let (xmin, xmax, _, _) = ax.compute_data_limits();
4169 assert!((xmin - 10.0).abs() < f64::EPSILON);
4171 assert!((xmax - 0.0).abs() < f64::EPSILON);
4172 }
4173
4174 #[test]
4175 fn invert_yaxis_swaps_limits() {
4176 let mut ax = Axes::new();
4177 ax.set_ylim(0.0, 100.0);
4178 ax.invert_yaxis();
4179 let (_, _, ymin, ymax) = ax.compute_data_limits();
4180 assert!((ymin - 100.0).abs() < f64::EPSILON);
4181 assert!((ymax - 0.0).abs() < f64::EPSILON);
4182 }
4183
4184 #[test]
4185 fn custom_xticks_appear_in_output() {
4186 let mut ax = Axes::new();
4187 ax.set_xticks(&[1.0, 2.0, 3.0]);
4188 let ticks = ax.resolve_xticks(0.0, 10.0);
4189 assert_eq!(ticks.len(), 3);
4190 assert!((ticks[0].value - 1.0).abs() < f64::EPSILON);
4191 assert!((ticks[1].value - 2.0).abs() < f64::EPSILON);
4192 assert!((ticks[2].value - 3.0).abs() < f64::EPSILON);
4193 }
4194
4195 #[test]
4196 fn custom_yticks_appear_in_output() {
4197 let mut ax = Axes::new();
4198 ax.set_yticks(&[0.0, 50.0, 100.0]);
4199 let ticks = ax.resolve_yticks(0.0, 100.0);
4200 assert_eq!(ticks.len(), 3);
4201 assert!((ticks[0].value - 0.0).abs() < f64::EPSILON);
4202 assert!((ticks[1].value - 50.0).abs() < f64::EPSILON);
4203 assert!((ticks[2].value - 100.0).abs() < f64::EPSILON);
4204 }
4205
4206 #[test]
4207 fn custom_tick_labels_override_default_format() {
4208 let mut ax = Axes::new();
4209 ax.set_xticks(&[0.0, 3.125, 6.25]);
4210 ax.set_xticklabels(&["0", "pi", "2pi"]);
4211 let ticks = ax.resolve_xticks(0.0, 7.0);
4212 assert_eq!(ticks.len(), 3);
4213 assert_eq!(ticks[0].label, "0");
4214 assert_eq!(ticks[1].label, "pi");
4215 assert_eq!(ticks[2].label, "2pi");
4216 }
4217
4218 #[test]
4219 fn custom_tick_labels_partial_match() {
4220 let mut ax = Axes::new();
4222 ax.set_xticks(&[1.0, 2.0, 3.0]);
4223 ax.set_xticklabels(&["one"]);
4224 let ticks = ax.resolve_xticks(0.0, 5.0);
4225 assert_eq!(ticks[0].label, "one");
4226 assert_eq!(ticks[1].label, "2");
4227 assert_eq!(ticks[2].label, "3");
4228 }
4229
4230 #[test]
4231 fn empty_custom_ticks() {
4232 let mut ax = Axes::new();
4233 ax.set_xticks(&[]);
4234 let ticks = ax.resolve_xticks(0.0, 10.0);
4235 assert!(ticks.is_empty());
4236 }
4237
4238 #[test]
4239 fn grid_visibility_toggle() {
4240 let mut ax = Axes::new();
4241 assert!(ax.show_grid.is_none());
4242 ax.grid(true);
4243 assert_eq!(ax.show_grid, Some(true));
4244 ax.grid(false);
4245 assert_eq!(ax.show_grid, Some(false));
4246 }
4247
4248 #[test]
4249 fn grid_axis_setting() {
4250 let mut ax = Axes::new();
4251 assert_eq!(ax.grid_axis, GridAxis::Both);
4252 ax.grid_axis("x");
4253 assert_eq!(ax.grid_axis, GridAxis::X);
4254 ax.grid_axis("y");
4255 assert_eq!(ax.grid_axis, GridAxis::Y);
4256 ax.grid_axis("both");
4257 assert_eq!(ax.grid_axis, GridAxis::Both);
4258 }
4259
4260 #[test]
4261 fn grid_alpha_setting() {
4262 let mut ax = Axes::new();
4263 ax.grid_alpha(0.5);
4264 assert!((ax.grid_alpha.unwrap() - 0.5).abs() < f64::EPSILON);
4265 }
4266
4267 #[test]
4268 fn grid_alpha_clamps() {
4269 let mut ax = Axes::new();
4270 ax.grid_alpha(2.0);
4271 assert!((ax.grid_alpha.unwrap() - 1.0).abs() < f64::EPSILON);
4272 ax.grid_alpha(-0.5);
4273 assert!((ax.grid_alpha.unwrap() - 0.0).abs() < f64::EPSILON);
4274 }
4275
4276 #[test]
4277 fn grid_style_setting() {
4278 let mut ax = Axes::new();
4279 ax.grid_style(crate::theme::LineStyle::Dashed);
4280 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dashed));
4281 }
4282
4283 #[test]
4284 fn tick_rotation_setting() {
4285 let mut ax = Axes::new();
4286 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
4287 ax.tick_params_x_rotation(45.0);
4288 assert!((ax.xtick_rotation - 45.0).abs() < f64::EPSILON);
4289 ax.tick_params_y_rotation(-30.0);
4290 assert!((ax.ytick_rotation - (-30.0)).abs() < f64::EPSILON);
4291 }
4292
4293 #[test]
4294 fn axis_control_chaining() {
4295 let mut ax = Axes::new();
4296 ax.set_xlim(0.0, 10.0)
4297 .set_ylim(-1.0, 1.0)
4298 .invert_xaxis()
4299 .grid(true)
4300 .grid_axis("y")
4301 .grid_alpha(0.3)
4302 .grid_style(crate::theme::LineStyle::Dotted)
4303 .set_xticks(&[0.0, 5.0, 10.0])
4304 .set_xticklabels(&["start", "mid", "end"])
4305 .tick_params_x_rotation(90.0);
4306
4307 assert_eq!(ax.xlim, Some((0.0, 10.0)));
4308 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
4309 assert!(ax.x_inverted);
4310 assert_eq!(ax.show_grid, Some(true));
4311 assert_eq!(ax.grid_axis, GridAxis::Y);
4312 assert!((ax.grid_alpha.unwrap() - 0.3).abs() < f64::EPSILON);
4313 assert_eq!(ax.grid_style, Some(crate::theme::LineStyle::Dotted));
4314 assert_eq!(ax.custom_xticks.as_ref().unwrap().len(), 3);
4315 assert_eq!(ax.custom_xticklabels.as_ref().unwrap().len(), 3);
4316 assert!((ax.xtick_rotation - 90.0).abs() < f64::EPSILON);
4317 }
4318
4319 #[test]
4320 fn new_axes_has_axis_control_defaults() {
4321 let ax = Axes::new();
4322 assert_eq!(ax.grid_axis, GridAxis::Both);
4323 assert!(ax.grid_alpha.is_none());
4324 assert!(ax.grid_style.is_none());
4325 assert!(!ax.x_inverted);
4326 assert!(!ax.y_inverted);
4327 assert!(ax.custom_xticks.is_none());
4328 assert!(ax.custom_yticks.is_none());
4329 assert!(ax.custom_xticklabels.is_none());
4330 assert!(ax.custom_yticklabels.is_none());
4331 assert!((ax.xtick_rotation - 0.0).abs() < f64::EPSILON);
4332 assert!((ax.ytick_rotation - 0.0).abs() < f64::EPSILON);
4333 }
4334
4335 #[test]
4336 fn autoscale_not_broken_without_user_limits() {
4337 let mut ax = Axes::new();
4338 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
4339 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4340 assert!(xmin < 1.0);
4342 assert!(xmax > 10.0);
4343 assert!(ymin < 2.0);
4344 assert!(ymax > 8.0);
4345 }
4346
4347 #[test]
4348 fn resolve_xticks_auto_when_not_set() {
4349 let ax = Axes::new();
4350 let ticks = ax.resolve_xticks(0.0, 10.0);
4351 assert!(!ticks.is_empty());
4353 }
4354
4355 #[test]
4356 fn resolve_yticks_auto_when_not_set() {
4357 let ax = Axes::new();
4358 let ticks = ax.resolve_yticks(0.0, 100.0);
4359 assert!(!ticks.is_empty());
4360 }
4361
4362 #[test]
4365 fn data_to_pixel_log10() {
4366 let mut ax = Axes::new();
4367 ax.set_xscale(Scale::Log10);
4368 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
4369
4370 let p = ax.data_to_pixel(1.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
4372 assert!((p.x - 100.0).abs() < 1e-6, "log10(1)=0 should be left edge, got {}", p.x);
4373
4374 let p = ax.data_to_pixel(1000.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
4376 assert!((p.x - 500.0).abs() < 1e-6, "log10(1000)=3 should be right edge, got {}", p.x);
4377
4378 let p = ax.data_to_pixel(10.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
4380 let expected_x = 100.0 + 400.0 / 3.0;
4381 assert!((p.x - expected_x).abs() < 1e-6, "log10(10)=1/3 of range, expected {}, got {}", expected_x, p.x);
4382
4383 let p = ax.data_to_pixel(100.0, 0.0, &plot_area, 1.0, 1000.0, 0.0, 10.0);
4385 let expected_x = 100.0 + 400.0 * 2.0 / 3.0;
4386 assert!((p.x - expected_x).abs() < 1e-6, "log10(100)=2/3 of range, expected {}, got {}", expected_x, p.x);
4387 }
4388
4389 #[test]
4390 fn data_to_pixel_log10_y() {
4391 let mut ax = Axes::new();
4392 ax.set_yscale(Scale::Log10);
4393 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
4394
4395 let p = ax.data_to_pixel(0.0, 10.0, &plot_area, 0.0, 10.0, 1.0, 100.0);
4397 assert!((p.y - 150.0).abs() < 1e-6, "log10(10) should be vertical center, got {}", p.y);
4400 }
4401
4402 #[test]
4403 fn compute_data_limits_log_clamps_positive() {
4404 let mut ax = Axes::new();
4405 ax.set_xscale(Scale::Log10);
4406 ax.set_yscale(Scale::Log10);
4407 ax.plot(vec![0.1, 1.0, 10.0], vec![1.0, 10.0, 100.0]).unwrap();
4408 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4409 assert!(xmin > 0.0, "log x-min must be positive, got {}", xmin);
4410 assert!(xmax > xmin, "log x-max must be > x-min");
4411 assert!(ymin > 0.0, "log y-min must be positive, got {}", ymin);
4412 assert!(ymax > ymin, "log y-max must be > y-min");
4413 }
4414
4415 #[test]
4416 fn compute_data_limits_log_with_zeros() {
4417 let mut ax = Axes::new();
4419 ax.set_xscale(Scale::Log10);
4420 ax.plot(vec![0.0, 1.0, 10.0], vec![1.0, 2.0, 3.0]).unwrap();
4421 let (xmin, xmax, _ymin, _ymax) = ax.compute_data_limits();
4422 assert!(xmin > 0.0, "log x-min should be clamped positive, got {}", xmin);
4423 assert!(xmax > xmin);
4424 }
4425
4426 #[test]
4427 fn data_to_pixel_symlog() {
4428 let mut ax = Axes::new();
4429 ax.set_xscale(Scale::SymLog { linthresh: 1.0 });
4430 let plot_area = Rect::new(0.0, 0.0, 400.0, 300.0);
4431
4432 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, -100.0, 100.0, 0.0, 1.0);
4434 assert!((p.x - 200.0).abs() < 1e-6, "symlog(0) should be center for symmetric range, got {}", p.x);
4435 }
4436
4437 #[test]
4438 fn set_scale_methods_return_self() {
4439 let mut ax = Axes::new();
4440 ax.set_xscale(Scale::Log10)
4441 .set_yscale(Scale::Log10)
4442 .set_title("Log plot");
4443 assert!(matches!(ax.xscale, Scale::Log10));
4444 assert!(matches!(ax.yscale, Scale::Log10));
4445 assert_eq!(ax.title.as_deref(), Some("Log plot"));
4446 }
4447
4448 #[test]
4449 fn compute_data_limits_log_no_artists() {
4450 let mut ax = Axes::new();
4451 ax.set_xscale(Scale::Log10);
4452 ax.set_yscale(Scale::Log10);
4453 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
4454 assert!(xmin > 0.0, "default log x-min must be positive");
4455 assert!(xmax > xmin);
4456 assert!(ymin > 0.0, "default log y-min must be positive");
4457 assert!(ymax > ymin);
4458 }
4459
4460 #[test]
4461 fn data_to_pixel_log10_very_large_range() {
4462 let mut ax = Axes::new();
4463 ax.set_xscale(Scale::Log10);
4464 let plot_area = Rect::new(0.0, 0.0, 1000.0, 100.0);
4465
4466 let p_lo = ax.data_to_pixel(1e-5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
4468 let p_hi = ax.data_to_pixel(1e5, 0.0, &plot_area, 1e-5, 1e5, 0.0, 1.0);
4469 assert!((p_lo.x - 0.0).abs() < 1e-6);
4470 assert!((p_hi.x - 1000.0).abs() < 1e-6);
4471 }
4472
4473 #[test]
4478 fn new_axes_has_no_twin_fields() {
4479 let ax = Axes::new();
4480 assert!(!ax.is_twin());
4481 assert_eq!(ax.twin_side(), None);
4482 }
4483
4484 #[test]
4489 fn colorbar_attaches_to_axes() {
4490 let mut ax = Axes::new();
4491 let cb = ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
4492 cb.set_label("Test");
4493 assert!(ax.colorbar.is_some());
4494 assert_eq!(ax.colorbar.as_ref().unwrap().label.as_deref(), Some("Test"));
4495 }
4496
4497 #[test]
4498 fn colorbar_replaces_previous() {
4499 let mut ax = Axes::new();
4500 ax.colorbar(crate::colormap::Colormap::Viridis, 0.0, 1.0);
4501 ax.colorbar(crate::colormap::Colormap::Plasma, -10.0, 10.0);
4502 let cb = ax.colorbar.as_ref().unwrap();
4503 assert_eq!(cb.cmap, crate::colormap::Colormap::Plasma);
4504 assert!((cb.vmin - (-10.0)).abs() < f64::EPSILON);
4505 assert!((cb.vmax - 10.0).abs() < f64::EPSILON);
4506 }
4507
4508 #[test]
4509 fn heatmap_auto_colorbar() {
4510 let mut ax = Axes::new();
4511 ax.heatmap(vec![vec![1.0, 2.0], vec![3.0, 4.0]])
4512 .unwrap()
4513 .colorbar(true);
4514 let auto_cb = ax.auto_colorbar_from_artists();
4515 assert!(auto_cb.is_some());
4516 let cb = auto_cb.unwrap();
4517 assert!((cb.vmin - 1.0).abs() < f64::EPSILON);
4518 assert!((cb.vmax - 4.0).abs() < f64::EPSILON);
4519 }
4520
4521 #[test]
4522 fn heatmap_no_auto_colorbar_by_default() {
4523 let mut ax = Axes::new();
4524 ax.heatmap(vec![vec![1.0, 2.0]]).unwrap();
4525 let auto_cb = ax.auto_colorbar_from_artists();
4526 assert!(auto_cb.is_none());
4527 }
4528
4529 #[test]
4530 fn new_axes_has_no_colorbar() {
4531 let ax = Axes::new();
4532 assert!(ax.colorbar.is_none());
4533 }
4534}