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