1use crate::artist::*;
9use crate::error::{PlotError, Result};
10use crate::layout::{self, LayoutConfig};
11use crate::legend::{self, LegendEntry, SwatchKind};
12use crate::primitives::*;
13use crate::renderer::Renderer;
14use crate::scale::Scale;
15use crate::series::{IntoCategories, IntoSeries};
16use crate::theme::{Loc, Marker, Theme, TickDirection};
17use crate::ticks;
18
19const DEFAULT_TICK_COUNT: usize = 7;
25
26const AUTOSCALE_PAD: f64 = 0.05;
29
30#[derive(Debug)]
44pub struct Axes {
45 pub(crate) artists: Vec<Artist>,
47 pub(crate) title: Option<String>,
49 pub(crate) xlabel: Option<String>,
51 pub(crate) ylabel: Option<String>,
53 pub(crate) xlim: Option<(f64, f64)>,
55 pub(crate) ylim: Option<(f64, f64)>,
57 pub(crate) xscale: Scale,
59 pub(crate) yscale: Scale,
61 pub(crate) show_grid: Option<bool>,
63 pub(crate) show_legend: bool,
65 pub(crate) legend_loc: Loc,
67 pub(crate) theme_override: Option<Theme>,
69 color_index: usize,
72}
73
74impl Axes {
79 pub(crate) fn new() -> Self {
81 Self {
82 artists: Vec::new(),
83 title: None,
84 xlabel: None,
85 ylabel: None,
86 xlim: None,
87 ylim: None,
88 xscale: Scale::default(),
89 yscale: Scale::default(),
90 show_grid: None,
91 show_legend: false,
92 legend_loc: Loc::Best,
93 theme_override: None,
94 color_index: 0,
95 }
96 }
97
98}
99
100impl Axes {
105 pub fn plot<X, Y>(&mut self, x: X, y: Y) -> Result<&mut LineArtist>
116 where
117 X: IntoSeries,
118 Y: IntoSeries,
119 {
120 let xs = x.into_series();
121 let ys = y.into_series();
122 if xs.len() != ys.len() {
123 return Err(PlotError::SeriesLengthMismatch {
124 expected: xs.len(),
125 got: ys.len(),
126 });
127 }
128 if xs.is_empty() {
129 return Err(PlotError::EmptyData);
130 }
131 let color = Color::TABLEAU_10[self.color_index % 10];
132 self.color_index += 1;
133 let artist = LineArtist {
134 x: xs,
135 y: ys,
136 color,
137 width: 1.5,
138 style: crate::theme::LineStyle::Solid,
139 label: None,
140 alpha: 1.0,
141 };
142 self.artists.push(Artist::Line(artist));
143 match self.artists.last_mut().expect("just pushed") {
144 Artist::Line(a) => Ok(a),
145 _ => unreachable!(),
146 }
147 }
148
149 pub fn scatter<X, Y>(&mut self, x: X, y: Y) -> Result<&mut ScatterArtist>
158 where
159 X: IntoSeries,
160 Y: IntoSeries,
161 {
162 let xs = x.into_series();
163 let ys = y.into_series();
164 if xs.len() != ys.len() {
165 return Err(PlotError::SeriesLengthMismatch {
166 expected: xs.len(),
167 got: ys.len(),
168 });
169 }
170 if xs.is_empty() {
171 return Err(PlotError::EmptyData);
172 }
173 let color = Color::TABLEAU_10[self.color_index % 10];
174 self.color_index += 1;
175 let artist = ScatterArtist {
176 x: xs,
177 y: ys,
178 color,
179 marker: Marker::Circle,
180 size: 6.0,
181 label: None,
182 alpha: 0.8,
183 colors: None,
184 };
185 self.artists.push(Artist::Scatter(artist));
186 match self.artists.last_mut().expect("just pushed") {
187 Artist::Scatter(a) => Ok(a),
188 _ => unreachable!(),
189 }
190 }
191
192 pub fn bar<C, H>(&mut self, categories: C, heights: H) -> Result<&mut BarArtist>
202 where
203 C: IntoCategories,
204 H: IntoSeries,
205 {
206 let cats = categories.into_categories();
207 let vals = heights.into_series();
208 if cats.len() != vals.len() {
209 return Err(PlotError::SeriesLengthMismatch {
210 expected: cats.len(),
211 got: vals.len(),
212 });
213 }
214 if cats.is_empty() {
215 return Err(PlotError::EmptyData);
216 }
217 let color = Color::TABLEAU_10[self.color_index % 10];
218 self.color_index += 1;
219 let artist = BarArtist {
220 categories: cats,
221 heights: vals,
222 color,
223 horizontal: false,
224 bar_width: 0.8,
225 label: None,
226 alpha: 1.0,
227 };
228 self.artists.push(Artist::Bar(artist));
229 match self.artists.last_mut().expect("just pushed") {
230 Artist::Bar(a) => Ok(a),
231 _ => unreachable!(),
232 }
233 }
234
235 pub fn barh<C, W>(&mut self, categories: C, widths: W) -> Result<&mut BarArtist>
244 where
245 C: IntoCategories,
246 W: IntoSeries,
247 {
248 let cats = categories.into_categories();
249 let vals = widths.into_series();
250 if cats.len() != vals.len() {
251 return Err(PlotError::SeriesLengthMismatch {
252 expected: cats.len(),
253 got: vals.len(),
254 });
255 }
256 if cats.is_empty() {
257 return Err(PlotError::EmptyData);
258 }
259 let color = Color::TABLEAU_10[self.color_index % 10];
260 self.color_index += 1;
261 let artist = BarArtist {
262 categories: cats,
263 heights: vals,
264 color,
265 horizontal: true,
266 bar_width: 0.8,
267 label: None,
268 alpha: 1.0,
269 };
270 self.artists.push(Artist::Bar(artist));
271 match self.artists.last_mut().expect("just pushed") {
272 Artist::Bar(a) => Ok(a),
273 _ => unreachable!(),
274 }
275 }
276
277 pub fn hist<D>(&mut self, data: D, bins: usize) -> Result<&mut HistArtist>
287 where
288 D: IntoSeries,
289 {
290 let series = data.into_series();
291 if series.is_empty() {
292 return Err(PlotError::EmptyData);
293 }
294 let bins = bins.max(1);
295
296 let (data_min, data_max) = series.bounds().unwrap_or((0.0, 1.0));
298
299 let (lo, hi) = if (data_max - data_min).abs() < f64::EPSILON {
301 (data_min - 0.5, data_max + 0.5)
302 } else {
303 (data_min, data_max)
304 };
305
306 let bin_width = (hi - lo) / bins as f64;
307
308 let mut edges: Vec<f64> = (0..=bins).map(|i| lo + i as f64 * bin_width).collect();
310 *edges.last_mut().expect("edges is non-empty") = hi;
312
313 let mut counts = vec![0.0f64; bins];
315 for &v in &series.data {
316 if !v.is_finite() {
317 continue;
318 }
319 let idx = if v >= hi {
321 bins - 1
323 } else {
324 let raw = ((v - lo) / bin_width) as usize;
325 raw.min(bins - 1)
326 };
327 counts[idx] += 1.0;
328 }
329
330 let color = Color::TABLEAU_10[self.color_index % 10];
331 self.color_index += 1;
332 let artist = HistArtist {
333 data: series,
334 bins,
335 bin_edges: edges,
336 counts,
337 color,
338 label: None,
339 alpha: 0.85,
340 density: false,
341 };
342
343
344
345
346
347
348
349 self.artists.push(Artist::Histogram(artist));
350 match self.artists.last_mut().expect("just pushed") {
351 Artist::Histogram(a) => Ok(a),
352 _ => unreachable!(),
353 }
354 }
355
356 pub fn fill_between<X, Y1, Y2>(
365 &mut self,
366 x: X,
367 y1: Y1,
368 y2: Y2,
369 ) -> Result<&mut FillBetweenArtist>
370 where
371 X: IntoSeries,
372 Y1: IntoSeries,
373 Y2: IntoSeries,
374 {
375 let xs = x.into_series();
376 let y1s = y1.into_series();
377 let y2s = y2.into_series();
378 if xs.len() != y1s.len() {
379 return Err(PlotError::SeriesLengthMismatch {
380 expected: xs.len(),
381 got: y1s.len(),
382 });
383 }
384 if xs.len() != y2s.len() {
385 return Err(PlotError::SeriesLengthMismatch {
386 expected: xs.len(),
387 got: y2s.len(),
388 });
389 }
390 if xs.is_empty() {
391 return Err(PlotError::EmptyData);
392 }
393 let color = Color::TABLEAU_10[self.color_index % 10];
394 self.color_index += 1;
395 let artist = FillBetweenArtist {
396 x: xs,
397 y1: y1s,
398 y2: y2s,
399 color,
400 label: None,
401 alpha: 0.3,
402 };
403 self.artists.push(Artist::FillBetween(artist));
404 match self.artists.last_mut().expect("just pushed") {
405 Artist::FillBetween(a) => Ok(a),
406 _ => unreachable!(),
407 }
408 }
409}
410
411impl Axes {
416 pub fn set_title(&mut self, title: &str) -> &mut Self {
418 self.title = Some(title.to_string());
419 self
420 }
421
422 pub fn set_xlabel(&mut self, label: &str) -> &mut Self {
424 self.xlabel = Some(label.to_string());
425 self
426 }
427
428 pub fn set_ylabel(&mut self, label: &str) -> &mut Self {
430 self.ylabel = Some(label.to_string());
431 self
432 }
433
434 pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
436 self.xlim = Some((min, max));
437 self
438 }
439
440 pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
442 self.ylim = Some((min, max));
443 self
444 }
445
446 pub fn set_xscale(&mut self, scale: Scale) -> &mut Self {
448 self.xscale = scale;
449 self
450 }
451
452 pub fn set_yscale(&mut self, scale: Scale) -> &mut Self {
454 self.yscale = scale;
455 self
456 }
457
458 pub fn grid(&mut self, show: bool) -> &mut Self {
460 self.show_grid = Some(show);
461 self
462 }
463
464 pub fn legend(&mut self) -> &mut Self {
466 self.show_legend = true;
467 self
468 }
469
470 pub fn set_legend_loc(&mut self, loc: Loc) -> &mut Self {
472 self.legend_loc = loc;
473 self
474 }
475
476 pub fn set_theme(&mut self, theme: Theme) -> &mut Self {
478 self.theme_override = Some(theme);
479 self
480 }
481}
482
483#[allow(clippy::too_many_arguments)]
488impl Axes {
489 pub(crate) fn render(&self, renderer: &mut impl Renderer, bounds: Rect, fig_theme: &Theme) {
505 let theme = self.theme_override.as_ref().unwrap_or(fig_theme);
506
507 let (xmin, xmax, ymin, ymax) = self.compute_data_limits();
509
510 let xticks = ticks::generate_ticks(xmin, xmax, DEFAULT_TICK_COUNT, &self.xscale);
512 let yticks = ticks::generate_ticks(ymin, ymax, DEFAULT_TICK_COUNT, &self.yscale);
513
514 let mut layout_config = LayoutConfig::new(bounds.width, bounds.height);
516 layout_config.has_title = self.title.is_some();
517 layout_config.has_xlabel = self.xlabel.is_some();
518 layout_config.has_ylabel = self.ylabel.is_some();
519 layout_config.has_legend = self.show_legend;
520
521
522
523
524
525 let layout_result = layout::compute_layout(&layout_config);
526
527 let plot_area = Rect::new(
529 bounds.x + layout_result.plot_area.x,
530 bounds.y + layout_result.plot_area.y,
531 layout_result.plot_area.width,
532 layout_result.plot_area.height,
533 );
534
535 let bg_path = Path::rect(plot_area);
537 renderer.fill_path(&bg_path, &Paint::new(theme.axes_background), Affine::IDENTITY);
538
539 if self.show_grid.unwrap_or(theme.show_grid) {
541 self.draw_grid(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
542 }
543
544 let clip_path = Path::rect(plot_area);
546 renderer.push_clip(&clip_path, Affine::IDENTITY);
547 for artist in &self.artists {
548 self.draw_artist(renderer, artist, &plot_area, xmin, xmax, ymin, ymax, theme);
549 }
550 renderer.pop_clip();
551
552 self.draw_spines(renderer, &plot_area, theme);
554
555 self.draw_ticks(renderer, &plot_area, &xticks, &yticks, xmin, xmax, ymin, ymax, theme);
557
558 self.draw_labels(renderer, &plot_area, &bounds, theme);
560
561 if self.show_legend {
563 self.draw_legend(renderer, &plot_area, theme);
564 }
565 }
566
567 fn compute_data_limits(&self) -> (f64, f64, f64, f64) {
574 let mut x_lo = f64::INFINITY;
575 let mut x_hi = f64::NEG_INFINITY;
576 let mut y_lo = f64::INFINITY;
577 let mut y_hi = f64::NEG_INFINITY;
578
579 for artist in &self.artists {
580 match artist {
581 Artist::Line(a) => {
582 if let Some((lo, hi)) = a.x.bounds() {
583 x_lo = x_lo.min(lo);
584 x_hi = x_hi.max(hi);
585 }
586 if let Some((lo, hi)) = a.y.bounds() {
587 y_lo = y_lo.min(lo);
588 y_hi = y_hi.max(hi);
589 }
590 }
591 Artist::Scatter(a) => {
592 if let Some((lo, hi)) = a.x.bounds() {
593 x_lo = x_lo.min(lo);
594 x_hi = x_hi.max(hi);
595 }
596 if let Some((lo, hi)) = a.y.bounds() {
597 y_lo = y_lo.min(lo);
598 y_hi = y_hi.max(hi);
599 }
600 }
601 Artist::Bar(a) => {
602 let n = a.categories.len() as f64;
603 if a.horizontal {
604 y_lo = 0.0_f64.min(y_lo);
606 y_hi = n.max(y_hi);
607 x_lo = 0.0_f64.min(x_lo);
608 if let Some((lo, hi)) = a.heights.bounds() {
609 x_lo = x_lo.min(lo.min(0.0));
610 x_hi = x_hi.max(hi);
611 }
612 } else {
613 x_lo = 0.0_f64.min(x_lo);
615 x_hi = n.max(x_hi);
616 y_lo = 0.0_f64.min(y_lo);
617 if let Some((lo, hi)) = a.heights.bounds() {
618 y_lo = y_lo.min(lo.min(0.0));
619 y_hi = y_hi.max(hi);
620 }
621 }
622 }
623 Artist::Histogram(a) => {
624 if let (Some(&first), Some(&last)) = (a.bin_edges.first(), a.bin_edges.last()) {
625 x_lo = x_lo.min(first);
626 x_hi = x_hi.max(last);
627 }
628 y_lo = 0.0_f64.min(y_lo);
629 let max_count = a.counts.iter().fold(0.0f64, |a, &b| a.max(b));
630 y_hi = y_hi.max(max_count);
631 }
632 Artist::FillBetween(a) => {
633 if let Some((lo, hi)) = a.x.bounds() {
634 x_lo = x_lo.min(lo);
635 x_hi = x_hi.max(hi);
636 }
637 if let Some((lo, hi)) = a.y1.bounds() {
638 y_lo = y_lo.min(lo);
639 y_hi = y_hi.max(hi);
640 }
641 if let Some((lo, hi)) = a.y2.bounds() {
642 y_lo = y_lo.min(lo);
643 y_hi = y_hi.max(hi);
644 }
645 }
646 }
647 }
648
649 if !x_lo.is_finite() || !x_hi.is_finite() {
651 x_lo = 0.0;
652 x_hi = 1.0;
653 }
654 if !y_lo.is_finite() || !y_hi.is_finite() {
655 y_lo = 0.0;
656 y_hi = 1.0;
657 }
658
659 if (x_hi - x_lo).abs() < f64::EPSILON {
661 x_lo -= 0.5;
662 x_hi += 0.5;
663 }
664 if (y_hi - y_lo).abs() < f64::EPSILON {
665 y_lo -= 0.5;
666 y_hi += 0.5;
667 }
668
669 let x_pad = (x_hi - x_lo) * AUTOSCALE_PAD;
671 let y_pad = (y_hi - y_lo) * AUTOSCALE_PAD;
672 x_lo -= x_pad;
673 x_hi += x_pad;
674 y_lo -= y_pad;
675 y_hi += y_pad;
676
677 if let Some((lo, hi)) = self.xlim {
679 x_lo = lo;
680 x_hi = hi;
681 }
682 if let Some((lo, hi)) = self.ylim {
683 y_lo = lo;
684 y_hi = hi;
685 }
686
687 (x_lo, x_hi, y_lo, y_hi)
688 }
689
690 fn draw_grid(
696 &self,
697 renderer: &mut impl Renderer,
698 plot_area: &Rect,
699 xticks: &[ticks::Tick],
700 yticks: &[ticks::Tick],
701 xmin: f64,
702 xmax: f64,
703 ymin: f64,
704 ymax: f64,
705 theme: &Theme,
706 ) {
707 let paint = Paint::new(theme.grid_color);
708 let stroke = Stroke::new(theme.grid_width);
709
710 for tick in xticks {
712 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
713 let mut path = Path::new();
714 path.move_to(pt.x, plot_area.y);
715 path.line_to(pt.x, plot_area.bottom());
716 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
717 }
718
719 for tick in yticks {
721 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
722 let mut path = Path::new();
723 path.move_to(plot_area.x, pt.y);
724 path.line_to(plot_area.right(), pt.y);
725 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
726 }
727 }
728
729 fn draw_artist(
735 &self,
736 renderer: &mut impl Renderer,
737 artist: &Artist,
738 plot_area: &Rect,
739 xmin: f64,
740 xmax: f64,
741 ymin: f64,
742 ymax: f64,
743 theme: &Theme,
744 ) {
745 match artist {
746 Artist::Line(a) => self.draw_line(renderer, a, plot_area, xmin, xmax, ymin, ymax),
747 Artist::Scatter(a) => self.draw_scatter(renderer, a, plot_area, xmin, xmax, ymin, ymax, theme),
748 Artist::Bar(a) => self.draw_bar(renderer, a, plot_area, xmin, xmax, ymin, ymax),
749 Artist::Histogram(a) => self.draw_hist(renderer, a, plot_area, xmin, xmax, ymin, ymax),
750 Artist::FillBetween(a) => self.draw_fill_between(renderer, a, plot_area, xmin, xmax, ymin, ymax),
751 }
752 }
753
754 fn draw_line(
756 &self,
757 renderer: &mut impl Renderer,
758 artist: &LineArtist,
759 plot_area: &Rect,
760 xmin: f64,
761 xmax: f64,
762 ymin: f64,
763 ymax: f64,
764 ) {
765 if artist.x.is_empty() {
766 return;
767 }
768
769 let mut path = Path::new();
770 let first = self.data_to_pixel(
771 artist.x.data[0],
772 artist.y.data[0],
773 plot_area,
774 xmin, xmax, ymin, ymax,
775 );
776 path.move_to(first.x, first.y);
777
778 for i in 1..artist.x.len() {
779 let pt = self.data_to_pixel(
780 artist.x.data[i],
781 artist.y.data[i],
782 plot_area,
783 xmin, xmax, ymin, ymax,
784 );
785 path.line_to(pt.x, pt.y);
786 }
787
788 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
789 let paint = Paint::new(color);
790 let mut stroke = Stroke::new(artist.width);
791
792 match artist.style {
794 crate::theme::LineStyle::Solid => {}
795 crate::theme::LineStyle::Dashed => {
796 stroke = stroke.with_dash(DashPattern {
797 dashes: vec![6.0, 4.0],
798 offset: 0.0,
799 });
800 }
801 crate::theme::LineStyle::Dotted => {
802 stroke = stroke.with_dash(DashPattern {
803 dashes: vec![2.0, 2.0],
804 offset: 0.0,
805 });
806 }
807 crate::theme::LineStyle::DashDot => {
808 stroke = stroke.with_dash(DashPattern {
809 dashes: vec![6.0, 3.0, 2.0, 3.0],
810 offset: 0.0,
811 });
812 }
813 }
814
815 renderer.stroke_path(&path, &paint, &stroke, Affine::IDENTITY);
816 }
817
818 fn draw_scatter(
820 &self,
821 renderer: &mut impl Renderer,
822 artist: &ScatterArtist,
823 plot_area: &Rect,
824 xmin: f64,
825 xmax: f64,
826 ymin: f64,
827 ymax: f64,
828 theme: &Theme,
829 ) {
830 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
831 let paint = Paint::new(color);
832 let radius = artist.size / 2.0;
833
834 for i in 0..artist.x.len() {
835 let pt = self.data_to_pixel(
836 artist.x.data[i],
837 artist.y.data[i],
838 plot_area,
839 xmin, xmax, ymin, ymax,
840 );
841
842 let marker_path = match artist.marker {
843 Marker::Circle | Marker::Point => Path::circle(pt, radius),
844 Marker::Square => {
845 Path::rect(Rect::new(pt.x - radius, pt.y - radius, radius * 2.0, radius * 2.0))
846 }
847 Marker::Diamond => {
848 let mut p = Path::new();
849 p.move_to(pt.x, pt.y - radius);
850 p.line_to(pt.x + radius, pt.y);
851 p.line_to(pt.x, pt.y + radius);
852 p.line_to(pt.x - radius, pt.y);
853 p.close();
854 p
855 }
856 Marker::Triangle => {
857 let mut p = Path::new();
858 let h = radius * 1.1547; p.move_to(pt.x, pt.y - radius);
860 p.line_to(pt.x + h * 0.5, pt.y + radius * 0.5);
861 p.line_to(pt.x - h * 0.5, pt.y + radius * 0.5);
862 p.close();
863 p
864 }
865 Marker::Plus => {
866 let mut p = Path::new();
868 p.move_to(pt.x - radius, pt.y);
869 p.line_to(pt.x + radius, pt.y);
870 p.move_to(pt.x, pt.y - radius);
871 p.line_to(pt.x, pt.y + radius);
872 let stroke = Stroke::new(theme.line_width.max(1.0));
873 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
874 continue;
875 }
876 Marker::Cross => {
877 let mut p = Path::new();
878 let d = radius * 0.707; p.move_to(pt.x - d, pt.y - d);
880 p.line_to(pt.x + d, pt.y + d);
881 p.move_to(pt.x + d, pt.y - d);
882 p.line_to(pt.x - d, pt.y + d);
883 let stroke = Stroke::new(theme.line_width.max(1.0));
884 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
885 continue;
886 }
887 Marker::Star => {
888 let mut p = Path::new();
890 let inner = radius * 0.382;
891 for j in 0..10 {
892 let angle = std::f64::consts::FRAC_PI_2
893 + j as f64 * std::f64::consts::PI / 5.0;
894 let r = if j % 2 == 0 { radius } else { inner };
895 let sx = pt.x + r * angle.cos();
896 let sy = pt.y - r * angle.sin();
897 if j == 0 {
898 p.move_to(sx, sy);
899 } else {
900 p.line_to(sx, sy);
901 }
902 }
903 p.close();
904 p
905 }
906 };
907
908 renderer.fill_path(&marker_path, &paint, Affine::IDENTITY);
909 }
910 }
911
912 fn draw_bar(
914 &self,
915 renderer: &mut impl Renderer,
916 artist: &BarArtist,
917 plot_area: &Rect,
918 xmin: f64,
919 xmax: f64,
920 ymin: f64,
921 ymax: f64,
922 ) {
923 let n = artist.categories.len();
924 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
925 let paint = Paint::new(color);
926
927 if artist.horizontal {
928 let cat_range = ymax - ymin;
930 let cat_step = cat_range / n as f64;
931 let bar_half = cat_step * artist.bar_width * 0.5;
932
933 for i in 0..n {
934 let cat_center = ymin + (i as f64 + 0.5) * cat_step;
935 let value = artist.heights.data[i];
936
937 let left_val = 0.0_f64.min(value);
938 let right_val = 0.0_f64.max(value);
939
940 let p_left = self.data_to_pixel(left_val, cat_center - bar_half, plot_area, xmin, xmax, ymin, ymax);
941 let p_right = self.data_to_pixel(right_val, cat_center + bar_half, plot_area, xmin, xmax, ymin, ymax);
942
943 let rect = Rect::from_points(p_left, p_right);
944 let bar_path = Path::rect(rect);
945 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
946 }
947 } else {
948 let cat_range = xmax - xmin;
950 let cat_step = cat_range / n as f64;
951 let bar_half = cat_step * artist.bar_width * 0.5;
952
953 for i in 0..n {
954 let cat_center = xmin + (i as f64 + 0.5) * cat_step;
955 let value = artist.heights.data[i];
956
957 let bottom_val = 0.0_f64.min(value);
958 let top_val = 0.0_f64.max(value);
959
960 let p_bl = self.data_to_pixel(cat_center - bar_half, bottom_val, plot_area, xmin, xmax, ymin, ymax);
961 let p_tr = self.data_to_pixel(cat_center + bar_half, top_val, plot_area, xmin, xmax, ymin, ymax);
962
963 let rect = Rect::from_points(p_bl, p_tr);
964 let bar_path = Path::rect(rect);
965 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
966 }
967 }
968 }
969
970 fn draw_hist(
972 &self,
973 renderer: &mut impl Renderer,
974 artist: &HistArtist,
975 plot_area: &Rect,
976 xmin: f64,
977 xmax: f64,
978 ymin: f64,
979 ymax: f64,
980 ) {
981 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
982 let paint = Paint::new(color);
983 let stroke_paint = Paint::new(Color::WHITE);
984 let stroke = Stroke::new(0.5);
985
986 for i in 0..artist.counts.len() {
987 let left = artist.bin_edges[i];
988 let right = artist.bin_edges[i + 1];
989 let height = artist.counts[i];
990
991 if height <= 0.0 {
992 continue;
993 }
994
995 let p_bl = self.data_to_pixel(left, 0.0, plot_area, xmin, xmax, ymin, ymax);
996 let p_tr = self.data_to_pixel(right, height, plot_area, xmin, xmax, ymin, ymax);
997
998 let rect = Rect::from_points(p_bl, p_tr);
999 let bar_path = Path::rect(rect);
1000 renderer.fill_path(&bar_path, &paint, Affine::IDENTITY);
1001 renderer.stroke_path(&bar_path, &stroke_paint, &stroke, Affine::IDENTITY);
1003 }
1004 }
1005
1006 fn draw_fill_between(
1009 &self,
1010 renderer: &mut impl Renderer,
1011 artist: &FillBetweenArtist,
1012 plot_area: &Rect,
1013 xmin: f64,
1014 xmax: f64,
1015 ymin: f64,
1016 ymax: f64,
1017 ) {
1018 if artist.x.is_empty() {
1019 return;
1020 }
1021
1022 let n = artist.x.len();
1023 let mut path = Path::new();
1024
1025 let first = self.data_to_pixel(
1027 artist.x.data[0],
1028 artist.y1.data[0],
1029 plot_area,
1030 xmin, xmax, ymin, ymax,
1031 );
1032 path.move_to(first.x, first.y);
1033 for i in 1..n {
1034 let pt = self.data_to_pixel(
1035 artist.x.data[i],
1036 artist.y1.data[i],
1037 plot_area,
1038 xmin, xmax, ymin, ymax,
1039 );
1040 path.line_to(pt.x, pt.y);
1041 }
1042
1043 for i in (0..n).rev() {
1045 let pt = self.data_to_pixel(
1046 artist.x.data[i],
1047 artist.y2.data[i],
1048 plot_area,
1049 xmin, xmax, ymin, ymax,
1050 );
1051 path.line_to(pt.x, pt.y);
1052 }
1053 path.close();
1054
1055 let color = artist.color.with_alpha((artist.alpha * 255.0) as u8);
1056 let paint = Paint::new(color);
1057 renderer.fill_path(&path, &paint, Affine::IDENTITY);
1058 }
1059
1060 fn draw_spines(
1066 &self,
1067 renderer: &mut impl Renderer,
1068 plot_area: &Rect,
1069 theme: &Theme,
1070 ) {
1071 let paint = Paint::new(theme.spine_color);
1072 let stroke = Stroke::new(theme.spine_width);
1073
1074 if theme.show_bottom_spine {
1076 let mut p = Path::new();
1077 p.move_to(plot_area.x, plot_area.bottom());
1078 p.line_to(plot_area.right(), plot_area.bottom());
1079 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1080 }
1081 if theme.show_left_spine {
1083 let mut p = Path::new();
1084 p.move_to(plot_area.x, plot_area.y);
1085 p.line_to(plot_area.x, plot_area.bottom());
1086 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1087 }
1088 if theme.show_top_spine {
1090 let mut p = Path::new();
1091 p.move_to(plot_area.x, plot_area.y);
1092 p.line_to(plot_area.right(), plot_area.y);
1093 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1094 }
1095 if theme.show_right_spine {
1097 let mut p = Path::new();
1098 p.move_to(plot_area.right(), plot_area.y);
1099 p.line_to(plot_area.right(), plot_area.bottom());
1100 renderer.stroke_path(&p, &paint, &stroke, Affine::IDENTITY);
1101 }
1102 }
1103
1104 fn draw_ticks(
1110 &self,
1111 renderer: &mut impl Renderer,
1112 plot_area: &Rect,
1113 xticks: &[ticks::Tick],
1114 yticks: &[ticks::Tick],
1115 xmin: f64,
1116 xmax: f64,
1117 ymin: f64,
1118 ymax: f64,
1119 theme: &Theme,
1120 ) {
1121 let tick_paint = Paint::new(theme.tick_color);
1122 let tick_stroke = Stroke::new(1.0);
1123 let tick_len = theme.tick_length;
1124
1125 let label_style = TextStyle {
1126 size: theme.tick_label_size,
1127 color: theme.text_color,
1128 weight: FontWeight::Normal,
1129 family: theme.font_family.clone(),
1130 halign: HAlign::Center,
1131 valign: VAlign::Top,
1132 };
1133
1134 let outward = matches!(theme.tick_direction, TickDirection::Outward);
1136
1137 for tick in xticks {
1139 let pt = self.data_to_pixel(tick.value, ymin, plot_area, xmin, xmax, ymin, ymax);
1140 if pt.x < plot_area.x - 0.5 || pt.x > plot_area.right() + 0.5 {
1142 continue;
1143 }
1144 let x = pt.x;
1145 let y_base = plot_area.bottom();
1146
1147 let (y_start, y_end) = if outward {
1149 (y_base, y_base + tick_len)
1150 } else {
1151 (y_base - tick_len, y_base)
1152 };
1153 let mut tp = Path::new();
1154 tp.move_to(x, y_start);
1155 tp.line_to(x, y_end);
1156 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1157
1158 let label_y = if outward {
1160 y_base + tick_len + 2.0
1161 } else {
1162 y_base + 2.0
1163 };
1164 renderer.draw_text(
1165 &tick.label,
1166 Point::new(x, label_y),
1167 &label_style,
1168 Affine::IDENTITY,
1169 );
1170 }
1171
1172 let y_label_style = TextStyle {
1174 halign: HAlign::Right,
1175 valign: VAlign::Middle,
1176 ..label_style.clone()
1177 };
1178
1179 for tick in yticks {
1180 let pt = self.data_to_pixel(xmin, tick.value, plot_area, xmin, xmax, ymin, ymax);
1181 if pt.y < plot_area.y - 0.5 || pt.y > plot_area.bottom() + 0.5 {
1183 continue;
1184 }
1185 let y = pt.y;
1186 let x_base = plot_area.x;
1187
1188 let (x_start, x_end) = if outward {
1190 (x_base - tick_len, x_base)
1191 } else {
1192 (x_base, x_base + tick_len)
1193 };
1194 let mut tp = Path::new();
1195 tp.move_to(x_start, y);
1196 tp.line_to(x_end, y);
1197 renderer.stroke_path(&tp, &tick_paint, &tick_stroke, Affine::IDENTITY);
1198
1199 let label_x = if outward {
1201 x_base - tick_len - 3.0
1202 } else {
1203 x_base - 3.0
1204 };
1205 renderer.draw_text(
1206 &tick.label,
1207 Point::new(label_x, y),
1208 &y_label_style,
1209 Affine::IDENTITY,
1210 );
1211 }
1212 }
1213
1214 fn draw_labels(
1220 &self,
1221 renderer: &mut impl Renderer,
1222 plot_area: &Rect,
1223 bounds: &Rect,
1224 theme: &Theme,
1225 ) {
1226 if let Some(title) = &self.title {
1228 let style = TextStyle {
1229 size: theme.title_size,
1230 color: theme.text_color,
1231 weight: theme.title_weight,
1232 family: theme.font_family.clone(),
1233 halign: HAlign::Center,
1234 valign: VAlign::Bottom,
1235 };
1236 let x = plot_area.x + plot_area.width / 2.0;
1237 let y = plot_area.y - 10.0;
1238 renderer.draw_text(title, Point::new(x, y), &style, Affine::IDENTITY);
1239 }
1240
1241 if let Some(xlabel) = &self.xlabel {
1243 let style = TextStyle {
1244 size: theme.axis_label_size,
1245 color: theme.text_color,
1246 weight: FontWeight::Normal,
1247 family: theme.font_family.clone(),
1248 halign: HAlign::Center,
1249 valign: VAlign::Top,
1250 };
1251 let x = plot_area.x + plot_area.width / 2.0;
1252 let y = plot_area.bottom() + theme.tick_length + theme.tick_label_size + 8.0;
1254 renderer.draw_text(xlabel, Point::new(x, y), &style, Affine::IDENTITY);
1255 }
1256
1257 if let Some(ylabel) = &self.ylabel {
1259 let style = TextStyle {
1260 size: theme.axis_label_size,
1261 color: theme.text_color,
1262 weight: FontWeight::Normal,
1263 family: theme.font_family.clone(),
1264 halign: HAlign::Center,
1265 valign: VAlign::Bottom,
1266 };
1267 let x = bounds.x + 4.0;
1268 let y = plot_area.y + plot_area.height / 2.0;
1269 let rotate = Affine::rotate(-std::f64::consts::FRAC_PI_2);
1271 let translate_to = Affine::translate(kurbo::Vec2::new(x, y));
1272 let translate_back = Affine::translate(kurbo::Vec2::new(-x, -y));
1273 let transform = translate_to * rotate * translate_back;
1274 renderer.draw_text(ylabel, Point::new(x, y), &style, transform);
1275 }
1276 }
1277
1278 fn draw_legend(
1287 &self,
1288 renderer: &mut impl Renderer,
1289 plot_area: &Rect,
1290 theme: &Theme,
1291 ) {
1292 let entries: Vec<LegendEntry> = self
1295 .artists
1296 .iter()
1297 .filter_map(|a| {
1298 let (label, color, swatch) = match a {
1299 Artist::Line(a) => (a.label.as_deref(), a.color, SwatchKind::Line),
1300 Artist::Scatter(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1301 Artist::Bar(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1302 Artist::Histogram(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1303 Artist::FillBetween(a) => (a.label.as_deref(), a.color, SwatchKind::Filled),
1304 };
1305 label.map(|l| LegendEntry { label: l.to_string(), color, swatch })
1306 })
1307 .collect();
1308
1309 legend::draw_legend(renderer, &entries, plot_area, self.legend_loc, theme);
1310 }
1311
1312 fn data_to_pixel(
1322 &self,
1323 x: f64,
1324 y: f64,
1325 plot_area: &Rect,
1326 xmin: f64,
1327 xmax: f64,
1328 ymin: f64,
1329 ymax: f64,
1330 ) -> Point {
1331 let tx = self.xscale.transform(x, xmin, xmax);
1332 let ty = self.yscale.transform(y, ymin, ymax);
1333 Point::new(
1334 plot_area.x + tx * plot_area.width,
1335 plot_area.y + (1.0 - ty) * plot_area.height, )
1337 }
1338}
1339
1340#[cfg(test)]
1345mod tests {
1346 use super::*;
1347
1348 #[test]
1349 fn new_axes_has_defaults() {
1350 let ax = Axes::new();
1351 assert!(ax.artists.is_empty());
1352 assert!(ax.title.is_none());
1353 assert!(ax.xlabel.is_none());
1354 assert!(ax.ylabel.is_none());
1355 assert!(ax.xlim.is_none());
1356 assert!(ax.ylim.is_none());
1357 assert!(!ax.show_legend);
1358 assert_eq!(ax.color_index, 0);
1359 }
1360
1361 #[test]
1362 fn plot_creates_line_artist() {
1363 let mut ax = Axes::new();
1364 let result = ax.plot(vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]);
1365 assert!(result.is_ok());
1366 assert_eq!(ax.artists.len(), 1);
1367 assert!(matches!(&ax.artists[0], Artist::Line(_)));
1368 assert_eq!(ax.color_index, 1);
1369 }
1370
1371 #[test]
1372 fn plot_length_mismatch() {
1373 let mut ax = Axes::new();
1374 let result = ax.plot(vec![1.0, 2.0], vec![1.0]);
1375 assert!(matches!(
1376 result,
1377 Err(PlotError::SeriesLengthMismatch { expected: 2, got: 1 })
1378 ));
1379 }
1380
1381 #[test]
1382 fn plot_empty_data() {
1383 let mut ax = Axes::new();
1384 let result = ax.plot(Vec::<f64>::new(), Vec::<f64>::new());
1385 assert!(matches!(result, Err(PlotError::EmptyData)));
1386 }
1387
1388 #[test]
1389 fn scatter_creates_artist() {
1390 let mut ax = Axes::new();
1391 let result = ax.scatter(vec![1.0, 2.0], vec![3.0, 4.0]);
1392 assert!(result.is_ok());
1393 assert!(matches!(&ax.artists[0], Artist::Scatter(_)));
1394 }
1395
1396 #[test]
1397 fn bar_creates_artist() {
1398 let mut ax = Axes::new();
1399 let cats: &[&str] = &["a", "b", "c"];
1400 let result = ax.bar(cats, vec![10.0, 20.0, 30.0]);
1401 assert!(result.is_ok());
1402 match &ax.artists[0] {
1403 Artist::Bar(a) => {
1404 assert!(!a.horizontal);
1405 assert_eq!(a.categories.len(), 3);
1406 }
1407 _ => panic!("expected Bar artist"),
1408 }
1409 }
1410
1411 #[test]
1412 fn barh_creates_horizontal_artist() {
1413 let mut ax = Axes::new();
1414 let cats: &[&str] = &["x", "y"];
1415 let result = ax.barh(cats, vec![5.0, 10.0]);
1416 assert!(result.is_ok());
1417 match &ax.artists[0] {
1418 Artist::Bar(a) => assert!(a.horizontal),
1419 _ => panic!("expected Bar artist"),
1420 }
1421 }
1422
1423 #[test]
1424 fn hist_computes_bins() {
1425 let mut ax = Axes::new();
1426 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1427 let result = ax.hist(data, 5);
1428 assert!(result.is_ok());
1429 match &ax.artists[0] {
1430 Artist::Histogram(a) => {
1431 assert_eq!(a.bin_edges.len(), 6); assert_eq!(a.counts.len(), 5);
1433 let total: f64 = a.counts.iter().sum();
1435 assert_eq!(total, 10.0);
1436 }
1437 _ => panic!("expected Hist artist"),
1438 }
1439 }
1440
1441 #[test]
1442 fn hist_single_value() {
1443 let mut ax = Axes::new();
1444 let result = ax.hist(vec![5.0, 5.0, 5.0], 3);
1445 assert!(result.is_ok());
1446 match &ax.artists[0] {
1447 Artist::Histogram(a) => {
1448 let total: f64 = a.counts.iter().sum();
1449 assert_eq!(total, 3.0);
1450 }
1451 _ => panic!("expected Hist artist"),
1452 }
1453 }
1454
1455 #[test]
1456 fn hist_empty_data() {
1457 let mut ax = Axes::new();
1458 let result = ax.hist(Vec::<f64>::new(), 10);
1459 assert!(matches!(result, Err(PlotError::EmptyData)));
1460 }
1461
1462 #[test]
1463 fn fill_between_creates_artist() {
1464 let mut ax = Axes::new();
1465 let result = ax.fill_between(
1466 vec![1.0, 2.0, 3.0],
1467 vec![1.0, 2.0, 1.0],
1468 vec![0.0, 0.0, 0.0],
1469 );
1470 assert!(result.is_ok());
1471 assert!(matches!(&ax.artists[0], Artist::FillBetween(_)));
1472 }
1473
1474 #[test]
1475 fn fill_between_length_mismatch() {
1476 let mut ax = Axes::new();
1477 let result = ax.fill_between(vec![1.0, 2.0], vec![1.0], vec![0.0, 0.0]);
1478 assert!(matches!(result, Err(PlotError::SeriesLengthMismatch { .. })));
1479 }
1480
1481 #[test]
1482 fn configuration_methods_return_self() {
1483 let mut ax = Axes::new();
1484 ax.set_title("Test")
1485 .set_xlabel("X")
1486 .set_ylabel("Y")
1487 .set_xlim(0.0, 10.0)
1488 .set_ylim(-1.0, 1.0)
1489 .set_xscale(Scale::Linear)
1490 .set_yscale(Scale::Log10)
1491 .grid(true)
1492 .legend();
1493
1494 assert_eq!(ax.title.as_deref(), Some("Test"));
1495 assert_eq!(ax.xlabel.as_deref(), Some("X"));
1496 assert_eq!(ax.ylabel.as_deref(), Some("Y"));
1497 assert_eq!(ax.xlim, Some((0.0, 10.0)));
1498 assert_eq!(ax.ylim, Some((-1.0, 1.0)));
1499 assert_eq!(ax.show_grid, Some(true));
1500 assert!(ax.show_legend);
1501 }
1502
1503 #[test]
1504 fn color_cycle_advances() {
1505 let mut ax = Axes::new();
1506 for _ in 0..12 {
1507 ax.plot(vec![1.0, 2.0], vec![1.0, 2.0]).unwrap();
1508 }
1509 assert_eq!(ax.color_index, 12);
1510 match (&ax.artists[0], &ax.artists[10]) {
1513 (Artist::Line(a), Artist::Line(b)) => {
1514 assert_eq!(a.color, b.color);
1515 }
1516 _ => panic!("expected Line artists"),
1517 }
1518 }
1519
1520 #[test]
1521 fn data_to_pixel_linear() {
1522 let ax = Axes::new();
1523 let plot_area = Rect::new(100.0, 50.0, 400.0, 300.0);
1524
1525 let p = ax.data_to_pixel(0.0, 0.0, &plot_area, 0.0, 10.0, 0.0, 10.0);
1527 assert!((p.x - 100.0).abs() < 1e-10);
1528 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);
1532 assert!((p.x - 500.0).abs() < 1e-10);
1533 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);
1537 assert!((p.x - 300.0).abs() < 1e-10);
1538 assert!((p.y - 200.0).abs() < 1e-10);
1539 }
1540
1541 #[test]
1542 fn compute_data_limits_no_artists() {
1543 let ax = Axes::new();
1544 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
1545 assert!(xmin < xmax);
1547 assert!(ymin < ymax);
1548 }
1549
1550 #[test]
1551 fn compute_data_limits_with_user_override() {
1552 let mut ax = Axes::new();
1553 ax.set_xlim(-5.0, 5.0).set_ylim(0.0, 100.0);
1554 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
1555 assert!((xmin - (-5.0)).abs() < f64::EPSILON);
1556 assert!((xmax - 5.0).abs() < f64::EPSILON);
1557 assert!((ymin - 0.0).abs() < f64::EPSILON);
1558 assert!((ymax - 100.0).abs() < f64::EPSILON);
1559 }
1560
1561 #[test]
1562 fn compute_data_limits_from_line_data() {
1563 let mut ax = Axes::new();
1564 ax.plot(vec![1.0, 5.0, 10.0], vec![2.0, 8.0, 3.0]).unwrap();
1565 let (xmin, xmax, ymin, ymax) = ax.compute_data_limits();
1566 assert!(xmin < 1.0);
1568 assert!(xmax > 10.0);
1569 assert!(ymin < 2.0);
1570 assert!(ymax > 8.0);
1571 }
1572}