Skip to main content

ravenrs_graphics/
plot.rs

1//! The charts of a dashboard: the same numbers as [`crate::chart`], laid out for a
2//! screen. Besides what to draw, it says where each thing landed, so that whoever
3//! shows it can tell what is under the pointer without asking for another picture.
4//!
5//! [`crate::chart`] stays as it is, so documents already printed keep their looks.
6
7use std::f32::consts::{FRAC_PI_2, TAU};
8use std::rc::Rc;
9
10use ravenrs_core::{Color, LinearGradient, Point, Rect, monotone_slopes};
11
12use crate::chart::{
13    ChartData, ChartDrawing, ChartKind, ChartLabel, ChartOptions, ChartStyle, LabelAlign, arc, nice_ticks,
14};
15use crate::vector::{self, Layer, PathSegment, Shape};
16
17/// How a line goes from one point to the next.
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19#[non_exhaustive]
20#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
21pub enum Curve {
22    /// A smooth curve that may swing past the points.
23    #[default]
24    Natural,
25    /// A smooth curve that never swings past the points, so it invents no dips.
26    Monotone,
27    /// Straight lines.
28    Linear,
29    /// Level until halfway, then a jump.
30    Step,
31}
32
33/// How series share the same category.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35#[non_exhaustive]
36#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
37pub enum Stack {
38    /// Side by side, or over each other.
39    #[default]
40    None,
41    /// Each on top of the one before.
42    Normal,
43    /// On top of each other, and always adding up to the whole height.
44    Expand,
45}
46
47/// Which gridlines a chart has, as they look on the screen.
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49#[non_exhaustive]
50#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
51pub enum Grid {
52    /// Level lines.
53    #[default]
54    Horizontal,
55    /// Upright lines.
56    Vertical,
57    /// Both.
58    Both,
59    /// No lines.
60    None,
61}
62
63/// The shape of the grid of a radar or radial chart.
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65#[non_exhaustive]
66#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
67pub enum PolarGrid {
68    /// Polygons through the spokes.
69    #[default]
70    Polygon,
71    /// Circles.
72    Circle,
73    /// Nothing behind the data.
74    None,
75}
76
77/// Where a label sits relative to what it names.
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79#[non_exhaustive]
80#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
81pub enum LabelAt {
82    /// Just outside it: above a point or a bar, around a slice.
83    #[default]
84    Outside,
85    /// In its middle.
86    Inside,
87    /// Inside, where it starts.
88    InsideStart,
89}
90
91/// The look of a chart. Start from [`PlotStyle::new`] and change what differs.
92#[allow(missing_debug_implementations, reason = "holds closures")]
93#[derive(Clone)]
94#[non_exhaustive]
95pub struct PlotStyle {
96    /// The kind of chart.
97    pub kind: ChartKind,
98    /// Series colors, or slice and ring colors, repeated as needed.
99    pub colors: Vec<Color>,
100    /// Colors per category, over the colors of the series.
101    pub item_colors: Vec<Color>,
102    /// Text that matters, such as the number in the center.
103    pub ink: Color,
104    /// Text around the data.
105    pub muted: Color,
106    /// Gridlines.
107    pub grid: Color,
108    /// What is behind the chart. Slices are kept apart by a real gap, so it no longer
109    /// paints anything.
110    pub surface: Color,
111    /// The track behind a ring.
112    pub track: Color,
113    /// The font size of the text around the data.
114    pub size: f32,
115    /// How lines go from point to point.
116    pub curve: Curve,
117    /// How series share a category.
118    pub stack: Stack,
119    /// Fills areas from strong at the top to faint at the bottom, and bars from strong
120    /// at the tip to fainter at the base.
121    pub gradient: bool,
122    /// How solid an area or a radar polygon is.
123    pub fill_opacity: f32,
124    /// Bars lie down, with their categories on the left.
125    pub horizontal: bool,
126    /// The value axis runs the other way: bars hang from the top, or if they lie down
127    /// they grow to the left from the right.
128    pub reversed: bool,
129    /// The shortest a bar with a value gets, however small the value is.
130    pub min_bar: f32,
131    /// The categories of bars sit on the line of zero, on its other side from the bar,
132    /// instead of along the edge of the chart.
133    pub labels_at_zero: bool,
134    /// The corners of a bar.
135    pub bar_radius: f32,
136    /// The widest a bar gets.
137    pub max_bar: f32,
138    /// The color of a bar below zero.
139    pub negative: Option<Color>,
140    /// The color of a candle that closed at or above where it opened.
141    pub rising: Color,
142    /// The color of a candle that closed below where it opened.
143    pub falling: Color,
144    /// The category standing out: a slice comes forward, a bar gets an outline.
145    pub active: Option<usize>,
146    /// Draws a dot at each point of a line or a radar.
147    pub dots: bool,
148    /// Where the values are written, if at all.
149    pub value_labels: Option<LabelAt>,
150    /// Where the categories are written beside the data, if at all.
151    pub category_labels: Option<LabelAt>,
152    /// How numbers are written.
153    pub format: Rc<dyn Fn(f64) -> String>,
154    /// How categories are written on the axis.
155    pub tick_format: Rc<dyn Fn(&str) -> String>,
156    /// Gridlines of a chart with axes.
157    pub grid_lines: Grid,
158    /// Writes the categories along the axis.
159    pub category_axis: bool,
160    /// Writes numbers along the axis.
161    pub value_axis: bool,
162    /// The scale fits the values instead of starting from zero.
163    pub tight_scale: bool,
164    /// The gap between slices; zero for none.
165    pub separator: f32,
166    /// How far each slice reaches, in order, as a share of the room from the hole to
167    /// the edge. One with no number here reaches the edge.
168    pub slice_radii: Vec<f32>,
169    /// Labels outside a round chart sit in a column on each side, none over another,
170    /// each joined to its slice by a line with an elbow.
171    pub label_guides: bool,
172    /// The hole of a doughnut or of rings, as a share of the radius.
173    pub hole: f32,
174    /// One ring per series, instead of only the first series.
175    pub concentric: bool,
176    /// The grid of a radar or radial chart.
177    pub polar_grid: PolarGrid,
178    /// Fills the grid of a radar chart with the first color.
179    pub polar_fill: bool,
180    /// Lines from the center of a radar chart to each category.
181    pub spokes: bool,
182    /// How many rings the grid of a radar chart has.
183    pub rings: usize,
184    /// Radar polygons with no fill.
185    pub lines_only: bool,
186    /// Writes numbers along the radius of a radar chart.
187    pub radius_axis: bool,
188    /// Where rings start, in degrees clockwise from the top.
189    pub start_angle: f32,
190    /// How many degrees the largest ring covers.
191    pub sweep: f32,
192    /// Draws the track behind each ring.
193    pub tracks: bool,
194    /// A big number and a caption in the middle of a doughnut or of rings.
195    pub center: Option<(String, String)>,
196    /// Leaves gridlines out of [`Plot::layers`] and says where they go in
197    /// [`Plot::rules`], for a screen that can line them up with its pixels and fade
198    /// the data without them. Pages leave this off and get everything in the layers.
199    pub rules_apart: bool,
200    /// Leaves bars out of [`Plot::layers`] and says where they go in [`Plot::bars`],
201    /// for a screen that draws them itself at no cost when the size changes. The
202    /// gridlines they cross are left out from behind them, so that a bar that fades
203    /// shows no line through it.
204    pub bars_apart: bool,
205    /// Gridlines in dashes.
206    pub grid_dashed: bool,
207    /// About how many steps the value axis has; round numbers decide how many exactly.
208    pub value_ticks: usize,
209    /// Upright gridlines evenly spread into this many columns, instead of at each
210    /// category or number, and whatever [`Self::grid_lines`] says of the others.
211    pub grid_columns: Option<usize>,
212    /// The first category along the axis starts at its point and the last one ends
213    /// at its point, so that they line up with the ends of a line or an area.
214    pub ends_aligned: bool,
215    /// Lines across the data at values that matter, such as a target.
216    pub references: Vec<Reference>,
217    /// The value axis goes exactly from the first number to the second, and what is
218    /// past them is cut off, instead of the axis fitting the values.
219    pub domain: Option<(f64, f64)>,
220}
221
222/// A dashed line across a chart at a value that matters.
223#[derive(Clone, Debug, PartialEq)]
224#[non_exhaustive]
225pub struct Reference {
226    /// Where it goes.
227    pub value: f64,
228    /// What it says, at its end.
229    pub label: Option<String>,
230    /// Its color; [`PlotStyle::muted`] if it has none.
231    pub color: Option<Color>,
232}
233
234impl Reference {
235    /// A line at this value.
236    pub fn new(value: f64, label: Option<String>, color: Option<Color>) -> Self {
237        Self { value, label, color }
238    }
239}
240
241impl PlotStyle {
242    /// The look most charts of this kind want.
243    pub fn new(kind: ChartKind) -> Self {
244        Self {
245            kind,
246            colors: crate::chart::PALETTE.to_vec(),
247            item_colors: Vec::new(),
248            ink: Color::BLACK,
249            muted: Color::rgba(0.35, 0.39, 0.46, 1.0),
250            grid: Color::rgba(0.85, 0.87, 0.9, 1.0),
251            surface: Color::WHITE,
252            track: Color::rgba(0.92, 0.93, 0.95, 1.0),
253            size: 12.0,
254            curve: Curve::Natural,
255            stack: Stack::None,
256            gradient: false,
257            fill_opacity: if kind == ChartKind::Radar { 0.6 } else { 0.4 },
258            horizontal: false,
259            reversed: false,
260            min_bar: 0.5,
261            labels_at_zero: false,
262            bar_radius: 6.0,
263            max_bar: 96.0,
264            negative: None,
265            rising: crate::chart::PALETTE[1],
266            falling: crate::chart::PALETTE[3],
267            active: None,
268            dots: false,
269            value_labels: None,
270            category_labels: None,
271            format: Rc::new(|value| format!("{value:.0}")),
272            tick_format: Rc::new(str::to_owned),
273            grid_lines: Grid::Horizontal,
274            category_axis: true,
275            value_axis: false,
276            tight_scale: false,
277            separator: 2.0,
278            slice_radii: Vec::new(),
279            label_guides: false,
280            hole: if kind == ChartKind::Doughnut { 0.6 } else { 0.0 },
281            concentric: false,
282            polar_grid: if kind == ChartKind::RadialBar { PolarGrid::None } else { PolarGrid::Polygon },
283            polar_fill: false,
284            spokes: true,
285            rings: 5,
286            lines_only: false,
287            radius_axis: false,
288            start_angle: 0.0,
289            sweep: 360.0,
290            tracks: false,
291            center: None,
292            rules_apart: false,
293            bars_apart: false,
294            grid_dashed: false,
295            value_ticks: TICKS,
296            grid_columns: None,
297            ends_aligned: false,
298            references: Vec::new(),
299            domain: None,
300        }
301    }
302
303    fn color_of(&self, series: usize) -> Color {
304        if self.colors.is_empty() { Color::BLACK } else { self.colors[series % self.colors.len()] }
305    }
306
307    /// The color of a category of a series: its own, if it has one.
308    fn item_color(&self, series_color: Color, index: usize) -> Color {
309        self.item_colors.get(index).copied().unwrap_or(series_color)
310    }
311}
312
313/// A category of a chart with axes, and the strip of the chart that belongs to it.
314#[derive(Clone, Copy, Debug, PartialEq)]
315#[non_exhaustive]
316pub struct Spot {
317    /// Which category.
318    pub index: usize,
319    /// The strip the pointer is over when it means this category.
320    pub band: Rect,
321    /// The middle of the strip's data: where a cursor goes.
322    pub center: Point,
323}
324
325/// A value's place on the chart: where a dot goes, or an active marker.
326#[derive(Clone, Copy, Debug, PartialEq)]
327#[non_exhaustive]
328pub struct Mark {
329    /// Which series.
330    pub series: usize,
331    /// Which category.
332    pub index: usize,
333    /// Where it landed.
334    pub at: Point,
335    /// Its color.
336    pub color: Color,
337}
338
339/// A slice of a circle or of a ring, for telling what is under the pointer.
340#[derive(Clone, Copy, Debug, PartialEq)]
341#[non_exhaustive]
342pub struct Sector {
343    /// Which series.
344    pub series: usize,
345    /// Which category.
346    pub index: usize,
347    /// The center of the circle.
348    pub center: Point,
349    /// Where the slice starts, from the center.
350    pub inner: f32,
351    /// Where the slice ends, from the center.
352    pub outer: f32,
353    /// Its first angle, in radians clockwise from the right.
354    pub from: f32,
355    /// Its last angle.
356    pub to: f32,
357    /// Its color.
358    pub color: Color,
359}
360
361impl Sector {
362    fn contains(&self, p: Point) -> bool {
363        let (dx, dy) = (p.x - self.center.x, p.y - self.center.y);
364        let distance = (dx * dx + dy * dy).sqrt();
365        if distance < self.inner || distance > self.outer {
366            return false;
367        }
368        let past_start = (dy.atan2(dx) - self.from).rem_euclid(TAU);
369        past_start <= self.to - self.from
370    }
371}
372
373/// A bar as it landed, for drawing it again over the picture.
374#[derive(Clone, Copy, Debug, PartialEq)]
375#[non_exhaustive]
376pub struct Bar {
377    /// Which series.
378    pub series: usize,
379    /// Which category.
380    pub index: usize,
381    /// Where it is.
382    pub rect: Rect,
383    /// Its color.
384    pub color: Color,
385    /// Its corners: top left, top right, bottom right, bottom left.
386    pub radii: [f32; 4],
387    /// What fills it in place of its color, from its tip to its base.
388    pub gradient: Option<LinearGradient>,
389}
390
391/// What a price did in a category of a [`ChartKind::Candlestick`].
392#[derive(Clone, Copy, Debug, PartialEq)]
393#[non_exhaustive]
394pub struct Candle {
395    /// Which category.
396    pub index: usize,
397    /// From where it opened to where it closed.
398    pub body: Rect,
399    /// From its body up to its high, and down to its low.
400    pub wicks: [Rect; 2],
401    /// Its color.
402    pub color: Color,
403    /// It closed at or above where it opened: its body is an outline, so that this
404    /// does not rest on the color alone.
405    pub rising: bool,
406}
407
408/// A thin straight line left out of the picture.
409#[derive(Clone, Copy, Debug, PartialEq)]
410#[non_exhaustive]
411pub struct Rule {
412    /// Where it is.
413    pub rect: Rect,
414    /// Its color.
415    pub color: Color,
416}
417
418/// What is under a point of the chart.
419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420#[non_exhaustive]
421pub struct Hit {
422    /// Which category.
423    pub index: usize,
424    /// Which series, when the point means only one.
425    pub series: Option<usize>,
426}
427
428/// A chart laid out.
429#[derive(Clone, Debug, Default, PartialEq)]
430#[non_exhaustive]
431pub struct Plot {
432    /// What to draw, back to front.
433    pub layers: Vec<Layer>,
434    /// The text, to draw over the layers.
435    pub labels: Vec<ChartLabel>,
436    /// The categories of a chart with axes.
437    pub spots: Vec<Spot>,
438    /// Where each value landed.
439    pub marks: Vec<Mark>,
440    /// The slices of a round chart.
441    pub sectors: Vec<Sector>,
442    /// Where the data is, inside the axes.
443    pub area: Rect,
444    /// The bars of a bar chart.
445    pub bars: Vec<Bar>,
446    /// The candles of a [`ChartKind::Candlestick`], when [`PlotStyle::bars_apart`] leaves
447    /// them out of the layers.
448    pub candles: Vec<Candle>,
449    /// Gridlines, behind the layers, when [`PlotStyle::rules_apart`] asks for them.
450    pub rules: Vec<Rule>,
451    /// Lines of reference, over the layers, when [`PlotStyle::rules_apart`] asks for them.
452    pub top_rules: Vec<Rule>,
453    /// How many of the first layers are the backdrop the data is drawn over, such as
454    /// the rings and spokes of a radar, which stay put while the data draws in.
455    pub backdrop: usize,
456    /// The layers that only point at the data, such as the guide from a slice to its
457    /// label: they come in with the labels, once the data is nearly whole.
458    pub notes: Vec<usize>,
459    /// The labels that belong to the data, such as a bar's value, and not to an axis.
460    pub data_labels: Vec<usize>,
461    /// Where the value axis has its zero, inside the area: what bars grow out of.
462    pub baseline: Option<f32>,
463    /// The gridlines before [`Self::rules`] stopped at the bars, for bars of another
464    /// length, such as while they grow, to cut them again with [`rules_around_bars`].
465    pub uncut_rules: Vec<Rule>,
466}
467
468impl From<ChartDrawing> for Plot {
469    fn from(drawing: ChartDrawing) -> Self {
470        Self {
471            layers: drawing.shapes.into_iter().map(Layer::Shape).collect(),
472            labels: drawing.labels,
473            ..Self::default()
474        }
475    }
476}
477
478impl Plot {
479    /// What is under `point`, in the coordinates the chart was laid out in.
480    pub fn hit(&self, point: Point) -> Option<Hit> {
481        if let Some(sector) = self.sectors.iter().find(|s| s.contains(point)) {
482            return Some(Hit { index: sector.index, series: Some(sector.series) });
483        }
484        self.spots.iter().find(|s| s.band.contains(point)).map(|s| Hit { index: s.index, series: None })
485    }
486
487    /// Whether `point` is over the slice of this category, once it has grown by `by`.
488    pub fn over_grown_slice(&self, point: Point, index: usize, by: f32) -> bool {
489        self.sectors
490            .iter()
491            .filter(|s| s.index == index)
492            .any(|s| Sector { outer: s.outer + by, ..*s }.contains(point))
493    }
494
495    /// Whether this category has any place on the chart, and how many there are.
496    pub fn categories(&self) -> usize {
497        let spots = self.spots.iter().map(|s| s.index + 1).max().unwrap_or(0);
498        spots.max(self.sectors.iter().map(|s| s.index + 1).max().unwrap_or(0))
499    }
500
501    /// Where a tooltip about a category should point at.
502    pub fn anchor_of(&self, index: usize) -> Option<Point> {
503        if let Some(spot) = self.spots.iter().find(|s| s.index == index) {
504            return Some(spot.center);
505        }
506        let sector = self.sectors.iter().find(|s| s.index == index)?;
507        let middle = (sector.from + sector.to) / 2.0;
508        let radius = (sector.inner + sector.outer) / 2.0;
509        Some(Point::new(sector.center.x + radius * middle.cos(), sector.center.y + radius * middle.sin()))
510    }
511}
512
513/// Roughly how wide a character is, as a share of the font size.
514const CHAR_WIDTH: f32 = 0.56;
515const TICKS: usize = 4;
516/// Room on the sides of a line, so that dots and labels at its ends stay in.
517const LINE_MARGIN: f32 = 12.0;
518const TICK_MARGIN: f32 = 8.0;
519const GRID_DASH: f32 = 4.0;
520const GRID_DASH_GAP: f32 = 3.0;
521const BAR_GAP: f32 = 4.0;
522/// How many series a candlestick chart needs: open, high, low and close.
523const CANDLE_SERIES: usize = 4;
524/// The share of its category a candle's body takes.
525const CANDLE_WIDTH: f32 = 0.6;
526/// How solid the base of a bar in a gradient is.
527const BAR_GRADIENT_BASE: f32 = 0.55;
528/// The share of a category left between its bars and the next category's.
529const CATEGORY_GAP: f32 = 0.2;
530const DOT_RADIUS: f32 = 4.0;
531const ACTIVE_SLICE_GROWTH: f32 = 10.0;
532const OUTSIDE_LINE: f32 = 10.0;
533/// How far a guide goes out of its slice before it turns.
534const GUIDE_OUT: f32 = 12.0;
535/// How far it then goes along, to its label.
536const GUIDE_RUN: f32 = 14.0;
537const GUIDE_GAP: f32 = 4.0;
538/// The least a slice reaches, or it would not be seen.
539const SLICE_RADIUS_MIN: f32 = 0.1;
540/// A slice smaller than this share of the circle has no label of its own.
541const SLICE_LABEL_MIN: f64 = 0.04;
542const RING_GAP: f32 = 4.0;
543const CENTER_VALUE_SCALE: f32 = 2.4;
544
545/// How wide text is at a font size, for whoever knows its font.
546pub type Measure<'a> = &'a dyn Fn(&str, f32) -> f32;
547
548/// How wide labels are: measured when there is a font, or else estimated, as a page
549/// that has not chosen its font yet does.
550#[derive(Clone, Copy)]
551struct Ruler<'a>(Option<Measure<'a>>);
552
553impl Ruler<'_> {
554    fn width(&self, content: &str, size: f32) -> f32 {
555        match self.0 {
556            Some(measure) => measure(content, size),
557            None => content.chars().count() as f32 * size * CHAR_WIDTH,
558        }
559    }
560
561    fn longest<'t>(&self, texts: impl Iterator<Item = &'t str>, size: f32) -> f32 {
562        texts.map(|t| self.width(t, size)).fold(0.0, f32::max)
563    }
564}
565
566/// Lays a chart out inside `rect`.
567pub fn draw(data: &ChartData, rect: Rect, style: &PlotStyle) -> Plot {
568    layout(data, rect, style, Ruler(None))
569}
570
571/// Lays a chart out inside `rect`, knowing how wide its labels really are.
572pub fn draw_measured(data: &ChartData, rect: Rect, style: &PlotStyle, measure: Measure<'_>) -> Plot {
573    layout(data, rect, style, Ruler(Some(measure)))
574}
575
576/// Lays out a scatter chart or a sparkline just as [`crate::chart`] does, and says
577/// where each value landed: each one owns the strip from halfway to its neighbors.
578pub fn draw_small(data: &ChartData, rect: Rect, options: &ChartOptions<'_>, style: &ChartStyle<'_>) -> Plot {
579    let mut places = Vec::new();
580    let drawing = crate::chart::draw_placed(data, rect, options, style, &mut places);
581    let mut plot = Plot::from(drawing);
582    plot.marks =
583        places.iter().map(|p| Mark { series: p.series, index: p.index, at: p.at, color: p.color }).collect();
584    let (top, bottom) =
585        places.iter().fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| (a.min(p.at.y), b.max(p.at.y)));
586    plot.area = if top <= bottom { Rect::new(rect.x(), top, rect.width(), bottom - top) } else { rect };
587    let mut columns: Vec<(usize, f32)> = Vec::new();
588    for place in &places {
589        if !columns.iter().any(|(index, _)| *index == place.index) {
590            columns.push((place.index, place.at.x));
591        }
592    }
593    columns.sort_by(|a, b| a.1.total_cmp(&b.1));
594    for (i, (index, x)) in columns.iter().enumerate() {
595        let from = if i == 0 { rect.x() } else { (columns[i - 1].1 + x) / 2.0 };
596        let to = columns.get(i + 1).map_or(rect.max_x(), |next| (next.1 + x) / 2.0);
597        plot.spots.push(Spot {
598            index: *index,
599            band: Rect::new(from, rect.y(), to - from, rect.height()),
600            center: Point::new(*x, plot.area.center().y),
601        });
602    }
603    plot
604}
605
606fn layout(data: &ChartData, rect: Rect, style: &PlotStyle, ruler: Ruler<'_>) -> Plot {
607    let mut plot = Plot { area: rect, ..Plot::default() };
608    if rect.width() <= 0.0 || rect.height() <= 0.0 {
609        return plot;
610    }
611    match style.kind {
612        ChartKind::Pie | ChartKind::Doughnut => pie(data, rect, style, ruler, &mut plot),
613        ChartKind::Radar => radar(data, rect, style, &mut plot),
614        ChartKind::RadialBar => radial(data, rect, style, &mut plot),
615        _ => cartesian(data, rect, style, ruler, &mut plot),
616    }
617    plot
618}
619
620fn text(content: impl Into<String>, rect: Rect, align: LabelAlign, size: f32, color: Color) -> ChartLabel {
621    ChartLabel { text: content.into(), rect, align, size, bold: false, color }
622}
623
624fn centered(content: impl Into<String>, at: Point, size: f32, color: Color) -> ChartLabel {
625    let content = content.into();
626    let width = (content.chars().count() as f32 * size * CHAR_WIDTH + 8.0) * 2.0;
627    text(
628        content,
629        Rect::new(at.x - width / 2.0, at.y - size, width, size * 2.0),
630        LabelAlign::Center,
631        size,
632        color,
633    )
634}
635
636fn with_alpha(color: Color, alpha: f32) -> Color {
637    Color::rgba(color.r, color.g, color.b, alpha)
638}
639
640/// Text that reads over this color.
641fn ink_over(color: Color) -> Color {
642    let luminance = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
643    if luminance > 0.6 { Color::rgba(0.07, 0.08, 0.11, 1.0) } else { Color::WHITE }
644}
645
646fn stroke(path: Vec<PathSegment>, width: f32, color: Color) -> Layer {
647    Layer::Shape(Shape { path, fill: None, stroke: Some((width, color)) })
648}
649
650fn fill(path: Vec<PathSegment>, color: Color) -> Layer {
651    Layer::Shape(Shape::filled(path, color))
652}
653
654fn dot(at: Point, radius: f32, color: Color) -> Layer {
655    let side = radius * 2.0;
656    fill(vector::ellipse(Rect::new(at.x - radius, at.y - radius, side, side)), color)
657}
658
659/// The two control points of each stretch of a natural spline, along one axis,
660/// from the system that makes curves agree in slope and bend where they meet.
661fn natural_controls(values: &[f32]) -> (Vec<f32>, Vec<f32>) {
662    let n = values.len() - 1;
663    let (mut lower, mut main, mut right) = (vec![1.0; n], vec![4.0; n], vec![0.0; n]);
664    lower[0] = 0.0;
665    main[0] = 2.0;
666    right[0] = values[0] + 2.0 * values[1];
667    for i in 1..n - 1 {
668        right[i] = 4.0 * values[i] + 2.0 * values[i + 1];
669    }
670    lower[n - 1] = 2.0;
671    main[n - 1] = 7.0;
672    right[n - 1] = 8.0 * values[n - 1] + values[n];
673    for i in 1..n {
674        let m = lower[i] / main[i - 1];
675        main[i] -= m;
676        right[i] -= m * right[i - 1];
677    }
678    let mut first = vec![0.0; n];
679    first[n - 1] = right[n - 1] / main[n - 1];
680    for i in (0..n - 1).rev() {
681        first[i] = (right[i] - first[i + 1]) / main[i];
682    }
683    let mut second = vec![0.0; n];
684    second[n - 1] = (values[n] + first[n - 1]) / 2.0;
685    for i in 0..n - 1 {
686        second[i] = 2.0 * values[i + 1] - first[i + 1];
687    }
688    (first, second)
689}
690
691/// The path through `points`, without its starting move.
692pub fn curve_through(points: &[Point], curve: Curve) -> Vec<PathSegment> {
693    if points.len() < 2 {
694        return Vec::new();
695    }
696    match curve {
697        Curve::Natural if points.len() > 2 => {
698            let xs: Vec<f32> = points.iter().map(|p| p.x).collect();
699            let ys: Vec<f32> = points.iter().map(|p| p.y).collect();
700            let ((x1, x2), (y1, y2)) = (natural_controls(&xs), natural_controls(&ys));
701            (0..points.len() - 1)
702                .map(|i| {
703                    PathSegment::Cubic(Point::new(x1[i], y1[i]), Point::new(x2[i], y2[i]), points[i + 1])
704                })
705                .collect()
706        }
707        Curve::Monotone => {
708            let slopes = monotone_slopes(points);
709            points
710                .windows(2)
711                .enumerate()
712                .map(|(i, pair)| {
713                    let third = (pair[1].x - pair[0].x) / 3.0;
714                    PathSegment::Cubic(
715                        Point::new(pair[0].x + third, pair[0].y + slopes[i] * third),
716                        Point::new(pair[1].x - third, pair[1].y - slopes[i + 1] * third),
717                        pair[1],
718                    )
719                })
720                .collect()
721        }
722        Curve::Step => {
723            let mut path = Vec::with_capacity(points.len() * 3);
724            for pair in points.windows(2) {
725                let middle = (pair[0].x + pair[1].x) / 2.0;
726                path.push(PathSegment::Line(Point::new(middle, pair[0].y)));
727                path.push(PathSegment::Line(Point::new(middle, pair[1].y)));
728                path.push(PathSegment::Line(pair[1]));
729            }
730            path
731        }
732        _ => points.iter().skip(1).map(|p| PathSegment::Line(*p)).collect(),
733    }
734}
735
736/// The corners of a bar, clockwise from the top left, kept to what fits in it.
737fn bar_corners(rect: Rect, start: f32, end: f32, horizontal: bool) -> [f32; 4] {
738    let limit = rect.width().min(rect.height()) / 2.0;
739    let (start, end) = (start.clamp(0.0, limit), end.clamp(0.0, limit));
740    if horizontal { [start, end, end, start] } else { [end, end, start, start] }
741}
742
743/// A rectangle with its own radius at the start and at the end of its length.
744fn bar_path(rect: Rect, start: f32, end: f32, horizontal: bool) -> Vec<PathSegment> {
745    if (start - end).abs() < f32::EPSILON {
746        return vector::rectangle(rect, start);
747    }
748    let (x0, x1, y0, y1) = (rect.x(), rect.max_x(), rect.y(), rect.max_y());
749    let [top_left, top_right, bottom_right, bottom_left] = bar_corners(rect, start, end, horizontal);
750    let at = Point::new;
751    let k = 1.0 - 0.552_284_8;
752    vec![
753        PathSegment::Move(at(x0 + top_left, y0)),
754        PathSegment::Line(at(x1 - top_right, y0)),
755        PathSegment::Cubic(at(x1 - top_right * k, y0), at(x1, y0 + top_right * k), at(x1, y0 + top_right)),
756        PathSegment::Line(at(x1, y1 - bottom_right)),
757        PathSegment::Cubic(
758            at(x1, y1 - bottom_right * k),
759            at(x1 - bottom_right * k, y1),
760            at(x1 - bottom_right, y1),
761        ),
762        PathSegment::Line(at(x0 + bottom_left, y1)),
763        PathSegment::Cubic(
764            at(x0 + bottom_left * k, y1),
765            at(x0, y1 - bottom_left * k),
766            at(x0, y1 - bottom_left),
767        ),
768        PathSegment::Line(at(x0, y0 + top_left)),
769        PathSegment::Cubic(at(x0, y0 + top_left * k), at(x0 + top_left * k, y0), at(x0 + top_left, y0)),
770        PathSegment::Close,
771    ]
772}
773
774/// The values of each series, stacked: where each one starts and ends per category.
775fn stacked(data: &ChartData, stack: Stack) -> Vec<Vec<(f64, f64)>> {
776    let count = data.series.iter().map(|s| s.values.len()).max().unwrap_or(0);
777    let value =
778        |s: usize, i: usize| data.series[s].values.get(i).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
779    let totals: Vec<f64> =
780        (0..count).map(|i| (0..data.series.len()).map(|s| value(s, i).abs()).sum()).collect();
781    let mut above = vec![0.0; count];
782    let mut below = vec![0.0; count];
783    (0..data.series.len())
784        .map(|s| {
785            (0..count)
786                .map(|i| match stack {
787                    Stack::None => (0.0, value(s, i)),
788                    Stack::Normal | Stack::Expand => {
789                        let mut v = value(s, i);
790                        if stack == Stack::Expand && totals[i] > 0.0 {
791                            v /= totals[i];
792                        }
793                        let tower = if v < 0.0 { &mut below } else { &mut above };
794                        let start = tower[i];
795                        tower[i] += v;
796                        (start, tower[i])
797                    }
798                })
799                .collect()
800        })
801        .collect()
802}
803
804/// Where the box of a label goes, `half` to each side of `at`: centered, unless a
805/// measured label would stick out of the chart, and then against the chart's edge.
806fn inside(at: f32, width: f32, half: f32, rect: Rect, ruler: Ruler<'_>) -> (f32, LabelAlign) {
807    if ruler.0.is_some() && at - width / 2.0 < rect.x() {
808        (rect.x(), LabelAlign::Left)
809    } else if ruler.0.is_some() && at + width / 2.0 > rect.max_x() {
810        (rect.max_x() - half * 2.0, LabelAlign::Right)
811    } else {
812        (at - half, LabelAlign::Center)
813    }
814}
815
816/// A gridline, wherever this chart wants them.
817fn push_rule(plot: &mut Plot, style: &PlotStyle, rect: Rect, color: Color) {
818    let mut push = |rect: Rect| {
819        if style.rules_apart {
820            plot.rules.push(Rule { rect, color });
821        } else {
822            plot.layers.push(fill(vector::rectangle(rect, 0.0), color));
823        }
824    };
825    if !style.grid_dashed {
826        push(rect);
827        return;
828    }
829    each_dash(rect, &mut push);
830}
831
832/// The dashes of a thin line, the last one cut short by its end.
833fn each_dash(rect: Rect, push: &mut dyn FnMut(Rect)) {
834    let upright = rect.height() > rect.width();
835    let length = if upright { rect.height() } else { rect.width() };
836    let mut from = 0.0;
837    while from < length {
838        let dash = GRID_DASH.min(length - from);
839        push(if upright {
840            Rect::new(rect.x(), rect.y() + from, rect.width(), dash)
841        } else {
842            Rect::new(rect.x() + from, rect.y(), dash, rect.height())
843        });
844        from += GRID_DASH + GRID_DASH_GAP;
845    }
846}
847
848struct Axes {
849    area: Rect,
850    low: f64,
851    high: f64,
852    horizontal: bool,
853    /// The axis runs the other way.
854    reversed: bool,
855    /// What is past the ends of the axis is cut off.
856    fixed: bool,
857}
858
859impl Axes {
860    /// How far along the value axis a number is, in the page's coordinates.
861    fn at(&self, value: f64) -> f32 {
862        let share = ((value - self.low) / (self.high - self.low).max(f64::EPSILON)) as f32;
863        let share = if self.reversed { 1.0 - share } else { share };
864        if self.horizontal {
865            self.area.x() + share * self.area.width()
866        } else {
867            self.area.max_y() - share * self.area.height()
868        }
869    }
870
871    /// The point of a value at a place along the category axis.
872    fn point(&self, along: f32, value: f64) -> Point {
873        if self.horizontal { Point::new(self.at(value), along) } else { Point::new(along, self.at(value)) }
874    }
875}
876
877/// The style a cartesian chart draws this data with, or nothing when there is nothing to
878/// draw. Prices do not add up, so candles are never stacked.
879fn cartesian_style<'a>(data: &ChartData, style: &'a PlotStyle) -> Option<std::borrow::Cow<'a, PlotStyle>> {
880    let is_candle = style.kind == ChartKind::Candlestick;
881    if data.categories.is_empty()
882        || data.series.is_empty()
883        || (is_candle && data.series.len() < CANDLE_SERIES)
884    {
885        return None;
886    }
887    Some(if is_candle && style.stack != Stack::None {
888        std::borrow::Cow::Owned(PlotStyle { stack: Stack::None, ..style.clone() })
889    } else {
890        std::borrow::Cow::Borrowed(style)
891    })
892}
893
894/// Where the value axis of a cartesian chart runs, and the values it stands on.
895struct Scale {
896    towers: Vec<Vec<(f64, f64)>>,
897    min: f64,
898    low: f64,
899    high: f64,
900    tick_values: Vec<f64>,
901    fixed: bool,
902}
903
904/// Where a cartesian chart's value axis starts and ends for this data, as drawing it picks:
905/// to ease from one set of data to another along a scale that moves with them, give the
906/// frames in between a [`PlotStyle::domain`] between the two.
907pub fn value_range(data: &ChartData, style: &PlotStyle) -> Option<(f64, f64)> {
908    let style = cartesian_style(data, style)?;
909    scale(data, &style).map(|scale| (scale.low, scale.high))
910}
911
912fn scale(data: &ChartData, style: &PlotStyle) -> Option<Scale> {
913    let is_candle = style.kind == ChartKind::Candlestick;
914    let towers = stacked(data, style.stack);
915    let is_bar = style.kind == ChartKind::Bar;
916    let banded = is_bar || is_candle;
917    let values = towers.iter().flatten().flat_map(|(a, b)| [*a, *b]);
918    let (mut min, mut max) =
919        values.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(v), b.max(v)));
920    if style.stack == Stack::None {
921        let real = data.series.iter().flat_map(|s| s.values.iter().copied()).filter(|v| v.is_finite());
922        (min, max) = real.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), v| (a.min(v), b.max(v)));
923    }
924    if !min.is_finite() {
925        return None;
926    }
927    // A candle is about where the price went, not how far it is from zero.
928    if !is_candle && (!style.tight_scale || is_bar) {
929        min = min.min(0.0);
930        max = max.max(0.0);
931    }
932    let domain =
933        style.domain.filter(|(a, b)| a.is_finite() && b.is_finite() && a < b && style.stack != Stack::Expand);
934    if domain.is_none() && style.stack != Stack::Expand {
935        // A target above every value is still worth seeing.
936        for reference in style.references.iter().filter(|r| r.value.is_finite()) {
937            min = min.min(reference.value);
938            max = max.max(reference.value);
939        }
940    }
941    if let Some((from, to)) = domain {
942        (min, max) = (from, to);
943    }
944    let (mut low, step, ticks) = nice_ticks(min, max, style.value_ticks.max(1));
945    let mut high = low + step * ticks.saturating_sub(1) as f64;
946    if style.stack == Stack::Expand {
947        high = 1.0;
948    }
949    let mut tick_values: Vec<f64> = if style.stack == Stack::Expand {
950        (0..=4).map(|i| f64::from(i) / 4.0).collect()
951    } else {
952        (0..ticks).map(|i| low + step * i as f64).collect()
953    };
954    // A line fitted to its values keeps a little room above its peak and below its lowest
955    // point, instead of running along the edge of the plot; values all on one side of zero
956    // stay on it.
957    let fitted = style.tight_scale && !banded && domain.is_none() && style.stack != Stack::Expand;
958    let domain = domain.or_else(|| {
959        let room = (max - min) * 0.06;
960        let below = if min >= 0.0 { (min - room).max(0.0) } else { min - room };
961        let above = if max <= 0.0 { (max + room).min(0.0) } else { max + room };
962        (fitted && below < above).then_some((below, above))
963    });
964    if let Some((from, to)) = domain {
965        // Round numbers stay, as far as they are inside what was asked for.
966        let slack = step * 1e-6;
967        tick_values.retain(|v| *v >= from - slack && *v <= to + slack);
968        (low, high) = (from, to);
969    }
970    Some(Scale { towers, min, low, high, tick_values, fixed: domain.is_some() })
971}
972
973fn cartesian(data: &ChartData, rect: Rect, style: &PlotStyle, ruler: Ruler<'_>, plot: &mut Plot) {
974    let Some(style) = cartesian_style(data, style) else { return };
975    let style = style.as_ref();
976    let Some(Scale { towers, min, low, high, tick_values, fixed }) = scale(data, style) else { return };
977    let count = data.categories.len();
978    let is_candle = style.kind == ChartKind::Candlestick;
979    let is_bar = style.kind == ChartKind::Bar;
980    let banded = is_bar || is_candle;
981    let tick_text = |value: f64| {
982        if style.stack == Stack::Expand { format!("{:.0}%", value * 100.0) } else { (style.format)(value) }
983    };
984    let size = style.size;
985    let line_height = size * 1.3;
986    let horizontal = is_bar && style.horizontal;
987    let tick_texts: Vec<String> = tick_values.iter().map(|v| tick_text(*v)).collect();
988    let longest_value = ruler.longest(tick_texts.iter().map(String::as_str), size).ceil();
989    let labels: Vec<String> = data.categories.iter().map(|c| (style.tick_format)(c)).collect();
990    let longest_label = ruler.longest(labels.iter().map(String::as_str), size).ceil();
991
992    let outside =
993        style.value_labels == Some(LabelAt::Outside) || style.category_labels == Some(LabelAt::Outside);
994    let mut left = rect.x();
995    let mut right = rect.max_x();
996    let mut top = rect.y() + 4.0;
997    let mut bottom = rect.max_y();
998    let reversed = is_bar && style.reversed;
999    if horizontal {
1000        let (categories, tips) = (longest_label + TICK_MARGIN, longest_value.max(size * 3.0) + TICK_MARGIN);
1001        if style.category_axis {
1002            if reversed {
1003                right -= categories;
1004            } else {
1005                left += categories;
1006            }
1007        }
1008        if style.value_axis {
1009            bottom -= line_height + TICK_MARGIN;
1010        }
1011        if outside {
1012            if reversed {
1013                left += tips;
1014            } else {
1015                right -= tips;
1016            }
1017        }
1018    } else {
1019        if style.category_axis {
1020            if reversed {
1021                top += line_height + TICK_MARGIN;
1022            } else {
1023                bottom -= line_height + TICK_MARGIN;
1024            }
1025        }
1026        if style.value_axis {
1027            left += longest_value + TICK_MARGIN;
1028        }
1029        if outside {
1030            let (tips, under_zero) = (line_height + 2.0, if min < 0.0 { line_height } else { 0.0 });
1031            // A label past a bar under zero needs the same room there.
1032            if reversed {
1033                bottom -= tips;
1034                top += under_zero;
1035            } else {
1036                top += tips;
1037                bottom -= under_zero;
1038            }
1039        }
1040        if !banded {
1041            left += if style.value_axis { 4.0 } else { LINE_MARGIN };
1042            right -= LINE_MARGIN;
1043        }
1044    }
1045    let area = Rect::new(left, top, (right - left).max(1.0), (bottom - top).max(1.0));
1046    plot.area = area;
1047    let axes = Axes { area, low, high, horizontal, reversed, fixed };
1048
1049    // The grid.
1050    let grid = with_alpha(style.grid, style.grid.a * 0.8);
1051    // Evenly spread columns take the place of whatever lines stand upright.
1052    let columns = style.grid_columns.filter(|n| *n > 0);
1053    let value_lines = if horizontal {
1054        columns.is_none() && matches!(style.grid_lines, Grid::Vertical | Grid::Both)
1055    } else {
1056        matches!(style.grid_lines, Grid::Horizontal | Grid::Both)
1057    };
1058    let category_lines = if horizontal {
1059        matches!(style.grid_lines, Grid::Horizontal | Grid::Both)
1060    } else {
1061        columns.is_none() && matches!(style.grid_lines, Grid::Vertical | Grid::Both)
1062    };
1063    if let Some(columns) = columns {
1064        for i in 0..=columns {
1065            let at = area.x() + area.width() * i as f32 / columns as f32;
1066            push_rule(plot, style, Rect::new(at - 0.5, area.y(), 1.0, area.height()), grid);
1067        }
1068    }
1069    let along_size = if horizontal { area.height() } else { area.width() };
1070    let along_start = if horizontal { area.y() } else { area.x() };
1071    let band = along_size / count as f32;
1072    let center_of = |i: usize| {
1073        if banded {
1074            along_start + band * (i as f32 + 0.5)
1075        } else if count > 1 {
1076            along_start + along_size * i as f32 / (count - 1) as f32
1077        } else {
1078            along_start + along_size / 2.0
1079        }
1080    };
1081    for value in &tick_values {
1082        let at = axes.at(*value);
1083        if value_lines {
1084            let line = if horizontal {
1085                Rect::new(at - 0.5, area.y(), 1.0, area.height())
1086            } else {
1087                Rect::new(area.x(), at - 0.5, area.width(), 1.0)
1088            };
1089            push_rule(plot, style, line, grid);
1090        }
1091        if style.value_axis {
1092            let label = tick_text(*value);
1093            plot.labels.push(if horizontal {
1094                let (x, align) = inside(at, ruler.width(&label, size), 40.0, rect, ruler);
1095                text(
1096                    label,
1097                    Rect::new(x, area.max_y() + TICK_MARGIN, 80.0, line_height),
1098                    align,
1099                    size,
1100                    style.muted,
1101                )
1102            } else {
1103                text(
1104                    label,
1105                    Rect::new(rect.x(), at - line_height / 2.0, longest_value, line_height),
1106                    LabelAlign::Right,
1107                    size,
1108                    style.muted,
1109                )
1110            });
1111        }
1112    }
1113    if category_lines {
1114        for i in 0..count {
1115            let at = center_of(i);
1116            let line = if horizontal {
1117                Rect::new(area.x(), at - 0.5, area.width(), 1.0)
1118            } else {
1119                Rect::new(at - 0.5, area.y(), 1.0, area.height())
1120            };
1121            push_rule(plot, style, line, grid);
1122        }
1123    }
1124
1125    // The categories along the axis, as many as fit, and the strips they own.
1126    let room = if horizontal { line_height + 2.0 } else { longest_label + TICK_MARGIN * 2.0 };
1127    let spacing = if count > 1 { (center_of(1) - center_of(0)).abs() } else { along_size };
1128    // Far from the size of a pixel: below one, categories are that much closer together.
1129    let every = (room / spacing.max(0.001)).ceil().max(1.0) as usize;
1130    let aligned = style.ends_aligned && !banded && !horizontal && count > 1;
1131    let last = count.saturating_sub(1);
1132    for (i, label) in labels.iter().enumerate() {
1133        let at = center_of(i);
1134        // With its ends lined up the last category always shows, in place of the one
1135        // before it if there is no room for both.
1136        let crowded = aligned && (last - i) as f32 * spacing < room && i != last;
1137        let shows = if aligned && i == last { true } else { i.is_multiple_of(every) && !crowded };
1138        // On the line of zero, it goes on the side the bars of this category leave free.
1139        let at_zero = (is_bar && style.labels_at_zero).then(|| {
1140            let values = data.series.iter().filter_map(|s| s.values.get(i)).filter(|v| v.is_finite());
1141            let mut seen = values.clone().peekable();
1142            let under = seen.peek().is_some()
1143                && values.clone().all(|v| *v <= 0.0)
1144                && values.clone().any(|v| *v < 0.0);
1145            (axes.at(0.0_f64.clamp(low, high)), under != reversed)
1146        });
1147        if style.category_axis && shows {
1148            plot.labels.push(if let (true, Some((zero, past))) = (horizontal, at_zero) {
1149                let x = if past { zero + TICK_MARGIN } else { zero - TICK_MARGIN - longest_label };
1150                let align = if past { LabelAlign::Left } else { LabelAlign::Right };
1151                let place = Rect::new(x, at - line_height / 2.0, longest_label, line_height);
1152                text(label.clone(), place, align, size, style.muted)
1153            } else if horizontal {
1154                let (x, align) = if reversed {
1155                    (area.max_x() + TICK_MARGIN, LabelAlign::Left)
1156                } else {
1157                    (rect.x(), LabelAlign::Right)
1158                };
1159                text(
1160                    label.clone(),
1161                    Rect::new(x, at - line_height / 2.0, longest_label, line_height),
1162                    align,
1163                    size,
1164                    style.muted,
1165                )
1166            } else {
1167                let (x, align) = if aligned && i == 0 {
1168                    (at, LabelAlign::Left)
1169                } else if aligned && i == last {
1170                    (at - spacing * 2.0, LabelAlign::Right)
1171                } else {
1172                    inside(at, ruler.width(label, size), spacing, rect, ruler)
1173                };
1174                let y = match at_zero {
1175                    Some((zero, true)) => zero - TICK_MARGIN - line_height,
1176                    Some((zero, false)) => zero + TICK_MARGIN,
1177                    None if reversed => area.y() - TICK_MARGIN - line_height,
1178                    None => area.max_y() + TICK_MARGIN,
1179                };
1180                text(label.clone(), Rect::new(x, y, spacing * 2.0, line_height), align, size, style.muted)
1181            });
1182        }
1183        let half = spacing / 2.0;
1184        let band_rect = if horizontal {
1185            Rect::new(rect.x(), at - half, rect.width(), spacing)
1186        } else {
1187            Rect::new(at - half, rect.y(), spacing, rect.height())
1188        };
1189        let center =
1190            if horizontal { Point::new(area.center().x, at) } else { Point::new(at, area.center().y) };
1191        plot.spots.push(Spot { index: i, band: band_rect, center });
1192    }
1193
1194    if is_bar {
1195        bars(data, &towers, style, &axes, band, plot);
1196        if style.bars_apart {
1197            plot.uncut_rules = plot.rules.clone();
1198            plot.rules = rules_around_bars(&plot.rules, &plot.bars);
1199        }
1200    } else if is_candle {
1201        candles(data, style, &axes, band, plot);
1202    } else {
1203        lines(data, &towers, style, &axes, &center_of, plot);
1204    }
1205    references(style, &axes, rect, ruler, plot);
1206}
1207
1208/// How far from its side a rounded corner leaves a line `from_end` past the bar's end.
1209fn corner_inset(radius: f32, from_end: f32) -> f32 {
1210    if radius <= 0.0 || from_end >= radius {
1211        return 0.0;
1212    }
1213    let rise = radius - from_end;
1214    radius - (radius * radius - rise * rise).max(0.0).sqrt()
1215}
1216
1217/// What is left of a thin line once it stops at the sides of a bar, following its
1218/// corners. A line along the bar's very edge stays: it is not behind it.
1219fn line_around_bar(line: Rect, bar: &Bar, out: &mut Vec<Rect>) {
1220    let [top_left, top_right, bottom_right, bottom_left] = bar.radii;
1221    let center = line.center();
1222    let upright = line.height() > line.width();
1223    let (from, to) = if upright {
1224        if center.x <= bar.rect.x() || center.x >= bar.rect.max_x() {
1225            out.push(line);
1226            return;
1227        }
1228        let (left, right) = (center.x - bar.rect.x(), bar.rect.max_x() - center.x);
1229        let start = corner_inset(top_left, left).max(corner_inset(top_right, right));
1230        let end = corner_inset(bottom_left, left).max(corner_inset(bottom_right, right));
1231        (bar.rect.y() + start, bar.rect.max_y() - end)
1232    } else {
1233        if center.y <= bar.rect.y() || center.y >= bar.rect.max_y() {
1234            out.push(line);
1235            return;
1236        }
1237        let (above, below) = (center.y - bar.rect.y(), bar.rect.max_y() - center.y);
1238        let start = corner_inset(top_left, above).max(corner_inset(bottom_left, below));
1239        let end = corner_inset(top_right, above).max(corner_inset(bottom_right, below));
1240        (bar.rect.x() + start, bar.rect.max_x() - end)
1241    };
1242    let (begin, finish) = if upright { (line.y(), line.max_y()) } else { (line.x(), line.max_x()) };
1243    let piece = |a: f32, b: f32| {
1244        if upright {
1245            Rect::new(line.x(), a, line.width(), b - a)
1246        } else {
1247            Rect::new(a, line.y(), b - a, line.height())
1248        }
1249    };
1250    if from > begin {
1251        out.push(piece(begin, from.min(finish)));
1252    }
1253    if to < finish {
1254        out.push(piece(to.max(begin), finish));
1255    }
1256}
1257
1258/// Gridlines stop where a bar starts and go on where it ends.
1259pub fn rules_around_bars(rules: &[Rule], bars: &[Bar]) -> Vec<Rule> {
1260    let mut kept = Vec::with_capacity(rules.len());
1261    let (mut pieces, mut next) = (Vec::new(), Vec::new());
1262    for rule in rules {
1263        pieces.clear();
1264        pieces.push(rule.rect);
1265        for bar in bars {
1266            next.clear();
1267            for piece in &pieces {
1268                line_around_bar(*piece, bar, &mut next);
1269            }
1270            std::mem::swap(&mut pieces, &mut next);
1271        }
1272        kept.extend(pieces.iter().map(|rect| Rule { rect: *rect, color: rule.color }));
1273    }
1274    kept
1275}
1276
1277/// The lines of reference that fall on the axis, over the data.
1278fn references(style: &PlotStyle, axes: &Axes, rect: Rect, ruler: Ruler<'_>, plot: &mut Plot) {
1279    let (area, size) = (axes.area, style.size);
1280    let line_height = size * 1.3;
1281    for reference in &style.references {
1282        if !reference.value.is_finite() || reference.value < axes.low || reference.value > axes.high {
1283            continue;
1284        }
1285        let at = axes.at(reference.value);
1286        let color = reference.color.unwrap_or(style.muted);
1287        let line = if axes.horizontal {
1288            Rect::new(at - 0.5, area.y(), 1.0, area.height())
1289        } else {
1290            Rect::new(area.x(), at - 0.5, area.width(), 1.0)
1291        };
1292        each_dash(line, &mut |rect| {
1293            if style.rules_apart {
1294                plot.top_rules.push(Rule { rect, color });
1295            } else {
1296                plot.layers.push(fill(vector::rectangle(rect, 0.0), color));
1297            }
1298        });
1299        let Some(label) = &reference.label else { continue };
1300        let wide = ruler.width(label, size).ceil() + 2.0;
1301        let place = if axes.horizontal {
1302            // At the top, on the side of the line with room.
1303            let x = if at + wide + 4.0 > rect.max_x() { at - wide - 4.0 } else { at + 4.0 };
1304            Rect::new(x, area.y(), wide, line_height)
1305        } else {
1306            // At the end, over the line unless that would leave the chart.
1307            let above = at - line_height - 1.0;
1308            let y = if above < rect.y() { at + 1.0 } else { above };
1309            Rect::new(area.max_x() - wide, y, wide, line_height)
1310        };
1311        plot.labels.push(text(label.clone(), place, LabelAlign::Right, size, color));
1312    }
1313}
1314
1315fn bars(
1316    data: &ChartData,
1317    towers: &[Vec<(f64, f64)>],
1318    style: &PlotStyle,
1319    axes: &Axes,
1320    band: f32,
1321    plot: &mut Plot,
1322) {
1323    let horizontal = axes.horizontal;
1324    let side_by_side = style.stack == Stack::None;
1325    let slots = if side_by_side { data.series.len().max(1) } else { 1 };
1326    let usable = band * (1.0 - CATEGORY_GAP) - BAR_GAP * (slots - 1) as f32;
1327    let thickness = (usable / slots as f32).clamp(1.0, style.max_bar);
1328    let group = thickness * slots as f32 + BAR_GAP * (slots - 1) as f32;
1329    let start = if horizontal { axes.area.y() } else { axes.area.x() };
1330    let size = style.size;
1331    let reversed = axes.reversed;
1332    plot.baseline = Some(axes.at(0.0_f64.clamp(axes.low, axes.high)));
1333    for (s, series) in data.series.iter().enumerate() {
1334        let series_color = style.color_of(s);
1335        let series_color = if series.color == Color::TRANSPARENT { series_color } else { series.color };
1336        for (i, (from, to)) in towers[s].iter().enumerate() {
1337            // A bar wholly past the ends of a fixed axis has nothing left to show.
1338            if axes.fixed && (from.max(*to) < axes.low || from.min(*to) > axes.high) {
1339                continue;
1340            }
1341            // Cut off, an end is square: it does not stop there.
1342            let cut_low = axes.fixed && from.min(*to) < axes.low;
1343            let cut_high = axes.fixed && from.max(*to) > axes.high;
1344            let reach = |value: f64| if axes.fixed { value.clamp(axes.low, axes.high) } else { value };
1345            let (from, to) = (&reach(*from), &reach(*to));
1346            let slot = if side_by_side { s } else { 0 };
1347            let along = start + band * i as f32 + (band - group) / 2.0 + (thickness + BAR_GAP) * slot as f32;
1348            let (a, b) = (axes.at(*from), axes.at(*to));
1349            // A bar too short grows away from its base, and nothing at all stays a sliver.
1350            let least = if from == to { style.min_bar.min(0.5) } else { style.min_bar };
1351            let b = if (b - a).abs() < least { a + least * if b < a { -1.0 } else { 1.0 } } else { b };
1352            let (low, high) = (a.min(b), a.max(b));
1353            let length = high - low;
1354            let rect = if horizontal {
1355                Rect::new(low, along, length, thickness)
1356            } else {
1357                Rect::new(along, low, thickness, length)
1358            };
1359            let original = series.values.get(i).copied().unwrap_or(0.0);
1360            let mut color = style.item_color(series_color, i);
1361            if original < 0.0
1362                && let Some(negative) = style.negative
1363            {
1364                color = negative;
1365            }
1366            let below = to < from;
1367            // In a tower, only its outer corners are round: those of whatever has a
1368            // value, each way from zero, wherever that is in the order of the series.
1369            let (base_round, tip_round) = match style.stack {
1370                Stack::None => (style.bar_radius, style.bar_radius),
1371                _ => {
1372                    let same_way = |tower: &Vec<(f64, f64)>| {
1373                        tower.get(i).is_some_and(|(a, b)| a != b && (b < a) == below)
1374                    };
1375                    let first = towers.iter().position(same_way);
1376                    let last = towers.iter().rposition(same_way);
1377                    (
1378                        if first == Some(s) { style.bar_radius } else { 0.0 },
1379                        if last == Some(s) { style.bar_radius } else { 0.0 },
1380                    )
1381                }
1382            };
1383            // `start` is the bottom or the left of the bar, and its tip may be there.
1384            let tip_at_start = below != reversed;
1385            let (first, end) = if tip_at_start { (tip_round, base_round) } else { (base_round, tip_round) };
1386            let (cut_start, cut_end) = if reversed { (cut_high, cut_low) } else { (cut_low, cut_high) };
1387            let (first, end) = (if cut_start { 0.0 } else { first }, if cut_end { 0.0 } else { end });
1388            let path = bar_path(rect, first, end, horizontal);
1389            let radii = bar_corners(rect, first, end, horizontal);
1390            let shown = if style.active == Some(i) { with_alpha(color, 0.8) } else { color };
1391            let gradient = style.gradient.then(|| {
1392                let (at_start, at_end) = if horizontal {
1393                    (Point::new(0.0, 0.0), Point::new(1.0, 0.0))
1394                } else {
1395                    (Point::new(0.0, 1.0), Point::new(0.0, 0.0))
1396                };
1397                let (tip, base) = if tip_at_start { (at_start, at_end) } else { (at_end, at_start) };
1398                let faint = with_alpha(shown, shown.a * BAR_GRADIENT_BASE);
1399                LinearGradient { start: tip, end: base, from: shown, to: faint }
1400            });
1401            // Nothing in a tower is not a line across it.
1402            let empty_in_tower = style.stack != Stack::None && from == to;
1403            if !empty_in_tower {
1404                plot.bars.push(Bar { series: s, index: i, rect, color: shown, radii, gradient });
1405            }
1406            if empty_in_tower {
1407            } else if style.active == Some(i) {
1408                if !style.bars_apart {
1409                    plot.layers.push(fill(path.clone(), shown));
1410                }
1411                plot.layers.push(Layer::Dashed { path, width: 2.0, dash: 4.0, color });
1412            } else if !style.bars_apart {
1413                plot.layers.push(fill(path, color));
1414            }
1415            let tip_value = *to;
1416            let center = rect.center();
1417            let tip = axes.point(along + thickness / 2.0, tip_value);
1418            plot.marks.push(Mark { series: s, index: i, at: tip, color });
1419            let category = data.categories.get(i).map(|c| (style.tick_format)(c)).unwrap_or_default();
1420            let number = (style.format)(original);
1421            let mut write = |content: String, at: LabelAt| {
1422                let wide = content.chars().count() as f32 * size * CHAR_WIDTH + 8.0;
1423                let height = size * 1.3;
1424                plot.labels.push(match (at, horizontal) {
1425                    (LabelAt::Outside, true) if reversed && original >= 0.0 => text(
1426                        content,
1427                        Rect::new(rect.x() - 6.0 - wide, center.y - height / 2.0, wide, height),
1428                        LabelAlign::Right,
1429                        size,
1430                        style.ink,
1431                    ),
1432                    (LabelAt::Outside, true) => text(
1433                        content,
1434                        Rect::new(rect.max_x() + 6.0, center.y - height / 2.0, wide, height),
1435                        LabelAlign::Left,
1436                        size,
1437                        style.ink,
1438                    ),
1439                    (LabelAt::Outside, false) => {
1440                        let under = (original < 0.0) != reversed;
1441                        let y = if under { rect.max_y() + 2.0 } else { rect.y() - height - 2.0 };
1442                        centered(content, Point::new(center.x, y + height / 2.0), size, style.ink)
1443                    }
1444                    (LabelAt::InsideStart, true) if reversed => text(
1445                        content,
1446                        Rect::new(rect.max_x() - 8.0 - wide, center.y - height / 2.0, wide, height),
1447                        LabelAlign::Right,
1448                        size,
1449                        ink_over(color),
1450                    ),
1451                    (LabelAt::InsideStart, true) => text(
1452                        content,
1453                        Rect::new(rect.x() + 8.0, center.y - height / 2.0, wide, height),
1454                        LabelAlign::Left,
1455                        size,
1456                        ink_over(color),
1457                    ),
1458                    (LabelAt::InsideStart, false) if reversed => {
1459                        centered(content, Point::new(center.x, rect.y() + height), size, ink_over(color))
1460                    }
1461                    (LabelAt::InsideStart, false) => {
1462                        centered(content, Point::new(center.x, rect.max_y() - height), size, ink_over(color))
1463                    }
1464                    _ => centered(content, center, size, ink_over(color)),
1465                });
1466                plot.data_labels.push(plot.labels.len() - 1);
1467            };
1468            if let Some(at) = style.value_labels {
1469                write(number, at);
1470            }
1471            if let Some(at) = style.category_labels {
1472                write(category, at);
1473            }
1474        }
1475    }
1476}
1477
1478fn candles(data: &ChartData, style: &PlotStyle, axes: &Axes, band: f32, plot: &mut Plot) {
1479    let [open, high, low, close] = [0, 1, 2, 3].map(|s: usize| data.series[s].values.as_slice());
1480    let width = (band * CANDLE_WIDTH).clamp(1.0, style.max_bar);
1481    let reach = |value: f64| if axes.fixed { value.clamp(axes.low, axes.high) } else { value };
1482    for i in 0..data.categories.len() {
1483        let value = |of: &[f64]| of.get(i).copied().filter(|v| v.is_finite());
1484        let (Some(open), Some(high), Some(low), Some(close)) =
1485            (value(open), value(high), value(low), value(close))
1486        else {
1487            continue;
1488        };
1489        // Whatever it says, the wick covers where the price was.
1490        let (top, bottom) = (high.max(open).max(close), low.min(open).min(close));
1491        if axes.fixed && (top < axes.low || bottom > axes.high) {
1492            continue;
1493        }
1494        let x = axes.area.x() + band * (i as f32 + 0.5);
1495        let (upper, lower) = (axes.at(reach(open.max(close))), axes.at(reach(open.min(close))));
1496        let body = Rect::new(x - width / 2.0, upper, width, (lower - upper).max(1.0));
1497        let (high_at, low_at) = (axes.at(reach(top)), axes.at(reach(bottom)));
1498        let wicks = [
1499            Rect::new(x - 0.5, high_at, 1.0, (body.y() - high_at).max(0.0)),
1500            Rect::new(x - 0.5, body.max_y(), 1.0, (low_at - body.max_y()).max(0.0)),
1501        ];
1502        let rising = close >= open;
1503        let color = if rising { style.rising } else { style.falling };
1504        plot.marks.push(Mark { series: 3, index: i, at: Point::new(x, axes.at(reach(close))), color });
1505        plot.candles.push(Candle { index: i, body, wicks, color, rising });
1506        if style.bars_apart {
1507            continue;
1508        }
1509        for wick in wicks {
1510            plot.layers.push(fill(vector::rectangle(wick, 0.0), color));
1511        }
1512        let outline = vector::rectangle(body, 1.0);
1513        plot.layers.push(if rising { stroke(outline, 1.5, color) } else { fill(outline, color) });
1514    }
1515}
1516
1517/// The lowest and the highest point of each column, in the order they come, and the
1518/// two ends: all a line with more points than columns can show.
1519fn thinned(points: &[Point], area: Rect) -> Vec<Point> {
1520    let mut kept = Vec::with_capacity(area.width() as usize * 2 + 2);
1521    let mut column = Vec::new();
1522    let mut flush = |column: &mut Vec<Point>| {
1523        let by_height = |a: &&Point, b: &&Point| a.y.total_cmp(&b.y);
1524        let (Some(low), Some(high)) = (column.iter().min_by(by_height), column.iter().max_by(by_height))
1525        else {
1526            return;
1527        };
1528        if low.x <= high.x {
1529            kept.extend([*low, *high]);
1530        } else {
1531            kept.extend([*high, *low]);
1532        }
1533        column.clear();
1534    };
1535    let mut current = f32::NEG_INFINITY;
1536    for point in points {
1537        let at = (point.x - area.x()).floor();
1538        if at != current {
1539            flush(&mut column);
1540            current = at;
1541        }
1542        column.push(*point);
1543    }
1544    flush(&mut column);
1545    if let (Some(first), Some(last)) = (points.first(), points.last()) {
1546        kept.insert(0, *first);
1547        kept.push(*last);
1548    }
1549    kept
1550}
1551
1552/// The path through the points of a line. With more of them than columns on the
1553/// screen a curve shows nothing a straight line does not, and past two per column
1554/// only the extremes of each can be told apart: they are all that is drawn, or a
1555/// long series would cost a picture far more than what can be seen of it.
1556fn curve_within(points: &[Point], curve: Curve, area: Rect) -> Vec<PathSegment> {
1557    let columns = area.width().max(1.0) as usize;
1558    if points.len() > columns * 2 {
1559        let kept = thinned(points, area);
1560        return curve_through(&kept, Curve::Linear);
1561    }
1562    curve_through(points, if points.len() > columns { Curve::Linear } else { curve })
1563}
1564
1565fn lines(
1566    data: &ChartData,
1567    towers: &[Vec<(f64, f64)>],
1568    style: &PlotStyle,
1569    axes: &Axes,
1570    center_of: &dyn Fn(usize) -> f32,
1571    plot: &mut Plot,
1572) {
1573    let filled = style.kind == ChartKind::Area;
1574    let is_stacked = style.stack != Stack::None;
1575    let size = style.size;
1576    for (s, series) in data.series.iter().enumerate() {
1577        let color = if series.color == Color::TRANSPARENT { style.color_of(s) } else { series.color };
1578        // A value that is missing breaks nothing: the line goes from the one before to the one after.
1579        let finite = |i: usize| series.values.get(i).is_some_and(|v| v.is_finite());
1580        let places: Vec<usize> = (0..towers[s].len()).filter(|i| finite(*i)).collect();
1581        let tops: Vec<Point> =
1582            places.iter().map(|i| Point::new(center_of(*i), axes.at(towers[s][*i].1))).collect();
1583        let (Some(first), Some(last)) = (tops.first().copied(), tops.last().copied()) else { continue };
1584        let curve = curve_within(&tops, style.curve, axes.area);
1585        let clipped = |layer: Layer| {
1586            if axes.fixed {
1587                // A little past the area, so a line along its edge keeps its width.
1588                Layer::Clipped { rect: axes.area.inflate(1.0), layer: Box::new(layer) }
1589            } else {
1590                layer
1591            }
1592        };
1593        if filled {
1594            let mut path = vec![PathSegment::Move(first)];
1595            path.extend(curve.iter().copied());
1596            if is_stacked {
1597                let mut under: Vec<Point> =
1598                    places.iter().map(|i| Point::new(center_of(*i), axes.at(towers[s][*i].0))).collect();
1599                under.reverse();
1600                path.push(PathSegment::Line(under[0]));
1601                path.extend(curve_within(&under, style.curve, axes.area));
1602            } else {
1603                let zero = axes.at(0.0_f64.clamp(axes.low, axes.high));
1604                path.push(PathSegment::Line(Point::new(last.x, zero)));
1605                path.push(PathSegment::Line(Point::new(first.x, zero)));
1606            }
1607            path.push(PathSegment::Close);
1608            if style.gradient {
1609                plot.layers.push(clipped(Layer::Gradient {
1610                    path,
1611                    top: (axes.area.y(), with_alpha(color, 0.8)),
1612                    bottom: (axes.area.max_y(), with_alpha(color, 0.1)),
1613                }));
1614            } else {
1615                plot.layers.push(clipped(fill(path, with_alpha(color, style.fill_opacity))));
1616            }
1617        }
1618        let mut line = vec![PathSegment::Move(first)];
1619        line.extend(curve);
1620        plot.layers.push(clipped(stroke(line, if filled { 1.5 } else { 2.0 }, color)));
1621        for (place, top) in places.iter().zip(&tops) {
1622            // A value past the ends of a fixed axis has no place on the chart.
1623            if axes.fixed && (top.y < axes.area.y() - 0.01 || top.y > axes.area.max_y() + 0.01) {
1624                continue;
1625            }
1626            let item = style.item_color(color, *place);
1627            plot.marks.push(Mark { series: s, index: *place, at: *top, color: item });
1628            if style.dots {
1629                plot.layers.push(dot(*top, DOT_RADIUS, item));
1630            }
1631            let above = Point::new(top.x, top.y - size * 1.1);
1632            if style.value_labels.is_some() {
1633                let value = series.values.get(*place).copied().unwrap_or(0.0);
1634                plot.data_labels.push(plot.labels.len());
1635                plot.labels.push(centered((style.format)(value), above, size, style.ink));
1636            }
1637            if style.category_labels.is_some() {
1638                let name = data.categories.get(*place).map(|c| (style.tick_format)(c)).unwrap_or_default();
1639                plot.data_labels.push(plot.labels.len());
1640                plot.labels.push(centered(name, above, size, style.ink));
1641            }
1642        }
1643    }
1644}
1645
1646/// A slice of a ring around `center`; a slice of a circle when `inner` is zero.
1647pub fn slice_path(center: Point, inner: f32, outer: f32, from: f32, to: f32) -> Vec<PathSegment> {
1648    let at = |r: f32, a: f32| Point::new(center.x + r * a.cos(), center.y + r * a.sin());
1649    let mut path = vec![PathSegment::Move(at(outer, from))];
1650    arc(&mut path, center, outer, from, to);
1651    if inner > 0.0 {
1652        path.push(PathSegment::Line(at(inner, to)));
1653        arc(&mut path, center, inner, to, from);
1654    } else {
1655        path.push(PathSegment::Line(center));
1656    }
1657    path.push(PathSegment::Close);
1658    path
1659}
1660
1661/// A slice as [`slice_path`] draws it, with `gap` left clear all around it: half on
1662/// each of its sides, of the same width all the way, and half at its ends. What is
1663/// behind the chart shows through.
1664pub fn gapped_slice(center: Point, inner: f32, outer: f32, from: f32, to: f32, gap: f32) -> Vec<PathSegment> {
1665    if gap <= 0.0 {
1666        return slice_path(center, inner, outer, from, to);
1667    }
1668    let half = gap / 2.0;
1669    let inner = if inner > 0.0 { inner + half } else { 0.0 };
1670    let outer = (outer - half).max(inner + 0.5);
1671    let span = to - from;
1672    // A whole circle has no sides to keep apart.
1673    if span >= TAU - 0.001 {
1674        return slice_path(center, inner, outer, from, to);
1675    }
1676    let at = |r: f32, a: f32| Point::new(center.x + r * a.cos(), center.y + r * a.sin());
1677    // A slice thinner than the gap keeps half of itself, or it would be gone.
1678    let along_outer = (half / outer).min(span / 4.0);
1679    let mut path = vec![PathSegment::Move(at(outer, from + along_outer))];
1680    arc(&mut path, center, outer, from + along_outer, to - along_outer);
1681    if inner > 0.0 {
1682        let along_inner = (half / inner).min(span / 4.0);
1683        path.push(PathSegment::Line(at(inner, to - along_inner)));
1684        arc(&mut path, center, inner, to - along_inner, from + along_inner);
1685    } else {
1686        // Where the two sides meet, once each has moved away from the other.
1687        let reach = half / (span / 2.0).sin().abs().max(0.05);
1688        path.push(PathSegment::Line(at(reach.min(outer / 2.0), (from + to) / 2.0)));
1689    }
1690    path.push(PathSegment::Close);
1691    path
1692}
1693
1694/// A label outside a round chart, before it knows where it goes.
1695struct Guide {
1696    text: String,
1697    angle: f32,
1698    reach: f32,
1699    color: Color,
1700    share: f64,
1701}
1702
1703/// Puts each side's labels in a column in the order of their slices, as close as they
1704/// fit to where their slices are, and draws the lines that join them.
1705fn place_guides(
1706    guides: Vec<Guide>,
1707    center: Point,
1708    rect: Rect,
1709    style: &PlotStyle,
1710    ruler: Ruler<'_>,
1711    plot: &mut Plot,
1712) {
1713    let line_height = style.size * 1.3;
1714    let edge = guides.iter().map(|g| g.reach).fold(0.0, f32::max);
1715    let (mut right, mut left): (Vec<Guide>, Vec<Guide>) =
1716        guides.into_iter().partition(|g| g.angle.cos() >= 0.0);
1717    for (side, sign) in [(&mut right, 1.0_f32), (&mut left, -1.0)] {
1718        let wanted = |g: &Guide| center.y + (g.reach + GUIDE_OUT) * g.angle.sin();
1719        let (top, bottom) = (rect.y() + line_height / 2.0, rect.max_y() - line_height / 2.0);
1720        // More than a column holds: the smallest slices go without.
1721        let holds = ((bottom - top) / line_height).floor().max(0.0) as usize + 1;
1722        side.sort_by(|a, b| b.share.total_cmp(&a.share));
1723        side.truncate(holds);
1724        side.sort_by(|a, b| wanted(a).total_cmp(&wanted(b)));
1725        let mut places: Vec<f32> = Vec::with_capacity(side.len());
1726        for guide in side.iter() {
1727            let least = places.last().map_or(top, |last| last + line_height);
1728            places.push(wanted(guide).max(least));
1729        }
1730        // What ran past the bottom pushes the others back up.
1731        let mut most = bottom;
1732        for place in places.iter_mut().rev() {
1733            *place = place.min(most);
1734            most = *place - line_height;
1735        }
1736        let column = center.x + sign * (edge + GUIDE_OUT + GUIDE_RUN);
1737        let clear = edge + GUIDE_OUT / 2.0;
1738        for (guide, y) in side.iter().zip(places) {
1739            let (cos, sin) = (guide.angle.cos(), guide.angle.sin());
1740            let from = Point::new(center.x + guide.reach * cos, center.y + guide.reach * sin);
1741            // It turns outside the circle, wherever the label had to go.
1742            let along = (clear * clear - (y - center.y).powi(2)).max(0.0).sqrt();
1743            let turn = (guide.reach + GUIDE_OUT) * cos.abs();
1744            let elbow = Point::new(center.x + sign * turn.max(along), y);
1745            let path = vec![
1746                PathSegment::Move(from),
1747                PathSegment::Line(elbow),
1748                PathSegment::Line(Point::new(column, y)),
1749            ];
1750            plot.notes.push(plot.layers.len());
1751            plot.layers.push(stroke(path, 1.0, with_alpha(guide.color, 0.8)));
1752            let wide = ruler.width(&guide.text, style.size).ceil() + 2.0;
1753            let (x, align) = if sign > 0.0 {
1754                (column + GUIDE_GAP, LabelAlign::Left)
1755            } else {
1756                (column - GUIDE_GAP - wide, LabelAlign::Right)
1757            };
1758            let place = Rect::new(x, y - line_height / 2.0, wide, line_height);
1759            plot.data_labels.push(plot.labels.len());
1760            plot.labels.push(text(guide.text.clone(), place, align, style.size, style.ink));
1761        }
1762    }
1763}
1764
1765/// What a guide says about a value of a round chart.
1766fn guide_text(data: &ChartData, style: &PlotStyle, index: usize, value: f64) -> String {
1767    let category = style.category_labels == Some(LabelAt::Outside);
1768    let number = style.value_labels == Some(LabelAt::Outside);
1769    let name = data.categories.get(index).map(|c| (style.tick_format)(c)).unwrap_or_default();
1770    match (category, number) {
1771        (true, true) => format!("{name}  {}", (style.format)(value)),
1772        (true, false) => name,
1773        _ => (style.format)(value),
1774    }
1775}
1776
1777fn center_text(style: &PlotStyle, center: Point, plot: &mut Plot) {
1778    let Some((value, caption)) = &style.center else { return };
1779    let big = style.size * CENTER_VALUE_SCALE;
1780    let mut number =
1781        centered(value.clone(), Point::new(center.x, center.y - style.size * 0.7), big, style.ink);
1782    number.bold = true;
1783    plot.labels.push(number);
1784    plot.labels.push(centered(
1785        caption.clone(),
1786        Point::new(center.x, center.y + big * 0.7),
1787        style.size,
1788        style.muted,
1789    ));
1790}
1791
1792fn pie(data: &ChartData, rect: Rect, style: &PlotStyle, ruler: Ruler<'_>, plot: &mut Plot) {
1793    let rings: Vec<usize> = if style.concentric { (0..data.series.len()).collect() } else { vec![0] };
1794    if data.series.is_empty() {
1795        return;
1796    }
1797    let outside =
1798        style.value_labels == Some(LabelAt::Outside) || style.category_labels == Some(LabelAt::Outside);
1799    let guided = outside && style.label_guides;
1800    let room = if outside { style.size * 1.5 + OUTSIDE_LINE } else { 2.0 };
1801    let growth = if style.active.is_some() { ACTIVE_SLICE_GROWTH } else { 0.0 };
1802    let center = rect.center();
1803    let radius = if guided {
1804        // The width of the labels comes out of the sides, and the top and the bottom
1805        // only need a line of text.
1806        let first = data.series.get(rings[0]).map_or(&[][..], |s| s.values.as_slice());
1807        let texts: Vec<String> =
1808            first.iter().enumerate().map(|(i, v)| guide_text(data, style, i, *v)).collect();
1809        let wide = ruler.longest(texts.iter().map(String::as_str), style.size).ceil();
1810        let across = rect.width() / 2.0 - wide - GUIDE_OUT - GUIDE_RUN - GUIDE_GAP - 2.0;
1811        let tall = rect.height() / 2.0 - style.size * 0.7 - GUIDE_OUT;
1812        (across.min(tall) - growth).max(rect.width().min(rect.height()) / 6.0).max(1.0)
1813    } else {
1814        (rect.width().min(rect.height()) / 2.0 - room - growth).max(1.0)
1815    };
1816    let hole = radius * style.hole;
1817    let mut guides = Vec::new();
1818    let ring_width = (radius - hole) / rings.len() as f32;
1819    for (depth, s) in rings.iter().enumerate() {
1820        let series = &data.series[*s];
1821        let total: f64 = series.values.iter().filter(|v| **v > 0.0).sum();
1822        if total <= 0.0 {
1823            continue;
1824        }
1825        let outer = radius - ring_width * depth as f32;
1826        let inner = if rings.len() == 1 { hole } else { outer - ring_width + 1.0 };
1827        let mut angle = -FRAC_PI_2;
1828        for (i, value) in series.values.iter().enumerate() {
1829            if *value <= 0.0 {
1830                continue;
1831            }
1832            let share = *value / total;
1833            let end = angle + share as f32 * TAU;
1834            let color = style.item_color(style.color_of(i), i);
1835            let share_of_room = style.slice_radii.get(i).map_or(1.0, |r| r.clamp(SLICE_RADIUS_MIN, 1.0));
1836            let own = inner + (outer - inner) * share_of_room;
1837            let reach = if style.active == Some(i) { own + growth } else { own };
1838            let path = gapped_slice(center, inner, reach, angle, end, style.separator);
1839            plot.layers.push(fill(path, color));
1840            plot.sectors.push(Sector {
1841                series: *s,
1842                index: i,
1843                center,
1844                inner,
1845                outer: reach,
1846                from: angle,
1847                to: end,
1848                color,
1849            });
1850            let middle = (angle + end) / 2.0;
1851            let inside = if inner > 0.0 { (reach + inner) / 2.0 } else { reach * 0.62 };
1852            plot.marks.push(Mark {
1853                series: *s,
1854                index: i,
1855                at: Point::new(center.x + inside * middle.cos(), center.y + inside * middle.sin()),
1856                color,
1857            });
1858            if guided && depth == 0 {
1859                let text = guide_text(data, style, i, *value);
1860                guides.push(Guide { text, angle: middle, reach, color, share });
1861            }
1862            if share >= SLICE_LABEL_MIN {
1863                let category = data.categories.get(i).map(|c| (style.tick_format)(c)).unwrap_or_default();
1864                let number = (style.format)(*value);
1865                let mut write = |content: String, at: LabelAt| match at {
1866                    LabelAt::Outside if guided => {}
1867                    LabelAt::Outside => {
1868                        let (cos, sin) = (middle.cos(), middle.sin());
1869                        let from = Point::new(center.x + reach * cos, center.y + reach * sin);
1870                        let to = Point::new(
1871                            center.x + (reach + OUTSIDE_LINE) * cos,
1872                            center.y + (reach + OUTSIDE_LINE) * sin,
1873                        );
1874                        plot.notes.push(plot.layers.len());
1875                        plot.layers.push(stroke(
1876                            vec![PathSegment::Move(from), PathSegment::Line(to)],
1877                            1.0,
1878                            with_alpha(color, 0.8),
1879                        ));
1880                        let wide = ruler.width(&content, style.size) + 6.0;
1881                        let tip = Point::new(to.x + cos * wide / 2.0, to.y + sin * style.size * 0.7);
1882                        plot.data_labels.push(plot.labels.len());
1883                        plot.labels.push(centered(content, tip, style.size, style.ink));
1884                    }
1885                    _ => {
1886                        let at =
1887                            Point::new(center.x + inside * middle.cos(), center.y + inside * middle.sin());
1888                        plot.data_labels.push(plot.labels.len());
1889                        plot.labels.push(centered(content, at, style.size, ink_over(color)));
1890                    }
1891                };
1892                if let Some(at) = style.value_labels {
1893                    write(number, at);
1894                }
1895                if let Some(at) = style.category_labels {
1896                    write(category, at);
1897                }
1898            }
1899            angle = end;
1900        }
1901    }
1902    place_guides(guides, center, rect, style, ruler, plot);
1903    center_text(style, center, plot);
1904}
1905
1906fn circle_path(center: Point, radius: f32) -> Vec<PathSegment> {
1907    vector::ellipse(Rect::new(center.x - radius, center.y - radius, radius * 2.0, radius * 2.0))
1908}
1909
1910fn polygon_path(points: &[Point]) -> Vec<PathSegment> {
1911    let mut path: Vec<PathSegment> = points
1912        .iter()
1913        .enumerate()
1914        .map(|(i, p)| if i == 0 { PathSegment::Move(*p) } else { PathSegment::Line(*p) })
1915        .collect();
1916    path.push(PathSegment::Close);
1917    path
1918}
1919
1920fn radar(data: &ChartData, rect: Rect, style: &PlotStyle, plot: &mut Plot) {
1921    let count = data.categories.len();
1922    if count < 3 || data.series.is_empty() {
1923        return;
1924    }
1925    let size = style.size;
1926    let center = rect.center();
1927    let room = size * 1.6 + if style.category_axis { 6.0 } else { 0.0 };
1928    let radius = (rect.width().min(rect.height()) / 2.0 - room).max(1.0);
1929    let max = data
1930        .series
1931        .iter()
1932        .flat_map(|s| s.values.iter().copied())
1933        .filter(|v| v.is_finite())
1934        .fold(0.0_f64, f64::max);
1935    let (_, step, ticks) = nice_ticks(0.0, max.max(f64::EPSILON), TICKS);
1936    let high = (step * ticks.saturating_sub(1) as f64).max(f64::EPSILON);
1937    let angle_of = |i: usize| -FRAC_PI_2 + TAU * i as f32 / count as f32;
1938    let at = |r: f32, a: f32| Point::new(center.x + r * a.cos(), center.y + r * a.sin());
1939    let outline = |r: f32| -> Vec<PathSegment> {
1940        match style.polar_grid {
1941            PolarGrid::Circle => circle_path(center, r),
1942            _ => polygon_path(&(0..count).map(|i| at(r, angle_of(i))).collect::<Vec<_>>()),
1943        }
1944    };
1945    if style.polar_grid != PolarGrid::None {
1946        if style.polar_fill {
1947            plot.layers.push(fill(outline(radius), with_alpha(style.color_of(0), 0.2)));
1948        }
1949        let rings = style.rings.max(1);
1950        for ring in 1..=rings {
1951            let share = ring as f32 / rings as f32;
1952            plot.layers.push(stroke(outline(radius * share), 1.0, style.grid));
1953        }
1954        if style.spokes {
1955            for i in 0..count {
1956                let path = vec![PathSegment::Move(center), PathSegment::Line(at(radius, angle_of(i)))];
1957                plot.layers.push(stroke(path, 1.0, style.grid));
1958            }
1959        }
1960    }
1961    plot.backdrop = plot.layers.len();
1962    for (s, series) in data.series.iter().enumerate() {
1963        let color = if series.color == Color::TRANSPARENT { style.color_of(s) } else { series.color };
1964        let points: Vec<Point> = (0..count)
1965            .map(|i| {
1966                let value = series.values.get(i).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
1967                at(radius * (value / high) as f32, angle_of(i))
1968            })
1969            .collect();
1970        let path = polygon_path(&points);
1971        if style.lines_only {
1972            plot.layers.push(stroke(path, 2.0, color));
1973        } else {
1974            plot.layers.push(Layer::Shape(Shape {
1975                path,
1976                fill: Some(with_alpha(color, style.fill_opacity)),
1977                stroke: Some((1.5, color)),
1978            }));
1979        }
1980        for (i, point) in points.iter().enumerate() {
1981            plot.marks.push(Mark { series: s, index: i, at: *point, color });
1982            if style.dots {
1983                plot.layers.push(dot(*point, DOT_RADIUS, color));
1984            }
1985        }
1986    }
1987    if style.radius_axis {
1988        let angle = -FRAC_PI_2 + TAU / 12.0;
1989        for tick in 1..ticks {
1990            let value = step * tick as f64;
1991            let spot = at(radius * (value / high) as f32, angle);
1992            plot.labels.push(centered((style.format)(value), spot, size * 0.9, style.muted));
1993        }
1994    }
1995    let half = TAU / count as f32 / 2.0;
1996    for i in 0..count {
1997        let angle = angle_of(i);
1998        if style.category_axis {
1999            let name = (style.tick_format)(&data.categories[i]);
2000            let lines = name.lines().count().max(1) as f32;
2001            let wide = name.lines().map(|l| l.chars().count()).max().unwrap_or(0) as f32 * size * CHAR_WIDTH;
2002            let spot = Point::new(
2003                center.x + (radius + 6.0) * angle.cos() + angle.cos() * wide / 2.0,
2004                center.y + (radius + 6.0 + size * lines * 0.5) * angle.sin(),
2005            );
2006            let mut label = centered(name, spot, size, style.muted);
2007            label.rect = Rect::new(
2008                spot.x - label.rect.width() / 2.0,
2009                spot.y - size * lines,
2010                label.rect.width(),
2011                size * lines * 2.0,
2012            );
2013            plot.labels.push(label);
2014        }
2015        // The pointer anywhere around a spoke means that category.
2016        plot.sectors.push(Sector {
2017            series: 0,
2018            index: i,
2019            center,
2020            inner: 0.0,
2021            outer: radius + room,
2022            from: angle - half,
2023            to: angle + half,
2024            color: Color::TRANSPARENT,
2025        });
2026    }
2027}
2028
2029fn radial(data: &ChartData, rect: Rect, style: &PlotStyle, plot: &mut Plot) {
2030    // An arc over its center, like a gauge, has room to sit lower and grow.
2031    let gauge = style.sweep <= 180.0 && (style.start_angle + 90.0).abs() < f32::EPSILON;
2032    let center_y_offset = if gauge { rect.height() * 0.25 } else { 0.0 };
2033    let center = Point::new(rect.center().x, rect.center().y + center_y_offset);
2034    let reach = if gauge {
2035        (rect.width() / 2.0).min(rect.height() * 0.75)
2036    } else {
2037        rect.width().min(rect.height()) / 2.0
2038    };
2039    let radius = (reach - 4.0).max(1.0);
2040    let hole = radius * if style.hole > 0.0 { style.hole } else { 0.45 };
2041    let start = style.start_angle.to_radians() - FRAC_PI_2;
2042    let sweep = style.sweep.to_radians();
2043    let stacking = style.stack != Stack::None;
2044    let Some(first) = data.series.first() else { return };
2045    let at = |r: f32, a: f32| Point::new(center.x + r * a.cos(), center.y + r * a.sin());
2046
2047    if stacking {
2048        // Every series is a piece of the same ring.
2049        let width = radius - hole;
2050        let middle = hole + width / 2.0;
2051        let total: f64 = data
2052            .series
2053            .iter()
2054            .map(|s| s.values.first().copied().filter(|v| v.is_finite() && *v > 0.0).unwrap_or(0.0))
2055            .sum();
2056        if total <= 0.0 {
2057            return;
2058        }
2059        let mut angle = start;
2060        // Each round end reaches into the next piece, and the first one is on top.
2061        let pieces: Vec<(usize, f32, f32, Color)> = data
2062            .series
2063            .iter()
2064            .enumerate()
2065            .map(|(s, series)| {
2066                let value = series.values.first().copied().filter(|v| v.is_finite()).unwrap_or(0.0);
2067                let end = angle + (value / total) as f32 * sweep;
2068                let piece = (
2069                    s,
2070                    angle,
2071                    end,
2072                    if series.color == Color::TRANSPARENT { style.color_of(s) } else { series.color },
2073                );
2074                angle = end;
2075                piece
2076            })
2077            .collect();
2078        for (s, from, to, color) in &pieces {
2079            let mut path = vec![PathSegment::Move(at(middle, *from))];
2080            arc(&mut path, center, middle, *from, *to);
2081            plot.layers.push(stroke(path, width - 2.0, *color));
2082            plot.sectors.push(Sector {
2083                series: *s,
2084                index: 0,
2085                center,
2086                inner: hole,
2087                outer: radius,
2088                from: *from,
2089                to: *to,
2090                color: *color,
2091            });
2092            let mid = (*from + *to) / 2.0;
2093            plot.marks.push(Mark { series: *s, index: 0, at: at(middle, mid), color: *color });
2094        }
2095        center_text(style, center, plot);
2096        return;
2097    }
2098
2099    let count = first.values.len().max(1);
2100    let max = first.values.iter().copied().filter(|v| v.is_finite()).fold(0.0_f64, f64::max);
2101    if max <= 0.0 && !style.tracks {
2102        return;
2103    }
2104    let max = max.max(f64::EPSILON);
2105    let width = ((radius - hole) - RING_GAP * (count - 1) as f32) / count as f32;
2106    for (i, value) in first.values.iter().enumerate() {
2107        let middle = radius - (width + RING_GAP) * i as f32 - width / 2.0;
2108        let (inner, outer) = (middle - width / 2.0, middle + width / 2.0);
2109        if style.polar_grid != PolarGrid::None {
2110            plot.layers.push(stroke(circle_path(center, outer + RING_GAP / 2.0), 1.0, style.grid));
2111        }
2112        if style.tracks {
2113            // The track goes all the way around, unless the chart is a gauge.
2114            let around = if gauge { sweep } else { TAU };
2115            let mut path = vec![PathSegment::Move(at(middle, start))];
2116            arc(&mut path, center, middle, start, start + around);
2117            plot.layers.push(stroke(path, width, style.track));
2118        }
2119        let value = if value.is_finite() { value.max(0.0) } else { 0.0 };
2120        let share = (value / max) as f32;
2121        let end = start + share * sweep;
2122        let color = style.item_color(style.color_of(i), i);
2123        // Nothing yet is no ring at all: a round cap alone would look like a value.
2124        if share > 0.0 {
2125            let mut path = vec![PathSegment::Move(at(middle, start))];
2126            arc(&mut path, center, middle, start, end);
2127            plot.layers.push(stroke(path, width, color));
2128        }
2129        plot.sectors.push(Sector {
2130            series: 0,
2131            index: i,
2132            center,
2133            inner,
2134            outer,
2135            from: start,
2136            to: start + sweep,
2137            color,
2138        });
2139        plot.marks.push(Mark { series: 0, index: i, at: at(middle, end), color });
2140        if style.category_labels.is_some() || style.value_labels.is_some() {
2141            let name = data.categories.get(i).map(|c| (style.tick_format)(c)).unwrap_or_default();
2142            let content = if style.category_labels.is_some() { name } else { (style.format)(value) };
2143            // Before its start, where a ring shorter than a whole turn leaves room.
2144            let spot = at(middle, start);
2145            let wide = content.chars().count() as f32 * style.size * CHAR_WIDTH + 8.0;
2146            plot.labels.push(text(
2147                content,
2148                Rect::new(spot.x - wide - width / 2.0 - 4.0, spot.y - width / 2.0, wide, width),
2149                LabelAlign::Right,
2150                style.size,
2151                style.muted,
2152            ));
2153        }
2154    }
2155    center_text(style, center, plot);
2156}
2157
2158#[cfg(test)]
2159mod tests {
2160    use super::*;
2161    use crate::chart::SeriesData;
2162
2163    const AREA: Rect = Rect::new(0.0, 0.0, 400.0, 300.0);
2164    const CLEAR: Color = Color::TRANSPARENT;
2165
2166    fn data(series: &[(&str, &[f64])]) -> ChartData {
2167        ChartData {
2168            categories: (0..series[0].1.len()).map(|i| format!("C{i}")).collect(),
2169            positions: Vec::new(),
2170            series: series
2171                .iter()
2172                .map(|(name, values)| SeriesData {
2173                    name: (*name).to_owned(),
2174                    color: CLEAR,
2175                    values: values.to_vec(),
2176                })
2177                .collect(),
2178        }
2179    }
2180
2181    fn rects_of(plot: &Plot) -> usize {
2182        plot.layers.iter().filter(|l| matches!(l, Layer::Shape(s) if s.fill.is_some())).count()
2183    }
2184
2185    #[test]
2186    fn measured_labels_set_the_room_they_take() {
2187        let data = data(&[("A", &[1000.0, 2000.0])]);
2188        let style = PlotStyle { value_axis: true, ..PlotStyle::new(ChartKind::Bar) };
2189        let area = |width: f32| draw_measured(&data, AREA, &style, &|_, _| width).area.x();
2190        assert!(area(80.0) > area(10.0) + 60.0);
2191    }
2192
2193    #[test]
2194    fn a_measured_label_that_would_stick_out_stays_in() {
2195        let mut data = data(&[("A", &[1.0, 2.0])]);
2196        data.categories = vec!["A long name for a point".into(), "Another long one".into()];
2197        let style = PlotStyle::new(ChartKind::Line);
2198        let plot = draw_measured(&data, AREA, &style, &|t, s| t.len() as f32 * s * 0.5);
2199        assert_eq!(plot.labels[0].align, LabelAlign::Left);
2200        assert_eq!(plot.labels[0].rect.x(), AREA.x());
2201        assert_eq!(plot.labels[1].align, LabelAlign::Right);
2202        assert_eq!(plot.labels[1].rect.max_x(), AREA.max_x());
2203    }
2204
2205    #[test]
2206    fn the_ends_can_line_up_with_the_ends_of_a_line() {
2207        let data = data(&[("A", &[1.0, 2.0, 3.0])]);
2208        let style = PlotStyle { ends_aligned: true, ..PlotStyle::new(ChartKind::Line) };
2209        let plot = draw_measured(&data, AREA, &style, &|t, s| t.len() as f32 * s * 0.5);
2210        let (first, last) = (&plot.labels[0], &plot.labels[2]);
2211        assert_eq!(first.align, LabelAlign::Left);
2212        assert_eq!(first.rect.x(), plot.area.x());
2213        assert_eq!(last.align, LabelAlign::Right);
2214        assert!((last.rect.max_x() - plot.area.max_x()).abs() < 0.01);
2215        assert_eq!(plot.labels[1].align, LabelAlign::Center);
2216    }
2217
2218    #[test]
2219    fn with_its_ends_lined_up_the_last_category_shows_even_if_it_was_to_be_skipped() {
2220        let values = [1.0; 40];
2221        let data = data(&[("A", &values)]);
2222        let style = PlotStyle { ends_aligned: true, ..PlotStyle::new(ChartKind::Line) };
2223        let plot = draw_measured(&data, AREA, &style, &|t, s| t.len() as f32 * s * 0.5);
2224        assert_eq!(plot.labels.last().map(|l| l.text.as_str()), Some("C39"));
2225        assert!(plot.labels.len() < 40);
2226        for pair in plot.labels.windows(2) {
2227            let (a, b) = (&pair[0], &pair[1]);
2228            assert!(a.text != b.text);
2229        }
2230    }
2231
2232    #[test]
2233    fn a_fixed_domain_is_the_axis_and_cuts_off_what_is_past_it() {
2234        let lines = data(&[("A", &[10.0, 500.0, 30.0])]);
2235        let style =
2236            PlotStyle { domain: Some((13.0, 87.0)), value_axis: true, ..PlotStyle::new(ChartKind::Line) };
2237        let plot = draw(&lines, AREA, &style);
2238        assert!(plot.layers.iter().any(|l| matches!(l, Layer::Clipped { .. })));
2239        assert_eq!(plot.marks.len(), 1, "only the value inside has a place");
2240        let ticks: Vec<&str> = plot
2241            .labels
2242            .iter()
2243            .map(|l| l.text.as_str())
2244            .filter(|t| t.starts_with(['2', '4', '6', '8']))
2245            .collect();
2246        assert_eq!(ticks, ["20", "40", "60", "80"], "round numbers, as far as they fit");
2247
2248        let bars = data(&[("A", &[5.0, 50.0, 500.0])]);
2249        let style = PlotStyle { domain: Some((20.0, 100.0)), ..PlotStyle::new(ChartKind::Bar) };
2250        let plot = draw(&bars, AREA, &style);
2251        assert_eq!(plot.bars.len(), 2, "the bar short of the axis is gone");
2252        assert_eq!(plot.bars[1].radii, [0.0; 4], "cut off at both ends, it has no round corners");
2253        let tall = plot.bars[1].rect;
2254        assert!((tall.y() - plot.area.y()).abs() < 0.01 && (tall.max_y() - plot.area.max_y()).abs() < 0.01);
2255    }
2256
2257    #[test]
2258    fn with_no_domain_nothing_is_clipped() {
2259        let lines = data(&[("A", &[10.0, 500.0, 30.0])]);
2260        let plot = draw(&lines, AREA, &PlotStyle::new(ChartKind::Line));
2261        assert!(!plot.layers.iter().any(|l| matches!(l, Layer::Clipped { .. })));
2262        assert_eq!(plot.marks.len(), 3);
2263    }
2264
2265    #[test]
2266    fn a_line_of_reference_stretches_the_scale_unless_the_domain_is_fixed() {
2267        let values = data(&[("A", &[10.0, 20.0])]);
2268        let reference = Reference::new(100.0, Some("Target".into()), None);
2269        let style = PlotStyle {
2270            rules_apart: true,
2271            references: vec![reference.clone()],
2272            ..PlotStyle::new(ChartKind::Line)
2273        };
2274        let plot = draw(&values, AREA, &style);
2275        assert!(plot.top_rules.len() > 10, "in dashes");
2276        assert!(plot.labels.iter().any(|l| l.text == "Target"));
2277
2278        let fixed = PlotStyle { domain: Some((0.0, 50.0)), references: vec![reference], ..style };
2279        let plot = draw(&values, AREA, &fixed);
2280        assert!(plot.top_rules.is_empty(), "past the end of a fixed axis");
2281        assert!(!plot.labels.iter().any(|l| l.text == "Target"));
2282    }
2283
2284    #[test]
2285    fn a_dashed_grid_has_gaps_and_keeps_to_the_area() {
2286        let data = data(&[("A", &[1.0, 2.0])]);
2287        let solid = PlotStyle { rules_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2288        let solid_count = draw(&data, AREA, &solid).rules.len();
2289        let dashed = PlotStyle { rules_apart: true, grid_dashed: true, ..PlotStyle::new(ChartKind::Bar) };
2290        let plot = draw(&data, AREA, &dashed);
2291        assert!(plot.rules.len() > solid_count * 10);
2292        assert!(plot.rules.iter().all(|r| r.rect.max_x() <= plot.area.max_x() + 0.01));
2293    }
2294
2295    #[test]
2296    fn the_axis_can_have_more_steps_and_the_grid_its_own_columns() {
2297        let data = data(&[("A", &[10.0, 100.0])]);
2298        let rules = |style: PlotStyle| {
2299            let style = PlotStyle { rules_apart: true, ..style };
2300            draw(&data, AREA, &style).rules
2301        };
2302        let few = rules(PlotStyle::new(ChartKind::Line));
2303        let many = rules(PlotStyle { value_ticks: 10, ..PlotStyle::new(ChartKind::Line) });
2304        assert!(many.len() > few.len(), "{} {}", many.len(), few.len());
2305        let columns = rules(PlotStyle { grid_columns: Some(5), ..PlotStyle::new(ChartKind::Line) });
2306        let upright = columns.iter().filter(|r| r.rect.height() > r.rect.width()).count();
2307        assert_eq!(upright, 6, "the ends of five columns");
2308    }
2309
2310    #[test]
2311    fn gridlines_stay_in_the_layers_unless_asked_otherwise() {
2312        let data = data(&[("A", &[1.0, 2.0, 3.0])]);
2313        let page = draw(&data, AREA, &PlotStyle::new(ChartKind::Bar));
2314        assert!(page.rules.is_empty());
2315        let screen = PlotStyle { rules_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2316        let screen = draw(&data, AREA, &screen);
2317        assert!(!screen.rules.is_empty());
2318        assert_eq!(rects_of(&page), rects_of(&screen) + screen.rules.len(), "the same lines, elsewhere");
2319        assert_eq!(screen.bars.len(), 3);
2320    }
2321
2322    fn line_length_on_rule(rules: &[Rule], y: f32, from: f32, to: f32) -> f32 {
2323        rules
2324            .iter()
2325            .filter(|r| r.rect.center().y == y)
2326            .map(|r| (r.rect.max_x().min(to) - r.rect.x().max(from)).max(0.0))
2327            .sum()
2328    }
2329
2330    #[test]
2331    fn bars_apart_leave_the_layers_and_the_lines_behind_them() {
2332        let data = data(&[("A", &[10.0, 20.0])]);
2333        let page = PlotStyle { rules_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2334        let whole = draw(&data, AREA, &page);
2335        assert_eq!(whole.layers.len(), 2, "on a page bars are shapes");
2336        let screen = PlotStyle { bars_apart: true, bar_radius: 0.0, ..page };
2337        let plot = draw(&data, AREA, &screen);
2338        assert!(plot.layers.is_empty());
2339        assert_eq!(plot.bars.len(), 2);
2340        let bar = plot.bars[1].rect;
2341        for rule in plot.rules.iter().filter(|r| r.rect.center().y > bar.y()) {
2342            let (y, a, b) = (rule.rect.center().y, bar.x() + 0.01, bar.max_x() - 0.01);
2343            if y < bar.max_y() {
2344                assert_eq!(line_length_on_rule(&plot.rules, y, a, b), 0.0, "nothing behind the bar");
2345            }
2346        }
2347        assert!(plot.rules.len() > whole.rules.len(), "they go on past the bar");
2348    }
2349
2350    #[test]
2351    fn a_line_near_a_round_corner_follows_it() {
2352        assert_eq!(corner_inset(0.0, 1.0), 0.0);
2353        assert_eq!(corner_inset(4.0, 4.0), 0.0);
2354        assert!((corner_inset(4.0, 0.0) - 4.0).abs() < 0.001);
2355        let bar = Bar {
2356            series: 0,
2357            index: 0,
2358            rect: Rect::new(10.0, 10.0, 20.0, 40.0),
2359            color: CLEAR,
2360            radii: [4.0, 4.0, 0.0, 0.0],
2361            gradient: None,
2362        };
2363        let mut out = Vec::new();
2364        line_around_bar(Rect::new(0.0, 10.5, 40.0, 1.0), &bar, &mut out);
2365        assert!(out[0].max_x() > 11.0, "it reaches the curve: {out:?}");
2366        let mut out = Vec::new();
2367        line_around_bar(Rect::new(0.0, 47.5, 40.0, 1.0), &bar, &mut out);
2368        assert_eq!(out[0].max_x(), 10.0, "a square corner has nothing to follow");
2369    }
2370
2371    #[test]
2372    fn a_line_with_more_points_than_columns_keeps_only_the_extremes() {
2373        let values: Vec<f64> =
2374            (0..20_000).map(|i| if i == 7_777 { 1000.0 } else { f64::from(i % 50) }).collect();
2375        let many = data(&[("A", &values)]);
2376        let plot = draw(&many, AREA, &PlotStyle { rules_apart: true, ..PlotStyle::new(ChartKind::Line) });
2377        let Some(Layer::Shape(shape)) = plot.layers.first() else { panic!("a line") };
2378        assert!(shape.path.len() <= AREA.width() as usize * 2 + 4, "{}", shape.path.len());
2379        assert!(shape.path.iter().all(|s| !matches!(s, PathSegment::Cubic(..))), "straight lines");
2380        let top = shape
2381            .path
2382            .iter()
2383            .filter_map(|s| match s {
2384                PathSegment::Line(p) | PathSegment::Move(p) => Some(p.y),
2385                _ => None,
2386            })
2387            .fold(f32::INFINITY, f32::min);
2388        let peak = plot.marks[7_777].at.y;
2389        assert!((top - peak).abs() < 0.01, "the one peak survives: {top} {peak}");
2390        assert_eq!(plot.marks.len(), 20_000, "and every value still has its place");
2391    }
2392
2393    #[test]
2394    fn categories_closer_than_a_pixel_do_not_pile_up_on_the_axis() {
2395        let values = vec![1.0; 5000];
2396        let many = data(&[("A", &values)]);
2397        let plot =
2398            draw_measured(&many, AREA, &PlotStyle::new(ChartKind::Line), &|t, s| t.len() as f32 * s * 0.5);
2399        assert!(plot.labels.len() < 20, "{}", plot.labels.len());
2400    }
2401
2402    #[test]
2403    fn a_line_that_fits_keeps_its_curve() {
2404        let plot = draw(
2405            &data(&[("A", &[1.0, 3.0, 2.0])]),
2406            AREA,
2407            &PlotStyle { rules_apart: true, ..PlotStyle::new(ChartKind::Line) },
2408        );
2409        let Some(Layer::Shape(shape)) = plot.layers.first() else { panic!("a line") };
2410        assert!(shape.path.iter().any(|s| matches!(s, PathSegment::Cubic(..))));
2411    }
2412
2413    fn bars_of(style: PlotStyle, series: &[(&str, &[f64])]) -> Plot {
2414        draw(&data(series), AREA, &style)
2415    }
2416
2417    #[test]
2418    fn a_tower_rounds_whatever_is_on_its_ends() {
2419        let style = PlotStyle {
2420            stack: Stack::Normal,
2421            bars_apart: true,
2422            bar_radius: 4.0,
2423            ..PlotStyle::new(ChartKind::Bar)
2424        };
2425        let plot = bars_of(style, &[("a", &[5.0, 5.0]), ("b", &[5.0, 5.0]), ("c", &[5.0, 0.0])]);
2426        let of =
2427            |s: usize, i: usize| plot.bars.iter().find(|b| b.series == s && b.index == i).map(|b| b.radii);
2428        assert_eq!(of(2, 0), Some([4.0, 4.0, 0.0, 0.0]), "the top of the first tower");
2429        assert_eq!(of(1, 0), Some([0.0; 4]), "the middle");
2430        assert_eq!(of(1, 1), Some([4.0, 4.0, 0.0, 0.0]), "with nothing above it, the second is the top");
2431        assert_eq!(of(2, 1), None, "and the empty one is not drawn");
2432        assert_eq!(of(0, 1), Some([0.0, 0.0, 4.0, 4.0]), "the bottom");
2433    }
2434
2435    #[test]
2436    fn a_tower_under_zero_rounds_its_lower_end() {
2437        let style = PlotStyle {
2438            stack: Stack::Normal,
2439            bars_apart: true,
2440            bar_radius: 4.0,
2441            ..PlotStyle::new(ChartKind::Bar)
2442        };
2443        let plot = bars_of(style, &[("a", &[-5.0]), ("b", &[-5.0])]);
2444        assert_eq!(plot.bars[1].radii, [0.0, 0.0, 4.0, 4.0], "the tip is the lowest");
2445        assert_eq!(plot.bars[0].radii, [4.0, 4.0, 0.0, 0.0]);
2446    }
2447
2448    #[test]
2449    fn a_short_bar_reaches_the_length_asked_for_from_its_base() {
2450        let style = PlotStyle { min_bar: 6.0, bars_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2451        let plot = bars_of(style, &[("a", &[0.001, 100.0, 0.0])]);
2452        let (tiny, tall, none) = (plot.bars[0].rect, plot.bars[1].rect, plot.bars[2].rect);
2453        assert!((tiny.height() - 6.0).abs() < 0.01);
2454        assert!((tiny.max_y() - tall.max_y()).abs() < 0.01, "both stand on the same line");
2455        assert!(none.height() <= 0.5, "nothing is not made to look like something");
2456    }
2457
2458    #[test]
2459    fn bars_can_hang_from_the_top_or_come_from_the_right() {
2460        let hanging = PlotStyle { reversed: true, bars_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2461        let plot = bars_of(hanging, &[("a", &[10.0, 20.0])]);
2462        let (short, long) = (plot.bars[0].rect, plot.bars[1].rect);
2463        assert!((short.y() - long.y()).abs() < 0.01, "from the same line");
2464        assert!(long.max_y() > short.max_y());
2465        assert!(plot.labels.iter().all(|l| l.rect.y() < plot.area.y()), "with the categories over them");
2466
2467        let from_right = PlotStyle {
2468            reversed: true,
2469            horizontal: true,
2470            bars_apart: true,
2471            ..PlotStyle::new(ChartKind::Bar)
2472        };
2473        let plot = bars_of(from_right, &[("a", &[10.0, 20.0])]);
2474        let (short, long) = (plot.bars[0].rect, plot.bars[1].rect);
2475        assert!((short.max_x() - long.max_x()).abs() < 0.01, "from the same line");
2476        assert!(long.x() < short.x());
2477        assert!(
2478            plot.labels.iter().all(|l| l.rect.x() > plot.area.max_x()),
2479            "with the categories beside them"
2480        );
2481    }
2482
2483    #[test]
2484    fn a_label_on_the_line_of_zero_keeps_off_the_bar() {
2485        let style = PlotStyle { labels_at_zero: true, ..PlotStyle::new(ChartKind::Bar) };
2486        let plot = bars_of(PlotStyle { bars_apart: true, ..style }, &[("a", &[10.0, -10.0])]);
2487        let name = |n: &str| plot.labels.iter().find(|l| l.text == n).map(|l| l.rect);
2488        let (up, down) = (name("C0").expect("a label"), name("C1").expect("a label"));
2489        let zero = plot.bars[0].rect.max_y();
2490        assert!(up.y() >= zero, "under the line, as its bar goes up");
2491        assert!(down.max_y() <= zero, "over the line, as its bar goes down");
2492    }
2493
2494    #[test]
2495    fn a_gradient_goes_from_the_tip_to_the_base_either_way() {
2496        let style = PlotStyle { gradient: true, bars_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2497        let plot = bars_of(style, &[("a", &[10.0, -10.0])]);
2498        let up = plot.bars[0].gradient.expect("a gradient");
2499        let down = plot.bars[1].gradient.expect("a gradient");
2500        assert!(up.start.y < up.end.y, "strong at the top");
2501        assert!(down.start.y > down.end.y, "strong at the bottom");
2502        assert!(up.to.a < up.from.a);
2503        let plain = PlotStyle { bars_apart: true, ..PlotStyle::new(ChartKind::Bar) };
2504        assert_eq!(bars_of(plain, &[("a", &[10.0])]).bars[0].gradient, None);
2505    }
2506
2507    fn small(kind: ChartKind, data: &ChartData) -> Plot {
2508        let format = |value: f64| value.to_string();
2509        let options = ChartOptions {
2510            kind,
2511            title: "",
2512            legend: false,
2513            size: 12.0,
2514            ink: Color::BLACK,
2515            muted: Color::BLACK,
2516            grid: Color::BLACK,
2517            format: &format,
2518        };
2519        draw_small(data, AREA, &options, &ChartStyle::default())
2520    }
2521
2522    #[test]
2523    fn a_sparkline_says_where_its_points_are_and_draws_the_same() {
2524        let values = data(&[("a", &[1.0, f64::NAN, 3.0, 2.0])]);
2525        let plot = small(ChartKind::Sparkline, &values);
2526        let indices: Vec<usize> = plot.marks.iter().map(|m| m.index).collect();
2527        assert_eq!(indices, [0, 2, 3], "a value that is missing has no place");
2528        assert_eq!(plot.hit(Point::new(1.0, 150.0)).map(|h| h.index), Some(0));
2529        assert_eq!(plot.hit(Point::new(399.0, 150.0)).map(|h| h.index), Some(3));
2530        assert_eq!(plot.categories(), 4);
2531        let format = |value: f64| value.to_string();
2532        let options = ChartOptions {
2533            kind: ChartKind::Sparkline,
2534            title: "",
2535            legend: false,
2536            size: 12.0,
2537            ink: Color::BLACK,
2538            muted: Color::BLACK,
2539            grid: Color::BLACK,
2540            format: &format,
2541        };
2542        let same = crate::chart::draw_styled(&values, AREA, &options, &ChartStyle::default());
2543        assert_eq!(plot.layers.len(), same.shapes.len());
2544    }
2545
2546    #[test]
2547    fn the_dots_of_a_scatter_chart_own_the_strips_around_them_whatever_their_order() {
2548        let mut values = data(&[("a", &[1.0, 2.0, 3.0])]);
2549        values.positions = vec![30.0, 10.0, 20.0];
2550        let plot = small(ChartKind::Scatter, &values);
2551        assert_eq!(plot.marks.len(), 3);
2552        let left = plot.marks[1].at;
2553        assert!(left.x < plot.marks[2].at.x && plot.marks[2].at.x < plot.marks[0].at.x);
2554        assert_eq!(plot.hit(Point::new(left.x, 10.0)).map(|h| h.index), Some(1));
2555    }
2556
2557    fn prices() -> ChartData {
2558        data(&[
2559            ("open", &[100.0, 110.0, 105.0, f64::NAN]),
2560            ("high", &[112.0, 111.0, 108.0, 105.0]),
2561            ("low", &[98.0, 101.0, 104.0, 105.0]),
2562            ("close", &[110.0, 102.0, 105.0, 105.0]),
2563        ])
2564    }
2565
2566    #[test]
2567    fn a_candle_goes_from_open_to_close_with_wicks_to_its_extremes() {
2568        let style = PlotStyle {
2569            bars_apart: true,
2570            rules_apart: true,
2571            value_axis: true,
2572            ..PlotStyle::new(ChartKind::Candlestick)
2573        };
2574        let plot = draw(&prices(), AREA, &style);
2575        assert!(plot.layers.is_empty());
2576        assert_eq!(plot.candles.len(), 3, "the one with a missing value is left out");
2577        let (up, down) = (plot.candles[0], plot.candles[1]);
2578        assert!(up.rising && !down.rising);
2579        assert_ne!(up.color, down.color);
2580        assert!(up.wicks[0].y() < up.body.y(), "the high is over the body");
2581        assert!(up.wicks[1].max_y() > up.body.max_y(), "the low is under it");
2582        assert!((up.wicks[0].max_y() - up.body.y()).abs() < 0.01);
2583        assert!(plot.candles[2].body.height() >= 1.0, "a price that did not move still shows");
2584        assert!(plot.labels.iter().any(|l| l.text == "100"), "round numbers on the axis");
2585        assert!(!plot.labels.iter().any(|l| l.text == "0"), "and it does not go down to zero");
2586    }
2587
2588    #[test]
2589    fn candles_need_their_four_series_and_do_not_stack() {
2590        let style = PlotStyle::new(ChartKind::Candlestick);
2591        assert!(draw(&data(&[("open", &[1.0])]), AREA, &style).layers.is_empty());
2592        let stacked =
2593            PlotStyle { stack: Stack::Normal, bars_apart: true, ..PlotStyle::new(ChartKind::Candlestick) };
2594        let plain = PlotStyle { bars_apart: true, ..PlotStyle::new(ChartKind::Candlestick) };
2595        assert_eq!(draw(&prices(), AREA, &stacked).candles, draw(&prices(), AREA, &plain).candles);
2596    }
2597
2598    #[test]
2599    fn a_candle_past_a_fixed_axis_is_cut_off_or_gone() {
2600        let style = PlotStyle {
2601            bars_apart: true,
2602            domain: Some((103.0, 109.0)),
2603            ..PlotStyle::new(ChartKind::Candlestick)
2604        };
2605        let plot = draw(&prices(), AREA, &style);
2606        assert_eq!(plot.candles.len(), 3);
2607        assert!(plot.candles[0].wicks[0].y() >= plot.area.y() - 0.01);
2608        let gone = PlotStyle {
2609            bars_apart: true,
2610            domain: Some((200.0, 300.0)),
2611            ..PlotStyle::new(ChartKind::Candlestick)
2612        };
2613        assert!(draw(&prices(), AREA, &gone).candles.is_empty());
2614    }
2615
2616    #[test]
2617    fn a_page_gets_candles_as_shapes_through_the_chart_module() {
2618        let format = |value: f64| value.to_string();
2619        let options = ChartOptions {
2620            kind: ChartKind::Candlestick,
2621            title: "",
2622            legend: false,
2623            size: 12.0,
2624            ink: Color::BLACK,
2625            muted: Color::BLACK,
2626            grid: Color::BLACK,
2627            format: &format,
2628        };
2629        let drawing = crate::chart::draw(&prices(), AREA, &options);
2630        assert!(drawing.shapes.len() > 9, "{}", drawing.shapes.len());
2631        assert!(drawing.labels.iter().any(|l| l.text == "100"));
2632    }
2633
2634    #[test]
2635    fn a_natural_curve_goes_through_every_point() {
2636        let points =
2637            [Point::new(0.0, 0.0), Point::new(10.0, 8.0), Point::new(20.0, 3.0), Point::new(30.0, 9.0)];
2638        let path = curve_through(&points, Curve::Natural);
2639        assert_eq!(path.len(), 3);
2640        for (segment, point) in path.iter().zip(&points[1..]) {
2641            let PathSegment::Cubic(_, _, end) = segment else { panic!("a curve") };
2642            assert_eq!(end, point);
2643        }
2644    }
2645
2646    #[test]
2647    fn a_monotone_curve_never_passes_the_points() {
2648        let points =
2649            [Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(20.0, 10.0), Point::new(30.0, 10.0)];
2650        for segment in curve_through(&points, Curve::Monotone) {
2651            let PathSegment::Cubic(a, b, _) = segment else { panic!("a curve") };
2652            assert!((0.0..=10.0).contains(&a.y) && (0.0..=10.0).contains(&b.y), "{a:?} {b:?}");
2653        }
2654    }
2655
2656    #[test]
2657    fn a_step_changes_halfway() {
2658        let points = [Point::new(0.0, 0.0), Point::new(10.0, 8.0)];
2659        let path = curve_through(&points, Curve::Step);
2660        assert_eq!(path[0], PathSegment::Line(Point::new(5.0, 0.0)));
2661        assert_eq!(path[1], PathSegment::Line(Point::new(5.0, 8.0)));
2662    }
2663
2664    #[test]
2665    fn stacked_series_start_where_the_last_one_ended() {
2666        let towers = stacked(&data(&[("a", &[1.0, 2.0]), ("b", &[3.0, 4.0])]), Stack::Normal);
2667        assert_eq!(towers[0], [(0.0, 1.0), (0.0, 2.0)]);
2668        assert_eq!(towers[1], [(1.0, 4.0), (2.0, 6.0)]);
2669    }
2670
2671    #[test]
2672    fn expanded_series_always_add_up_to_the_whole() {
2673        let towers = stacked(&data(&[("a", &[1.0, 30.0]), ("b", &[3.0, 10.0])]), Stack::Expand);
2674        assert!((towers[1][0].1 - 1.0).abs() < 1e-9);
2675        assert!((towers[1][1].1 - 1.0).abs() < 1e-9);
2676        assert!((towers[0][1].1 - 0.75).abs() < 1e-9);
2677    }
2678
2679    #[test]
2680    fn a_bar_below_zero_goes_the_other_way_in_its_own_color() {
2681        let negative = Color::rgba(1.0, 0.0, 0.0, 1.0);
2682        let mut style = PlotStyle::new(ChartKind::Bar);
2683        style.negative = Some(negative);
2684        let plot = draw(&data(&[("a", &[5.0, -5.0])]), AREA, &style);
2685        let up = plot.marks.iter().find(|m| m.index == 0).expect("a mark");
2686        let down = plot.marks.iter().find(|m| m.index == 1).expect("a mark");
2687        assert!(up.at.y < down.at.y);
2688        assert_eq!(down.color, negative);
2689        assert_ne!(up.color, negative);
2690    }
2691
2692    #[test]
2693    fn horizontal_bars_grow_to_the_right() {
2694        let mut style = PlotStyle::new(ChartKind::Bar);
2695        style.horizontal = true;
2696        let plot = draw(&data(&[("a", &[2.0, 8.0])]), AREA, &style);
2697        assert!(plot.marks[1].at.x > plot.marks[0].at.x);
2698        assert!(plot.marks[1].at.y > plot.marks[0].at.y, "the first category is at the top");
2699    }
2700
2701    #[test]
2702    fn side_by_side_bars_do_not_overlap() {
2703        let plot = draw(&data(&[("a", &[5.0]), ("b", &[5.0])]), AREA, &PlotStyle::new(ChartKind::Bar));
2704        assert!(plot.marks[1].at.x - plot.marks[0].at.x > 10.0);
2705    }
2706
2707    #[test]
2708    fn the_pointer_finds_the_category_of_its_strip() {
2709        let plot = draw(&data(&[("a", &[1.0, 2.0, 3.0, 4.0])]), AREA, &PlotStyle::new(ChartKind::Bar));
2710        assert_eq!(plot.hit(Point::new(20.0, 100.0)).map(|h| h.index), Some(0));
2711        assert_eq!(plot.hit(Point::new(380.0, 100.0)).map(|h| h.index), Some(3));
2712        assert_eq!(plot.categories(), 4);
2713    }
2714
2715    #[test]
2716    fn the_pointer_finds_the_slice_it_is_on() {
2717        let plot = draw(&data(&[("a", &[1.0, 1.0])]), AREA, &PlotStyle::new(ChartKind::Doughnut));
2718        let center = AREA.center();
2719        // Clockwise from the top: the first half is on the right.
2720        assert_eq!(plot.hit(Point::new(center.x + 100.0, center.y)).map(|h| h.index), Some(0));
2721        assert_eq!(plot.hit(Point::new(center.x - 100.0, center.y)).map(|h| h.index), Some(1));
2722        assert_eq!(plot.hit(center), None, "the hole is nothing");
2723    }
2724
2725    #[test]
2726    fn the_active_slice_comes_forward() {
2727        let mut style = PlotStyle::new(ChartKind::Pie);
2728        style.active = Some(1);
2729        let plot = draw(&data(&[("a", &[1.0, 1.0])]), AREA, &style);
2730        assert!(plot.sectors[1].outer > plot.sectors[0].outer);
2731    }
2732
2733    fn image_alpha(image: &ravenrs_core::ImageData, x: u32, y: u32) -> u8 {
2734        image.pixels()[((y * image.width() + x) * 4 + 3) as usize]
2735    }
2736
2737    #[test]
2738    fn the_gap_between_slices_is_really_empty() {
2739        let plot = draw(&data(&[("a", &[1.0, 1.0])]), AREA, &PlotStyle::new(ChartKind::Pie));
2740        let image = vector::rasterize_layers(&plot.layers, AREA, 1.0).expect("a picture");
2741        let center = AREA.center();
2742        // The two halves meet along the upright line through the center.
2743        assert_eq!(image_alpha(&image, center.x as u32, (center.y - 60.0) as u32), 0);
2744        assert_eq!(image_alpha(&image, center.x as u32 + 40, (center.y - 60.0) as u32), 255);
2745    }
2746
2747    #[test]
2748    fn a_whole_circle_has_no_gap() {
2749        let plot = draw(&data(&[("a", &[1.0])]), AREA, &PlotStyle::new(ChartKind::Pie));
2750        let image = vector::rasterize_layers(&plot.layers, AREA, 1.0).expect("a picture");
2751        let center = AREA.center();
2752        assert_eq!(image_alpha(&image, center.x as u32, (center.y - 60.0) as u32), 255);
2753    }
2754
2755    #[test]
2756    fn each_slice_can_have_its_own_radius() {
2757        let style = PlotStyle { slice_radii: vec![1.0, 0.5], ..PlotStyle::new(ChartKind::Doughnut) };
2758        let plot = draw(&data(&[("a", &[1.0, 1.0, 1.0])]), AREA, &style);
2759        let (a, b, c) = (plot.sectors[0], plot.sectors[1], plot.sectors[2]);
2760        assert!(b.outer < a.outer);
2761        assert!(b.outer > b.inner, "it keeps going from the hole");
2762        assert_eq!(c.outer, a.outer, "with no number it reaches the edge");
2763    }
2764
2765    #[test]
2766    fn guided_labels_never_cover_each_other_and_stay_in_the_chart() {
2767        // Small slices side by side, which straight lines would pile up.
2768        let values = [40.0, 1.5, 1.5, 1.5, 1.5, 1.5, 40.0, 1.5, 1.5, 1.5, 1.5, 1.5];
2769        let mut many = data(&[("a", &values)]);
2770        many.categories = (0..values.len()).map(|i| format!("Category {i}")).collect();
2771        let style = PlotStyle {
2772            category_labels: Some(LabelAt::Outside),
2773            value_labels: Some(LabelAt::Outside),
2774            label_guides: true,
2775            ..PlotStyle::new(ChartKind::Pie)
2776        };
2777        let measure = |t: &str, s: f32| t.len() as f32 * s * 0.5;
2778        let plot = draw_measured(&many, AREA, &style, &measure);
2779        assert!(plot.labels.len() > 8, "{}", plot.labels.len());
2780        for (i, a) in plot.labels.iter().enumerate() {
2781            assert!(a.rect.y() >= AREA.y() && a.rect.max_y() <= AREA.max_y() + 0.01, "{:?}", a.rect);
2782            assert!(a.rect.x() >= AREA.x() - 0.5 && a.rect.max_x() <= AREA.max_x() + 0.5, "{:?}", a.rect);
2783            for b in &plot.labels[i + 1..] {
2784                assert!(a.rect.inflate(-0.01).intersect(&b.rect).is_empty(), "{} over {}", a.text, b.text);
2785            }
2786        }
2787        assert!(plot.labels.iter().any(|l| l.text.contains("Category 0") && l.text.contains("40")));
2788        let elbows = plot
2789            .layers
2790            .iter()
2791            .filter(|l| matches!(l, Layer::Shape(s) if s.stroke.is_some() && s.path.len() == 3))
2792            .count();
2793        assert_eq!(elbows, plot.labels.len(), "a line to each, with a turn");
2794    }
2795
2796    #[test]
2797    fn rings_get_one_per_series() {
2798        let mut style = PlotStyle::new(ChartKind::Pie);
2799        style.concentric = true;
2800        let plot = draw(&data(&[("a", &[1.0, 1.0]), ("b", &[1.0, 2.0])]), AREA, &style);
2801        assert_eq!(plot.sectors.len(), 4);
2802        assert!(plot.sectors[2].outer < plot.sectors[0].outer);
2803    }
2804
2805    #[test]
2806    fn a_radar_has_a_vertex_per_category() {
2807        let plot = draw(&data(&[("a", &[1.0, 2.0, 3.0, 4.0, 5.0])]), AREA, &PlotStyle::new(ChartKind::Radar));
2808        assert_eq!(plot.marks.len(), 5);
2809        let top = plot.marks[0].at;
2810        assert!((top.x - AREA.center().x).abs() < 0.01, "the first spoke points up");
2811        assert!(top.y < AREA.center().y);
2812    }
2813
2814    #[test]
2815    fn a_radar_needs_three_spokes() {
2816        let plot = draw(&data(&[("a", &[1.0, 2.0])]), AREA, &PlotStyle::new(ChartKind::Radar));
2817        assert!(plot.layers.is_empty());
2818    }
2819
2820    #[test]
2821    fn the_longest_ring_covers_the_whole_sweep() {
2822        let mut style = PlotStyle::new(ChartKind::RadialBar);
2823        style.sweep = 180.0;
2824        let plot = draw(&data(&[("a", &[10.0, 5.0])]), AREA, &style);
2825        assert_eq!(plot.marks.len(), 2);
2826        let (long, short) = (plot.marks[0], plot.marks[1]);
2827        // A half circle clockwise from the top ends at the bottom.
2828        assert!(long.at.y > short.at.y);
2829    }
2830
2831    #[test]
2832    fn labels_go_where_they_were_asked() {
2833        let mut style = PlotStyle::new(ChartKind::Bar);
2834        style.value_labels = Some(LabelAt::Outside);
2835        let plot = draw(&data(&[("a", &[5.0])]), AREA, &style);
2836        let value = plot.labels.iter().find(|l| l.text == "5").expect("the value");
2837        assert!(value.rect.center().y < plot.marks[0].at.y);
2838    }
2839
2840    #[test]
2841    fn no_data_draws_nothing() {
2842        for kind in [ChartKind::Bar, ChartKind::Line, ChartKind::Pie, ChartKind::Radar, ChartKind::RadialBar]
2843        {
2844            let empty = ChartData::default();
2845            assert_eq!(rects_of(&draw(&empty, AREA, &PlotStyle::new(kind))), 0, "{kind:?}");
2846        }
2847    }
2848
2849    #[test]
2850    fn a_kind_only_this_module_knows_still_comes_out_as_shapes() {
2851        let values = data(&[("a", &[1.0, 2.0, 3.0])]);
2852        let options = crate::chart::ChartOptions {
2853            kind: ChartKind::Radar,
2854            title: "",
2855            legend: false,
2856            size: 12.0,
2857            ink: Color::BLACK,
2858            muted: Color::BLACK,
2859            grid: Color::BLACK,
2860            format: &|value| value.to_string(),
2861        };
2862        assert!(!crate::chart::draw(&values, AREA, &options).shapes.is_empty());
2863    }
2864
2865    #[test]
2866    fn what_draws_in_is_told_from_what_stays_put() {
2867        // Bars grow out of the zero line, which a negative value puts above the bottom.
2868        let style = PlotStyle {
2869            bars_apart: true,
2870            value_labels: Some(LabelAt::Outside),
2871            ..PlotStyle::new(ChartKind::Bar)
2872        };
2873        let plot = draw(&data(&[("a", &[-2.0, 4.0])]), AREA, &style);
2874        let zero = plot.baseline.expect("a zero line");
2875        assert!(zero > plot.area.y() && zero < plot.area.max_y());
2876        assert!(
2877            plot.bars.iter().all(|b| (b.rect.y() - zero).abs() < 0.5 || (b.rect.max_y() - zero).abs() < 0.5)
2878        );
2879        // Their values are on the data and the axis labels are not.
2880        assert_eq!(plot.data_labels.len(), 2);
2881        assert!(plot.data_labels.iter().all(|i| ["-2", "4"].contains(&plot.labels[*i].text.as_str())));
2882        assert!(plot.labels.len() > plot.data_labels.len(), "the axis keeps its labels");
2883
2884        // A radar's rings and spokes are its backdrop.
2885        let mut style = PlotStyle::new(ChartKind::Radar);
2886        style.rings = 3;
2887        let plot = draw(&data(&[("a", &[1.0, 2.0, 3.0, 4.0])]), AREA, &style);
2888        assert_eq!(plot.backdrop, 3 + 4);
2889        assert!(plot.layers.len() > plot.backdrop, "the series come after it");
2890
2891        // A pie's guides only point at the slices.
2892        let mut style = PlotStyle::new(ChartKind::Pie);
2893        style.value_labels = Some(LabelAt::Outside);
2894        style.label_guides = true;
2895        let plot = draw(&data(&[("a", &[1.0, 2.0, 3.0])]), AREA, &style);
2896        assert_eq!(plot.notes.len(), 3);
2897        assert_eq!(plot.data_labels.len(), 3);
2898        assert_eq!(plot.layers.len() - plot.notes.len(), plot.sectors.len());
2899    }
2900}