Skip to main content

ravenrs_widgets/
chart.rs

1//! Charts and barcodes. The layout comes from `ravenrs-graphics`, the same as in
2//! reports; here its shapes become a picture and its labels become text. Both are
3//! kept between frames, so a rebuild that changes nothing uploads nothing.
4
5use std::cell::{Cell, RefCell};
6use std::collections::HashMap;
7use std::fmt;
8use std::rc::Rc;
9
10use ravenrs_animation::{CubicBezier, Easing, Transition};
11use ravenrs_core::{
12    Brush, Color, CursorIcon, Effect, ImageData, ImageQuad, Insets, Key, PathElement, PathFill, Point, Quad,
13    Rect, Role, Size,
14};
15use ravenrs_graphics::barcode::{self, Pattern};
16use ravenrs_graphics::chart::{
17    self, ChartData, ChartDrawing, ChartLabel, ChartOptions, ChartStyle, LabelAlign, SeriesData,
18};
19use ravenrs_graphics::plot::{self, Plot, PlotStyle};
20use ravenrs_graphics::vector::{self, PathSegment, Shape};
21use ravenrs_layout::{Align, Axis, Justify, LayoutStyle};
22use ravenrs_reactive::{Binding, State};
23use ravenrs_style::{Semantic, TextStyle, Theme, Weight};
24use ravenrs_text::{TextAttrs, TextLayout};
25
26pub use ravenrs_graphics::barcode::Symbology;
27pub use ravenrs_graphics::chart::ChartKind;
28pub use ravenrs_graphics::plot::{
29    Curve as ChartCurve, Grid as ChartGrid, LabelAt as ChartLabelAt, PolarGrid as ChartPolarGrid,
30    Stack as ChartStack,
31};
32
33use crate::context::{BuildCx, NodeId, Paint, PaintCx, Placement, QuadPaint, Semantics};
34use crate::motion::{self, Motion};
35use crate::{AnyView, Card, IconName, View, util};
36
37/// How a chart with [`Chart::animate_changes`] goes from its old data to the new: quick to
38/// start, settling softly, in the curve the data draws in with.
39const CHANGE: Transition = Transition::tween(0.75, Easing::Bezier(CubicBezier::new(0.25, 1.0, 0.5, 1.0)));
40/// The widest a bar gets, so a few bars in a wide chart still read as bars.
41const MAX_BAR_WIDTH: f32 = 44.0;
42const LABEL_LINE_HEIGHT: f32 = 1.3;
43/// How many values a screen reader hears before the rest is left out.
44const SPOKEN_VALUES: usize = 12;
45const BARCODE_PADDING: f32 = 8.0;
46
47const LEGEND_SWATCH: f32 = 8.0;
48const LEGEND_ICON: f32 = 12.0;
49const TOOLTIP_MIN_WIDTH: f32 = 128.0;
50const TOOLTIP_INDICATOR: f32 = 10.0;
51const ACTIVE_DOT: f32 = 5.0;
52/// How far the ring around an active dot reaches past it.
53const ACTIVE_HALO: f32 = 5.0;
54/// How solid that ring is.
55const HALO_ALPHA: f32 = 0.22;
56/// How much of its strength a chart gives up around the bar or slice in focus.
57const FOCUS_DIM: f32 = 0.45;
58/// How far a slice in focus comes forward. It stays under the gap a chart keeps
59/// from its title and legend, so a pie needs no smaller radius to make room for it.
60const SLICE_LIFT: f32 = 6.0;
61/// How much a slice's way forward it takes it to be solid.
62const LIFT_SOLID_FROM: f32 = 4.0;
63/// How solid the line through the points of the category in focus is.
64const CURSOR_ALPHA: f32 = 0.55;
65const DOT_ICON: f32 = 18.0;
66const CANDLE_OUTLINE: f32 = 1.5;
67
68/// Lays a drawing out in a rectangle, at a number of pixels per logical pixel, and
69/// with a way to know how wide its text is.
70type DrawingFn = Rc<dyn Fn(Rect, f32, plot::Measure<'_>) -> Plot>;
71pub(crate) type ValueFormat = Rc<dyn Fn(f64) -> String>;
72/// Writes the panel of a tooltip, given the index and the name of a category.
73type TooltipContent = Rc<dyn Fn(usize, &str) -> AnyView>;
74/// The color of a value in a tooltip, given its series and the value.
75type ValueColor = Rc<dyn Fn(usize, f64) -> Option<Semantic>>;
76type TickFormat = Rc<dyn Fn(&str) -> String>;
77/// Which tab is chosen, and the name and the total of each.
78type ChartTabs = (Binding<usize>, Vec<(String, String)>);
79
80/// Bytes of pictures kept after their chart is gone: about ten charts filling a laptop's
81/// screen at 2x.
82const PICTURES_KEPT_BYTES: usize = 128 << 20;
83
84/// A picture that is not on screen anymore.
85struct KeptPicture {
86    layers: Vec<vector::Layer>,
87    size: (u32, u32),
88    image: Option<ImageData>,
89    bytes: usize,
90}
91
92thread_local! {
93    /// Drawing a chart on the CPU is what costs most in opening a screen, and a screen
94    /// that opens again shows the same chart. The most recent one comes last.
95    static PICTURES: RefCell<Vec<KeptPicture>> = const { RefCell::new(Vec::new()) };
96}
97
98/// The part of `area` these layers draw on, on whole pixels: drawing and sending the
99/// rest of the frame, empty, is what a chart's picture spends the most on.
100fn drawn_part(layers: &[vector::Layer], area: Rect, scale: f32) -> Option<Rect> {
101    let (mut x0, mut y0, mut x1, mut y1) =
102        (f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY);
103    for bounds in layers.iter().filter_map(vector::Layer::bounds) {
104        x0 = x0.min(bounds.x());
105        y0 = y0.min(bounds.y());
106        x1 = x1.max(bounds.max_x());
107        y1 = y1.max(bounds.max_y());
108    }
109    let (left, top) = ((x0.max(area.x()) * scale).floor(), (y0.max(area.y()) * scale).floor());
110    let (right, bottom) = ((x1.min(area.max_x()) * scale).ceil(), (y1.min(area.max_y()) * scale).ceil());
111    (right > left && bottom > top)
112        .then(|| Rect::new(left / scale, top / scale, (right - left) / scale, (bottom - top) / scale))
113}
114
115/// The picture of these layers, drawn now only if none is kept.
116fn picture_of(layers: &[vector::Layer], area: Rect, scale: f32) -> Option<ImageData> {
117    let size = ((area.width() * scale).round() as u32, (area.height() * scale).round() as u32);
118    PICTURES.with_borrow_mut(|kept| {
119        if let Some(found) = kept.iter().position(|p| p.size == size && p.layers == layers) {
120            let picture = kept.remove(found);
121            let image = picture.image.clone();
122            kept.push(picture);
123            return image;
124        }
125        // A little under its width, for rounding not to make the picture a pixel wider.
126        let whole = Rect::new(area.x(), area.y(), area.width() - 0.01 / scale, area.height() - 0.01 / scale);
127        let image = vector::rasterize_layers(layers, whole, scale);
128        let bytes = size.0 as usize * size.1 as usize * 4;
129        kept.push(KeptPicture { layers: layers.to_vec(), size, image: image.clone(), bytes });
130        let mut total: usize = kept.iter().map(|p| p.bytes).sum();
131        while total > PICTURES_KEPT_BYTES && kept.len() > 1 {
132            total -= kept.remove(0).bytes;
133        }
134        image
135    })
136}
137
138/// What a drawing looked like when it was last painted, relative to its frame.
139#[derive(Default)]
140struct Picture {
141    source: Option<DrawingFn>,
142    size: Size,
143    scale: f32,
144    drawing: Plot,
145    image: Option<(Rect, ImageData)>,
146    labels: Vec<(TextLayout, Point, Color)>,
147    dot_icons: Vec<(TextLayout, Point, Color)>,
148    /// How wide each label is, as a rebuild asks again for the same ones.
149    widths: HashMap<u32, HashMap<String, f32>>,
150    /// The picture split in its backdrop, its data and its notes, drawn the first time
151    /// a draw-in asks for it.
152    parts: Option<Parts>,
153}
154
155/// The pictures of a chart's backdrop, of its data and of its notes, apart, for its
156/// data to draw in over a backdrop that stays put.
157#[derive(Default)]
158struct Parts {
159    backdrop: Option<(Rect, ImageData)>,
160    data: Option<(Rect, ImageData)>,
161    notes: Option<(Rect, ImageData)>,
162}
163
164/// Measured widths a chart keeps, more than even a long series has categories: one
165/// forgotten while the same labels are still in use would be measured again each frame.
166const WIDTHS_KEPT: usize = 1 << 15;
167
168#[cfg_attr(feature = "live", ravenrs_live::expose)]
169impl Picture {
170    /// Brings the picture up to date, redoing only what changed.
171    fn refresh(&mut self, source: &DrawingFn, dot_icon: Option<IconName>, size: Size, cx: &mut PaintCx<'_>) {
172        let same_source = self.source.as_ref().is_some_and(|known| Rc::ptr_eq(known, source));
173        let same_pixels = self.size == size && self.scale == cx.scale;
174        if same_source && same_pixels {
175            return;
176        }
177        let area = Rect::new(0.0, 0.0, size.width, size.height);
178        if self.scale != cx.scale || self.widths.values().map(HashMap::len).sum::<usize>() > WIDTHS_KEPT {
179            self.widths.clear();
180        }
181        let drawing = {
182            let (text, widths) = (RefCell::new(&mut *cx.text), RefCell::new(&mut self.widths));
183            let measure = |content: &str, size: f32| {
184                let mut widths = widths.borrow_mut();
185                let of_size = widths.entry(size.to_bits()).or_default();
186                if let Some(width) = of_size.get(content) {
187                    return *width;
188                }
189                let attrs = label_attrs(size, false);
190                let width = text.borrow_mut().layout(content, &attrs).measure(None).width;
191                of_size.insert(content.to_owned(), width);
192                width
193            };
194            source(area, cx.scale, &measure)
195        };
196        if !same_pixels || drawing.layers != self.drawing.layers {
197            self.parts = None;
198            self.image = drawn_part(&drawing.layers, area, cx.scale)
199                .and_then(|part| picture_of(&drawing.layers, part, cx.scale).map(|image| (part, image)));
200        }
201        if drawing.labels != self.drawing.labels {
202            self.labels = drawing.labels.iter().map(|label| typeset(label, cx)).collect();
203        }
204        if drawing.marks != self.drawing.marks {
205            self.dot_icons = dot_icon.map_or_else(Vec::new, |icon| {
206                let mut glyph = [0u8; 4];
207                let glyph = icon.glyph().encode_utf8(&mut glyph);
208                let half = DOT_ICON / 2.0;
209                drawing
210                    .marks
211                    .iter()
212                    .map(|mark| {
213                        let mut layout = cx.text.layout(glyph, &util::icon_attrs(DOT_ICON));
214                        layout.measure(None);
215                        (layout, Point::new(mark.at.x - half, mark.at.y - half), mark.color)
216                    })
217                    .collect()
218            });
219        }
220        self.source = Some(source.clone());
221        self.size = size;
222        self.scale = cx.scale;
223        self.drawing = drawing;
224    }
225
226    /// Its backdrop, its data and its notes, each a picture of its own. They are only
227    /// drawn while the data draws in, and kept with the other pictures.
228    fn parts(&mut self) -> &Parts {
229        if self.parts.is_none() {
230            let area = Rect::new(0.0, 0.0, self.size.width, self.size.height);
231            let scale = self.scale;
232            let image = |layers: Vec<vector::Layer>| {
233                drawn_part(&layers, area, scale)
234                    .and_then(|part| picture_of(&layers, part, scale).map(|image| (part, image)))
235            };
236            let plot = &self.drawing;
237            let backdrop = plot.backdrop.min(plot.layers.len());
238            let (mut data, mut notes) = (Vec::new(), Vec::new());
239            for (i, layer) in plot.layers.iter().enumerate().skip(backdrop) {
240                if plot.notes.contains(&i) { notes.push(layer.clone()) } else { data.push(layer.clone()) }
241            }
242            self.parts = Some(Parts {
243                backdrop: image(plot.layers[..backdrop].to_vec()),
244                data: image(data),
245                notes: image(notes),
246            });
247        }
248        self.parts.get_or_insert_with(Parts::default)
249    }
250}
251
252/// How a chart's data draws in the first time it is painted. Its axes, its grid and
253/// their labels are there from the first frame.
254#[derive(Clone, Copy, Debug, PartialEq)]
255enum DrawIn {
256    /// From the left across, as a pen would: lines, areas, dots and candles.
257    Reveal,
258    /// Out of the zero line, all bars together, a negative one to its own side.
259    Grow { horizontal: bool },
260    /// Clockwise from the first slice, with the labels coming in at the end.
261    Sweep { separator: f32 },
262    /// Out of the center, over rings and spokes that stay put.
263    Spread,
264}
265
266/// How far a chart's data has drawn in, from 0 to 1, and how it does.
267#[derive(Clone, Copy, Debug)]
268struct Appear {
269    how: DrawIn,
270    progress: f32,
271}
272
273/// How far into a sweep the labels and their guides start to come in, once the
274/// slices are mostly there.
275const LABELS_COME_IN: f32 = 0.7;
276/// How far before the first point a reveal starts, so a dot there shows whole as
277/// soon as the reveal reaches it.
278const REVEAL_BLEED: f32 = ACTIVE_DOT;
279
280/// What a chart's key to draw in again comes down to.
281pub(crate) fn appear_key_of(key: impl std::hash::Hash) -> u64 {
282    use std::hash::{BuildHasher, BuildHasherDefault, DefaultHasher};
283    BuildHasherDefault::<DefaultHasher>::default().hash_one(key)
284}
285
286/// Where a bar is once it has grown `t` of the way out of `baseline`.
287fn grown(rect: Rect, baseline: f32, horizontal: bool, t: f32) -> Rect {
288    let toward = |at: f32| baseline + (at - baseline) * t;
289    if horizontal {
290        let (a, b) = (toward(rect.x()), toward(rect.max_x()));
291        Rect::new(a, rect.y(), b - a, rect.height())
292    } else {
293        let (a, b) = (toward(rect.y()), toward(rect.max_y()));
294        Rect::new(rect.x(), a, rect.width(), b - a)
295    }
296}
297
298/// The angles of a slice `t` of the way into a sweep that starts at `start`.
299fn swept(from: f32, to: f32, start: f32, t: f32) -> (f32, f32) {
300    (start + (from - start) * t, start + (to - start) * t)
301}
302
303/// The part of a frame a reveal shows, `t` of the way from the plot's left edge to
304/// the frame's right one. It bleeds a dot's width past both, so dots show whole.
305pub(crate) fn revealed(frame: Rect, area: Rect, t: f32) -> Rect {
306    let left = frame.x() + area.x() - REVEAL_BLEED;
307    let right = left + (frame.max_x() + REVEAL_BLEED - left) * t;
308    let top = frame.y() - REVEAL_BLEED;
309    let x = frame.x() - REVEAL_BLEED;
310    Rect::new(x, top, (right - x).max(0.0), frame.height() + REVEAL_BLEED * 2.0)
311}
312
313/// One of the pictures, at `opacity`, scaled `by` around a point when it grows.
314fn paint_image(
315    part: Option<&(Rect, ImageData)>,
316    frame: Rect,
317    opacity: f32,
318    by: Option<(Point, f32)>,
319    cx: &mut PaintCx<'_>,
320) {
321    let Some((part, image)) = part else { return };
322    if opacity <= 0.0 {
323        return;
324    }
325    let part = match by {
326        Some((center, k)) => Rect::new(
327            center.x + (part.x() - center.x) * k,
328            center.y + (part.y() - center.y) * k,
329            part.width() * k,
330            part.height() * k,
331        ),
332        None => *part,
333    };
334    if part.width() <= 0.0 || part.height() <= 0.0 {
335        return;
336    }
337    let at = Rect::new(frame.x() + part.x(), frame.y() + part.y(), part.width(), part.height());
338    cx.scene.push_image(ImageQuad { opacity, ..ImageQuad::new(at, image.clone()) });
339}
340
341/// The labels, or only those on the data (`Some(true)`) or the others (`Some(false)`),
342/// at a share of their strength.
343fn paint_labels(picture: &Picture, frame: Rect, on_data: Option<bool>, strength: f32, cx: &mut PaintCx<'_>) {
344    if strength <= 0.0 {
345        return;
346    }
347    for (i, (layout, at, color)) in picture.labels.iter().enumerate() {
348        if on_data.is_some_and(|wanted| picture.drawing.data_labels.binary_search(&i).is_ok() != wanted) {
349            continue;
350        }
351        let origin = Point::new(frame.x() + at.x, frame.y() + at.y);
352        layout.paint(origin, color.with_alpha(color.a * strength), cx.scene);
353    }
354}
355
356fn paint_dot_icons(picture: &Picture, frame: Rect, strength: f32, cx: &mut PaintCx<'_>) {
357    if strength <= 0.0 {
358        return;
359    }
360    for (layout, at, color) in &picture.dot_icons {
361        let origin = Point::new(frame.x() + at.x, frame.y() + at.y);
362        layout.paint(origin, color.with_alpha(color.a * strength), cx.scene);
363    }
364}
365
366/// The slices of a round chart `t` of the way into their sweep, each ring from its
367/// own first slice.
368fn paint_swept_slices(plot: &Plot, frame: Rect, t: f32, separator: f32, cx: &mut PaintCx<'_>) {
369    for sector in &plot.sectors {
370        let start = plot
371            .sectors
372            .iter()
373            .filter(|s| s.series == sector.series)
374            .map(|s| s.from)
375            .fold(f32::INFINITY, f32::min);
376        let (from, to) = swept(sector.from, sector.to, start, t);
377        if to - from <= f32::EPSILON {
378            continue;
379        }
380        let path = plot::gapped_slice(sector.center, sector.inner, sector.outer, from, to, separator);
381        cx.scene.push_path(PathFill {
382            transform: [1.0, 0.0, 0.0, 1.0, frame.x(), frame.y()],
383            ..PathFill::solid(path_elements(&path), sector.color)
384        });
385    }
386}
387
388/// The data while it draws in, with its labels and notes, but not the labels of the axes.
389fn paint_drawing_in(appear: Appear, picture: &mut Picture, frame: Rect, cx: &mut PaintCx<'_>) {
390    let t = appear.progress.clamp(0.0, 1.0);
391    // The grid is there from the first frame. Only bars, as they grow, cut it anew.
392    if !matches!(appear.how, DrawIn::Grow { .. }) {
393        paint_rules(&picture.drawing.rules, frame, cx);
394    }
395    match appear.how {
396        DrawIn::Reveal => {
397            cx.scene.push_clip(revealed(frame, picture.drawing.area, t));
398            paint_bars(None, &picture.drawing.bars, frame, cx);
399            paint_candles(None, &picture.drawing, frame, cx);
400            paint_image(picture.image.as_ref(), frame, 1.0, None, cx);
401            paint_labels(picture, frame, Some(true), 1.0, cx);
402            paint_dot_icons(picture, frame, 1.0, cx);
403            cx.scene.pop_clip();
404        }
405        DrawIn::Grow { horizontal } => {
406            let plot = &picture.drawing;
407            let baseline =
408                plot.baseline.unwrap_or(if horizontal { plot.area.x() } else { plot.area.max_y() });
409            let bars: Vec<plot::Bar> = plot
410                .bars
411                .iter()
412                .map(|bar| {
413                    let mut bar = *bar;
414                    bar.rect = grown(bar.rect, baseline, horizontal, t);
415                    bar
416                })
417                .collect();
418            // The grid stops at the bars as they are now, and not where they will end.
419            let rules = if plot.uncut_rules.is_empty() {
420                plot.rules.clone()
421            } else {
422                plot::rules_around_bars(&plot.uncut_rules, &bars)
423            };
424            paint_rules(&rules, frame, cx);
425            paint_bars(None, &bars, frame, cx);
426            paint_image(picture.image.as_ref(), frame, t, None, cx);
427            paint_labels(picture, frame, Some(true), t, cx);
428            paint_dot_icons(picture, frame, t, cx);
429        }
430        DrawIn::Sweep { separator } => {
431            paint_swept_slices(&picture.drawing, frame, t, separator, cx);
432            let late = ((t - LABELS_COME_IN) / (1.0 - LABELS_COME_IN)).clamp(0.0, 1.0);
433            let notes = picture.parts().notes.clone();
434            paint_image(notes.as_ref(), frame, late, None, cx);
435            paint_labels(picture, frame, Some(true), late, cx);
436        }
437        DrawIn::Spread => {
438            let parts = picture.parts();
439            let (backdrop, data, notes) = (parts.backdrop.clone(), parts.data.clone(), parts.notes.clone());
440            let plot = &picture.drawing;
441            let center = plot.sectors.first().map_or_else(|| plot.area.center(), |s| s.center);
442            paint_image(backdrop.as_ref(), frame, 1.0, None, cx);
443            paint_image(data.as_ref(), frame, 1.0, Some((center, t)), cx);
444            paint_image(notes.as_ref(), frame, t, None, cx);
445            paint_labels(picture, frame, Some(true), t, cx);
446            paint_dot_icons(picture, frame, t, cx);
447        }
448    }
449}
450
451pub(crate) fn label_attrs(size: f32, bold: bool) -> TextAttrs {
452    TextAttrs {
453        size,
454        line_height: size * LABEL_LINE_HEIGHT,
455        weight: if bold { Weight::SEMIBOLD.0 } else { Weight::REGULAR.0 },
456        letter_spacing: 0.0,
457        family: None,
458    }
459}
460
461pub(crate) fn typeset(label: &ChartLabel, cx: &mut PaintCx<'_>) -> (TextLayout, Point, Color) {
462    let attrs = label_attrs(label.size, label.bold);
463    let mut layout = cx.text.layout(&label.text, &attrs);
464    let measured = layout.measure(None);
465    let x = match label.align {
466        LabelAlign::Center => label.rect.x() + (label.rect.width() - measured.width) / 2.0,
467        LabelAlign::Right => label.rect.max_x() - measured.width,
468        _ => label.rect.x(),
469    };
470    let y = label.rect.y() + (label.rect.height() - measured.height) / 2.0;
471    (layout, Point::new(x, y), label.color)
472}
473
474/// What stands out over the picture while a category is in focus, or was until a
475/// moment ago: all of it fades, so it is drawn for as long as `focus` is above zero.
476#[derive(Clone)]
477struct Highlight {
478    /// The category, between two of them while it glides from one to the other.
479    at: f32,
480    /// From nothing to in full.
481    focus: f32,
482    /// How much the picture fades at full focus.
483    dim: f32,
484    /// How far forward each category's slice is, from 0 to 1 (a spring may pass 1).
485    lifts: Vec<f32>,
486    /// The gap between slices, which a slice drawn over them has to leave alone.
487    separator: f32,
488    /// A strip behind the bars.
489    strip: bool,
490    /// A line through the points.
491    line: bool,
492    /// A dot on each value.
493    dots: bool,
494    strip_color: Color,
495    line_color: Color,
496    ring_color: Color,
497}
498
499fn mix(a: f32, b: f32, t: f32) -> f32 {
500    a + (b - a) * t
501}
502
503#[cfg_attr(feature = "live", ravenrs_live::expose)]
504impl Highlight {
505    /// The two categories it is between, and how far from the first to the second.
506    fn between(&self) -> (usize, usize, f32) {
507        let at = self.at.max(0.0);
508        (at.floor() as usize, at.ceil() as usize, at.fract())
509    }
510
511    /// The opacity of the picture.
512    fn opacity(&self) -> f32 {
513        1.0 - self.dim * self.focus
514    }
515}
516
517/// Keeps a thin line on whole pixels, so that it stays sharp and of one width. Its
518/// place on the page is only known here, and it may fall anywhere.
519fn snapped(rect: Rect, scale: f32) -> Rect {
520    let snap = |at: f32, length: f32| {
521        let thick = (length * scale).round().max(1.0);
522        ((at * scale).round() / scale, thick / scale)
523    };
524    if rect.width() < rect.height() {
525        let (x, width) = snap(rect.x(), rect.width());
526        Rect::new(x, rect.y(), width, rect.height())
527    } else {
528        let (y, height) = snap(rect.y(), rect.height());
529        Rect::new(rect.x(), y, rect.width(), height)
530    }
531}
532
533fn paint_rules(rules: &[plot::Rule], frame: Rect, cx: &mut PaintCx<'_>) {
534    for rule in rules {
535        let rect = Rect::new(
536            frame.x() + rule.rect.x(),
537            frame.y() + rule.rect.y(),
538            rule.rect.width(),
539            rule.rect.height(),
540        );
541        cx.scene.push_quad(Quad::new(snapped(rect, cx.scale), rule.color));
542    }
543}
544
545fn paint_top_rules(plot: &Plot, frame: Rect, cx: &mut PaintCx<'_>) {
546    for rule in &plot.top_rules {
547        let rect = Rect::new(
548            frame.x() + rule.rect.x(),
549            frame.y() + rule.rect.y(),
550            rule.rect.width(),
551            rule.rect.height(),
552        );
553        cx.scene.push_quad(Quad::new(snapped(rect, cx.scale), rule.color));
554    }
555}
556
557fn paint_strip(highlight: &Highlight, plot: &Plot, frame: Rect, cx: &mut PaintCx<'_>) {
558    if !highlight.strip {
559        return;
560    }
561    let (from, to, t) = highlight.between();
562    let spot = |index: usize| plot.spots.iter().find(|s| s.index == index);
563    let (Some(from), Some(to)) = (spot(from), spot(to)) else { return };
564    let (a, b) = (from.band, to.band);
565    let band = Rect::new(
566        mix(a.x(), b.x(), t),
567        mix(a.y(), b.y(), t),
568        mix(a.width(), b.width(), t),
569        mix(a.height(), b.height(), t),
570    );
571    let area = plot.area;
572    let along_x = (band.width() - frame.width()).abs() > 0.5;
573    let strip = if along_x {
574        Rect::new(band.x(), area.y(), band.width(), area.height())
575    } else {
576        Rect::new(area.x(), band.y(), area.width(), band.height())
577    };
578    let strip = Rect::new(frame.x() + strip.x(), frame.y() + strip.y(), strip.width(), strip.height());
579    let color = highlight.strip_color;
580    cx.scene.push_quad(Quad::new(strip, color.with_alpha(color.a * highlight.focus)));
581}
582
583/// The bars. Around the focus they fade, less the closer they are to it, so focus
584/// hands over from one category to the next as it passes.
585fn paint_bars(highlight: Option<&Highlight>, bars: &[plot::Bar], frame: Rect, cx: &mut PaintCx<'_>) {
586    for bar in bars {
587        let shape = bar.rect;
588        // One that has not started to grow yet.
589        if shape.width() <= 0.0 || shape.height() <= 0.0 {
590            continue;
591        }
592        let faded = highlight.map_or(0.0, |highlight| {
593            let distance = (bar.index as f32 - highlight.at).abs().min(1.0);
594            highlight.dim * highlight.focus * distance
595        });
596        let rect = Rect::new(frame.x() + shape.x(), frame.y() + shape.y(), shape.width(), shape.height());
597        let strength = 1.0 - faded;
598        let brush: Brush = match bar.gradient {
599            Some(gradient) => ravenrs_core::LinearGradient {
600                from: gradient.from.with_alpha(gradient.from.a * strength),
601                to: gradient.to.with_alpha(gradient.to.a * strength),
602                ..gradient
603            }
604            .into(),
605            None => bar.color.with_alpha(bar.color.a * strength).into(),
606        };
607        let mut quad = Quad::new(rect, brush);
608        quad.corner_radii = bar.radii;
609        cx.scene.push_quad(quad);
610    }
611}
612
613/// The candles. A body that rose is an outline and one that fell is solid, so that
614/// which it did does not rest on the color alone.
615fn paint_candles(highlight: Option<&Highlight>, plot: &Plot, frame: Rect, cx: &mut PaintCx<'_>) {
616    let place =
617        |rect: Rect| Rect::new(frame.x() + rect.x(), frame.y() + rect.y(), rect.width(), rect.height());
618    for candle in &plot.candles {
619        let faded = highlight.map_or(0.0, |highlight| {
620            let distance = (candle.index as f32 - highlight.at).abs().min(1.0);
621            highlight.dim * highlight.focus * distance
622        });
623        let color = candle.color.with_alpha(candle.color.a * (1.0 - faded));
624        for wick in candle.wicks {
625            if wick.height() > 0.0 {
626                cx.scene.push_quad(Quad::new(snapped(place(wick), cx.scale), color));
627            }
628        }
629        let body =
630            Quad::new(place(candle.body), if candle.rising { Color::TRANSPARENT } else { color }).radius(1.0);
631        cx.scene.push_quad(if candle.rising { body.border(CANDLE_OUTLINE, color) } else { body });
632    }
633}
634
635pub(crate) fn path_elements(path: &[PathSegment]) -> Vec<PathElement> {
636    path.iter()
637        .map(|segment| match segment {
638            PathSegment::Move(p) => PathElement::MoveTo(*p),
639            PathSegment::Line(p) => PathElement::LineTo(*p),
640            PathSegment::Cubic(a, b, c) => PathElement::CubicTo(*a, *b, *c),
641            _ => PathElement::Close,
642        })
643        .collect()
644}
645
646/// The slices that came forward, over a picture that faded.
647fn paint_lifted_slices(highlight: &Highlight, plot: &Plot, frame: Rect, cx: &mut PaintCx<'_>) {
648    let outermost = plot.sectors.iter().map(|s| s.inner).fold(0.0, f32::max);
649    for sector in &plot.sectors {
650        let lift = highlight.lifts.get(sector.index).copied().unwrap_or(0.0);
651        if lift <= 0.001 {
652            continue;
653        }
654        // Only the outside ring has anywhere to go.
655        let outer = if sector.inner >= outermost { sector.outer + SLICE_LIFT * lift } else { sector.outer };
656        // The same gap as in the picture keeps the slice the same width as it comes forward.
657        let path = plot::gapped_slice(
658            sector.center,
659            sector.inner,
660            outer,
661            sector.from,
662            sector.to,
663            highlight.separator,
664        );
665        // Solid for most of its way, so that it never looks like a ghost of itself, and
666        // only at the very end does it hand over to the slice in the picture.
667        let color = sector.color.with_alpha(sector.color.a * (lift * LIFT_SOLID_FROM).clamp(0.0, 1.0));
668        cx.scene.push_path(PathFill {
669            transform: [1.0, 0.0, 0.0, 1.0, frame.x(), frame.y()],
670            ..PathFill::solid(path_elements(&path), color)
671        });
672    }
673}
674
675fn paint_highlight(highlight: &Highlight, plot: &Plot, frame: Rect, cx: &mut PaintCx<'_>) {
676    let (from, to, t) = highlight.between();
677    if highlight.line {
678        let spot = |index: usize| plot.spots.iter().find(|s| s.index == index);
679        if let (Some(a), Some(b)) = (spot(from), spot(to)) {
680            let x = frame.x() + mix(a.center.x, b.center.x, t) - 0.5;
681            let line = snapped(Rect::new(x, frame.y() + plot.area.y(), 1.0, plot.area.height()), cx.scale);
682            let color = highlight.line_color;
683            cx.scene.push_quad(Quad::new(line, color.with_alpha(color.a * highlight.focus)));
684        }
685    }
686    if highlight.dots {
687        let focus = highlight.focus;
688        let radius = ACTIVE_DOT * mix(0.6, 1.0, focus);
689        let halo = radius + ACTIVE_HALO * focus;
690        for mark in plot.marks.iter().filter(|m| m.index == from) {
691            let next = plot.marks.iter().find(|m| m.index == to && m.series == mark.series).unwrap_or(mark);
692            let x = frame.x() + mix(mark.at.x, next.at.x, t);
693            let y = frame.y() + mix(mark.at.y, next.at.y, t);
694            let color = if t < 0.5 { mark.color } else { next.color };
695            let round = |r: f32| Rect::new(x - r, y - r, r * 2.0, r * 2.0);
696            cx.scene.push_quad(Quad::new(round(halo), color.with_alpha(HALO_ALPHA * focus)).radius(halo));
697            let ring = highlight.ring_color;
698            cx.scene.push_quad(
699                Quad::new(round(radius), color.with_alpha(color.a * focus))
700                    .radius(radius)
701                    .border(2.0, ring.with_alpha(ring.a * focus)),
702            );
703        }
704    }
705}
706
707/// A box that paints what `drawing` lays out for its frame, around `children`: what
708/// stands out in it now, and how far its data has drawn in.
709fn plot_node(
710    cx: &mut BuildCx<'_>,
711    style: LayoutStyle,
712    picture: Rc<RefCell<Picture>>,
713    drawing: DrawingFn,
714    dot_icon: Option<IconName>,
715    (highlight, appear): (Option<Highlight>, Option<Appear>),
716    children: &[NodeId],
717) -> NodeId {
718    cx.container(
719        style,
720        children,
721        Paint::Rich(Rc::new(move |frame: Rect, paint: &mut PaintCx<'_>| {
722            if frame.is_empty() {
723                return;
724            }
725            let mut picture = picture.borrow_mut();
726            picture.refresh(&drawing, dot_icon, frame.size, paint);
727            if let Some(highlight) = &highlight {
728                paint_strip(highlight, &picture.drawing, frame, paint);
729            }
730            let drawing_in = appear.filter(|appear| appear.progress < 1.0);
731            if let Some(appear) = drawing_in {
732                paint_drawing_in(appear, &mut picture, frame, paint);
733            } else {
734                paint_rules(&picture.drawing.rules, frame, paint);
735                paint_bars(highlight.as_ref(), &picture.drawing.bars, frame, paint);
736                paint_candles(highlight.as_ref(), &picture.drawing, frame, paint);
737                let opacity = highlight.as_ref().map_or(1.0, Highlight::opacity);
738                paint_image(picture.image.as_ref(), frame, opacity, None, paint);
739                if let Some(highlight) = &highlight {
740                    paint_lifted_slices(highlight, &picture.drawing, frame, paint);
741                }
742            }
743            paint_top_rules(&picture.drawing, frame, paint);
744            if let Some(highlight) = &highlight {
745                paint_highlight(highlight, &picture.drawing, frame, paint);
746            }
747            if drawing_in.is_some() {
748                paint_labels(&picture, frame, Some(false), 1.0, paint);
749            } else {
750                paint_labels(&picture, frame, None, 1.0, paint);
751                paint_dot_icons(&picture, frame, 1.0, paint);
752            }
753        })),
754    )
755}
756
757/// A box that paints what `drawing` lays out for its frame.
758fn drawing_node(cx: &mut BuildCx<'_>, style: LayoutStyle, drawing: DrawingFn) -> NodeId {
759    let picture = cx.use_state(|| Rc::new(RefCell::new(Picture::default()))).peek().clone();
760    plot_node(cx, style, picture, drawing, None, (None, None), &[])
761}
762
763/// The colors of a chart in this theme: none of them means anything, unlike status
764/// colors, and past the theme's five come those of the shared palette.
765pub(crate) fn palette(theme: &Theme) -> Vec<Color> {
766    [Semantic::Chart1, Semantic::Chart2, Semantic::Chart3, Semantic::Chart4, Semantic::Chart5]
767        .map(|role| theme.color(role))
768        .into_iter()
769        .chain(chart::PALETTE[5..].iter().copied())
770        .collect()
771}
772
773/// Shows a number on an axis: at most two decimals, none when they are zeros.
774pub(crate) fn plain_number(value: f64) -> String {
775    let text = format!("{value:.2}");
776    text.trim_end_matches('0').trim_end_matches('.').to_owned()
777}
778
779struct Series {
780    name: String,
781    color: Option<Color>,
782    icon: Option<IconName>,
783    values: Vec<f64>,
784}
785
786/// How a row of a [`ChartTooltip`] shows its color.
787#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
788#[non_exhaustive]
789#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
790pub enum ChartIndicator {
791    /// A small square.
792    #[default]
793    Dot,
794    /// A thin bar.
795    Line,
796    /// A thin dashed bar.
797    Dashed,
798    /// No color at all.
799    None,
800}
801
802/// What a [`Chart`] says about the category under the pointer or the arrow keys.
803///
804/// ```
805/// use ravenrs_widgets::{ChartIndicator, ChartTooltip};
806///
807/// let tooltip = ChartTooltip::new()
808///     .indicator(ChartIndicator::Line)
809///     .label_format(|month| format!("Month of {month}"))
810///     .show_total("Total");
811/// ```
812#[derive(Clone, Default)]
813#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
814pub struct ChartTooltip {
815    indicator: ChartIndicator,
816    hide_label: bool,
817    label: Option<String>,
818    label_format: Option<TickFormat>,
819    value_format: Option<ValueFormat>,
820    total: Option<String>,
821    icons: bool,
822    no_cursor: bool,
823    percent: bool,
824    value_color: Option<ValueColor>,
825    no_indicator: Vec<String>,
826    content: Option<TooltipContent>,
827}
828
829impl fmt::Debug for ChartTooltip {
830    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
831        f.debug_struct("ChartTooltip").field("indicator", &self.indicator).finish_non_exhaustive()
832    }
833}
834
835#[cfg_attr(feature = "live", ravenrs_live::expose)]
836impl ChartTooltip {
837    /// A tooltip with the category on top and a square before each series.
838    pub fn new() -> Self {
839        Self::default()
840    }
841
842    /// How each row shows its color.
843    pub fn indicator(mut self, indicator: ChartIndicator) -> Self {
844        self.indicator = indicator;
845        self
846    }
847
848    /// Leaves out the category on top.
849    pub fn hide_label(mut self) -> Self {
850        self.hide_label = true;
851        self
852    }
853
854    /// Writes this on top, in place of the category.
855    pub fn label(mut self, label: impl Into<String>) -> Self {
856        self.label = Some(label.into());
857        self
858    }
859
860    /// Writes the category on top in another way, such as a date in full.
861    pub fn label_format(mut self, format: impl Fn(&str) -> String + 'static) -> Self {
862        self.label_format = Some(Rc::new(format));
863        self
864    }
865
866    /// Writes values in another way than on the axis, such as with their unit.
867    pub fn value_format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
868        self.value_format = Some(Rc::new(format));
869        self
870    }
871
872    /// Adds a last row with the sum of the series, under this name.
873    pub fn show_total(mut self, name: impl Into<String>) -> Self {
874        self.total = Some(name.into());
875        self
876    }
877
878    /// Shows the icons of the series in place of their colors.
879    pub fn icons(mut self) -> Self {
880        self.icons = true;
881        self
882    }
883
884    /// Writes after each value its share: of the circle in a pie or a doughnut, of
885    /// the category in the others.
886    pub fn show_percent(mut self) -> Self {
887        self.percent = true;
888        self
889    }
890
891    /// Colors a value by what it says, such as a loss in red. It is given the index
892    /// of the series and the value; `None` leaves it in the color of the text.
893    pub fn value_color(mut self, color: impl Fn(usize, f64) -> Option<Semantic> + 'static) -> Self {
894        self.value_color = Some(Rc::new(color));
895        self
896    }
897
898    /// The row of this series has no square or bar, for a value that is not one of
899    /// those the chart tells apart, such as a target.
900    pub fn no_indicator_for(mut self, series: impl Into<String>) -> Self {
901        self.no_indicator.push(series.into());
902        self
903    }
904
905    /// Writes the panel with a view of your own, given the index and the name of the
906    /// category. A screen reader still hears the values.
907    pub fn content(mut self, content: impl Fn(usize, &str) -> AnyView + 'static) -> Self {
908        self.content = Some(Rc::new(content));
909        self
910    }
911
912    /// Leaves out the strip or line that marks the category on the chart.
913    pub fn no_cursor(mut self) -> Self {
914        self.no_cursor = true;
915        self
916    }
917}
918
919#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
920impl ChartTooltip {
921    fn color_of_value(&self, series: usize, value: f64, theme: &Theme) -> Option<Color> {
922        self.value_color.as_ref().and_then(|color| color(series, value)).map(|role| theme.color(role))
923    }
924}
925
926/// One row of a tooltip or legend.
927pub(crate) struct KeyRow {
928    pub(crate) name: String,
929    color: Color,
930    icon: Option<IconName>,
931    value: Option<f64>,
932    /// Its share of the whole, already written.
933    share: Option<String>,
934    value_color: Option<Color>,
935    no_indicator: bool,
936}
937
938#[cfg_attr(feature = "live", ravenrs_live::expose)]
939impl KeyRow {
940    pub(crate) fn named(name: String, color: Color, icon: Option<IconName>, value: Option<f64>) -> Self {
941        Self { name, color, icon, value, share: None, value_color: None, no_indicator: false }
942    }
943}
944
945/// A share of a whole, with no decimals to write in the reader's language.
946fn percent_text(part: f64, whole: f64) -> Option<String> {
947    if whole <= 0.0 || !part.is_finite() {
948        return None;
949    }
950    let percent = part.abs() / whole * 100.0;
951    Some(if percent > 0.0 && percent < 1.0 { "<1%".to_owned() } else { format!("{percent:.0}%") })
952}
953
954pub(crate) struct TooltipPanel {
955    pub(crate) title: Option<String>,
956    pub(crate) rows: Vec<KeyRow>,
957    pub(crate) total: Option<(String, f64)>,
958    pub(crate) tooltip: ChartTooltip,
959    pub(crate) format: ValueFormat,
960    pub(crate) index: usize,
961    pub(crate) category: String,
962}
963
964fn indicator_node(cx: &mut BuildCx<'_>, indicator: ChartIndicator, color: Color) -> Option<NodeId> {
965    let solid = |width: f32, height: f32| LayoutStyle {
966        width: Some(width),
967        height: Some(height),
968        ..LayoutStyle::leaf()
969    };
970    let paint = || Paint::Quad(QuadPaint { corner_radius: 2.0, ..QuadPaint::new(color) });
971    Some(match indicator {
972        ChartIndicator::Dot => cx.container(solid(TOOLTIP_INDICATOR, TOOLTIP_INDICATOR), &[], paint()),
973        ChartIndicator::Line => cx.container(solid(4.0, 18.0), &[], paint()),
974        ChartIndicator::Dashed => {
975            let dashes: Vec<NodeId> = (0..3).map(|_| cx.container(solid(2.0, 4.0), &[], paint())).collect();
976            cx.container(
977                LayoutStyle { spacing: 2.0, ..LayoutStyle::stack(Axis::Vertical) },
978                &dashes,
979                Paint::None,
980            )
981        }
982        _ => return None,
983    })
984}
985
986#[cfg_attr(feature = "live", ravenrs_live::expose)]
987impl TooltipPanel {
988    fn row(&self, cx: &mut BuildCx<'_>, row: &KeyRow, strong: bool) -> NodeId {
989        let theme = *cx.theme();
990        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
991        let mut parts = Vec::new();
992        if let (true, Some(icon)) = (self.tooltip.icons, row.icon) {
993            parts.push(util::icon(cx, icon, LEGEND_ICON, muted));
994        } else if !strong
995            && !row.no_indicator
996            && let Some(mark) = indicator_node(cx, self.tooltip.indicator, row.color)
997        {
998            parts.push(mark);
999        }
1000        let weight = strong.then_some(Weight::SEMIBOLD);
1001        parts.push(util::label(cx, &row.name, TextStyle::Caption, weight, if strong { ink } else { muted }));
1002        let gap = cx.container(
1003            LayoutStyle { grow: 1.0, min_width: Some(12.0), ..LayoutStyle::leaf() },
1004            &[],
1005            Paint::None,
1006        );
1007        parts.push(gap);
1008        if let Some(value) = row.value {
1009            let format = self.tooltip.value_format.as_ref().unwrap_or(&self.format);
1010            let tone = row.value_color.unwrap_or(ink);
1011            parts.push(util::label(cx, &format(value), TextStyle::Caption, Some(Weight::SEMIBOLD), tone));
1012        }
1013        if let Some(share) = &row.share {
1014            parts.push(util::label(cx, share, TextStyle::Caption, None, muted));
1015        }
1016        cx.container(
1017            LayoutStyle {
1018                spacing: 6.0,
1019                align: Align::Center,
1020                align_self: Some(Align::Stretch),
1021                ..LayoutStyle::stack(Axis::Horizontal)
1022            },
1023            &parts,
1024            Paint::None,
1025        )
1026    }
1027}
1028
1029impl View for TooltipPanel {
1030    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1031        let theme = *cx.theme();
1032        let mut lines = Vec::new();
1033        if let Some(content) = &self.tooltip.content {
1034            let view = content(self.index, &self.category);
1035            lines.push(cx.child(&*view));
1036        } else {
1037            if let Some(title) = &self.title {
1038                let ink = theme.color(Semantic::Ink);
1039                lines.push(util::label(cx, title, TextStyle::Caption, Some(Weight::SEMIBOLD), ink));
1040            }
1041            for row in &self.rows {
1042                lines.push(self.row(cx, row, false));
1043            }
1044            if let Some((name, total)) = &self.total {
1045                let row = KeyRow::named(name.clone(), Color::TRANSPARENT, None, Some(*total));
1046                lines.push(self.row(cx, &row, true));
1047            }
1048        }
1049        let node = cx.container(
1050            LayoutStyle {
1051                padding: Insets::symmetric(6.0, 10.0),
1052                spacing: 6.0,
1053                min_width: Some(TOOLTIP_MIN_WIDTH),
1054                ..LayoutStyle::stack(Axis::Vertical)
1055            },
1056            &lines,
1057            Paint::Quad(QuadPaint {
1058                corner_radius: theme.radii.md,
1059                border: Some(ravenrs_core::Border { width: 1.0, color: theme.color(Semantic::Border) }),
1060                shadow: Some(theme.elevation.raised),
1061                ..QuadPaint::new(theme.color(Semantic::SurfaceElevated))
1062            }),
1063        );
1064        // The chart says the same to a screen reader.
1065        cx.semantics(node, Semantics::hidden());
1066        // One panel that glides from place to place, and picks up from wherever it is
1067        // when the pointer changes its mind.
1068        let m = theme.motion;
1069        cx.motion(
1070            node,
1071            Motion::new()
1072                .anchored()
1073                .layout(m.gentle)
1074                .enter(Effect::scaled(motion::SURFACE_ENTER_SCALE), m.snappy)
1075                .exit(Effect::scaled(motion::SURFACE_EXIT_SCALE), m.fast),
1076        );
1077        node
1078    }
1079}
1080
1081/// A dashed line across a [`Chart`] at a value that matters, such as a target.
1082///
1083/// ```
1084/// use ravenrs_style::Semantic;
1085/// use ravenrs_widgets::{Chart, ChartKind, ChartReference};
1086///
1087/// let chart = Chart::new(ChartKind::Bar)
1088///     .categories(["Jan", "Feb"])
1089///     .series("Sales", [12.0, 18.0])
1090///     .reference(ChartReference::new(15.0).label("Target").color(Semantic::Danger));
1091/// ```
1092#[derive(Clone, Debug, PartialEq)]
1093#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1094pub struct ChartReference {
1095    value: f64,
1096    label: Option<String>,
1097    color: Option<Semantic>,
1098}
1099
1100#[cfg_attr(feature = "live", ravenrs_live::expose)]
1101impl ChartReference {
1102    /// A line at this value, in the color of secondary text.
1103    pub fn new(value: f64) -> Self {
1104        Self { value, label: None, color: None }
1105    }
1106
1107    /// What it says, at its end.
1108    pub fn label(mut self, label: impl Into<String>) -> Self {
1109        self.label = Some(label.into());
1110        self
1111    }
1112
1113    /// Its color.
1114    pub fn color(mut self, color: Semantic) -> Self {
1115        self.color = Some(color);
1116        self
1117    }
1118}
1119
1120/// Where the bars of a [`Chart`] grow from.
1121#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1122#[non_exhaustive]
1123#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
1124pub enum ChartBarFrom {
1125    /// Up from the bottom.
1126    #[default]
1127    Bottom,
1128    /// Down from the top, with their categories over them.
1129    Top,
1130    /// To the right from the left, as [`Chart::horizontal`] does.
1131    Left,
1132    /// To the left from the right, with their categories beside them.
1133    Right,
1134}
1135
1136/// Where a [`Chart`]'s legend goes.
1137#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1138#[non_exhaustive]
1139#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
1140pub enum ChartLegend {
1141    /// Under the chart.
1142    #[default]
1143    Bottom,
1144    /// Over the chart.
1145    Top,
1146}
1147
1148struct LegendRow {
1149    rows: Vec<KeyRow>,
1150}
1151
1152impl View for LegendRow {
1153    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1154        let theme = *cx.theme();
1155        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
1156        let entries: Vec<NodeId> = self
1157            .rows
1158            .iter()
1159            .map(|row| {
1160                let mark = match row.icon {
1161                    Some(icon) => util::icon(cx, icon, LEGEND_ICON, ink),
1162                    None => cx.container(
1163                        LayoutStyle {
1164                            width: Some(LEGEND_SWATCH),
1165                            height: Some(LEGEND_SWATCH),
1166                            ..LayoutStyle::leaf()
1167                        },
1168                        &[],
1169                        Paint::Quad(QuadPaint { corner_radius: 2.0, ..QuadPaint::new(row.color) }),
1170                    ),
1171                };
1172                let name = util::label(cx, &row.name, TextStyle::Caption, None, muted);
1173                cx.container(
1174                    LayoutStyle {
1175                        spacing: 6.0,
1176                        align: Align::Center,
1177                        ..LayoutStyle::stack(Axis::Horizontal)
1178                    },
1179                    &[mark, name],
1180                    Paint::None,
1181                )
1182            })
1183            .collect();
1184        cx.container(
1185            LayoutStyle {
1186                spacing: 16.0,
1187                wrap: true,
1188                justify: Justify::Center,
1189                align: Align::Center,
1190                align_self: Some(Align::Stretch),
1191                ..LayoutStyle::stack(Axis::Horizontal)
1192            },
1193            &entries,
1194            Paint::None,
1195        )
1196    }
1197}
1198
1199/// A chart of any [`ChartKind`]. Its shapes are drawn into a picture at the screen's
1200/// density, and its labels are real text. Over the picture it says what each value
1201/// is, under the pointer or with the arrow keys. [`ChartKind::Scatter`] and the
1202/// sparklines stay as plain pictures, which take no focus, until given a
1203/// [`tooltip`](Self::tooltip).
1204///
1205/// Available with the `charts` feature, on by default.
1206///
1207/// ```
1208/// use ravenrs_widgets::{Chart, ChartKind};
1209///
1210/// let sales = Chart::new(ChartKind::Line)
1211///     .title("Sales")
1212///     .categories(["Jan", "Feb", "Mar"])
1213///     .series("2025", [12.0, 18.0, 15.0])
1214///     .series("2026", [14.0, 21.0, 26.0])
1215///     .height(240.0);
1216/// ```
1217#[derive(Clone)]
1218#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
1219pub struct Chart {
1220    kind: ChartKind,
1221    categories: Vec<String>,
1222    positions: Vec<f64>,
1223    series: Vec<Rc<Series>>,
1224    title: String,
1225    alt: Option<String>,
1226    legend: bool,
1227    format: ValueFormat,
1228    width: Option<f32>,
1229    height: f32,
1230    look: Look,
1231    legend_at: ChartLegend,
1232    tooltip: Option<ChartTooltip>,
1233    /// The small kinds only say anything if asked to.
1234    tooltip_asked: bool,
1235    dot_icon: Option<IconName>,
1236    item_colors: Vec<Semantic>,
1237    vary_colors: bool,
1238    negative: Option<Semantic>,
1239    hover_focus: bool,
1240    hover_motion: bool,
1241    appear: bool,
1242    animate_changes: bool,
1243    appear_key: u64,
1244}
1245
1246/// What [`PlotStyle`] takes as it is. The theme and the data fill in the rest.
1247#[derive(Clone)]
1248struct Look {
1249    curve: ChartCurve,
1250    stack: ChartStack,
1251    gradient: bool,
1252    fill_opacity: Option<f32>,
1253    horizontal: bool,
1254    reversed: bool,
1255    min_bar: Option<f32>,
1256    labels_at_zero: bool,
1257    bar_radius: Option<f32>,
1258    active: Option<usize>,
1259    dots: bool,
1260    value_labels: Option<ChartLabelAt>,
1261    category_labels: Option<ChartLabelAt>,
1262    tick_format: Option<TickFormat>,
1263    grid: ChartGrid,
1264    grid_dashed: bool,
1265    domain: Option<(f64, f64)>,
1266    references: Vec<ChartReference>,
1267    ends_aligned: bool,
1268    grid_columns: Option<usize>,
1269    value_ticks: Option<usize>,
1270    category_axis: bool,
1271    value_axis: bool,
1272    tight_scale: bool,
1273    no_separator: bool,
1274    slice_radii: Vec<f32>,
1275    label_guides: bool,
1276    hole: Option<f32>,
1277    concentric: bool,
1278    polar_grid: Option<ChartPolarGrid>,
1279    polar_fill: bool,
1280    spokes: bool,
1281    rings: usize,
1282    lines_only: bool,
1283    radius_axis: bool,
1284    start_angle: f32,
1285    sweep: f32,
1286    tracks: bool,
1287    center: Option<(String, String)>,
1288}
1289
1290impl Default for Look {
1291    fn default() -> Self {
1292        Self {
1293            curve: ChartCurve::Natural,
1294            stack: ChartStack::None,
1295            gradient: false,
1296            fill_opacity: None,
1297            horizontal: false,
1298            reversed: false,
1299            min_bar: None,
1300            labels_at_zero: false,
1301            bar_radius: None,
1302            active: None,
1303            dots: false,
1304            value_labels: None,
1305            category_labels: None,
1306            tick_format: None,
1307            grid: ChartGrid::Horizontal,
1308            grid_dashed: false,
1309            domain: None,
1310            references: Vec::new(),
1311            ends_aligned: false,
1312            grid_columns: None,
1313            value_ticks: None,
1314            category_axis: true,
1315            value_axis: false,
1316            tight_scale: false,
1317            no_separator: false,
1318            slice_radii: Vec::new(),
1319            label_guides: false,
1320            hole: None,
1321            concentric: false,
1322            polar_grid: None,
1323            polar_fill: false,
1324            spokes: true,
1325            rings: 5,
1326            lines_only: false,
1327            radius_axis: false,
1328            start_angle: 0.0,
1329            sweep: 360.0,
1330            tracks: false,
1331            center: None,
1332        }
1333    }
1334}
1335
1336impl fmt::Debug for Chart {
1337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1338        f.debug_struct("Chart")
1339            .field("kind", &self.kind)
1340            .field("title", &self.title)
1341            .field("categories", &self.categories.len())
1342            .field("series", &self.series.len())
1343            .finish_non_exhaustive()
1344    }
1345}
1346
1347#[cfg_attr(feature = "live", ravenrs_live::expose)]
1348impl Chart {
1349    /// An empty chart, 220 logical pixels tall, as wide as its parent allows.
1350    pub fn new(kind: ChartKind) -> Self {
1351        Self {
1352            kind,
1353            categories: Vec::new(),
1354            positions: Vec::new(),
1355            series: Vec::new(),
1356            title: String::new(),
1357            alt: None,
1358            legend: true,
1359            format: Rc::new(plain_number),
1360            width: None,
1361            height: 220.0,
1362            look: Look::default(),
1363            legend_at: ChartLegend::Bottom,
1364            tooltip: Some(ChartTooltip::new()),
1365            tooltip_asked: false,
1366            dot_icon: None,
1367            item_colors: Vec::new(),
1368            vary_colors: false,
1369            negative: None,
1370            hover_focus: true,
1371            hover_motion: true,
1372            appear: true,
1373            animate_changes: false,
1374            appear_key: 0,
1375        }
1376    }
1377
1378    /// What each point is about: the labels along the bottom, or of each slice.
1379    pub fn categories(mut self, categories: impl IntoIterator<Item = impl Into<String>>) -> Self {
1380        self.categories = categories.into_iter().map(Into::into).collect();
1381        self
1382    }
1383
1384    /// Where each point sits on the horizontal axis of a [`ChartKind::Scatter`].
1385    pub fn positions(mut self, positions: impl IntoIterator<Item = f64>) -> Self {
1386        self.positions = positions.into_iter().collect();
1387        self
1388    }
1389
1390    /// Adds a series in the theme's next color, with one value per category. Pie
1391    /// and doughnut charts use the first series.
1392    pub fn series(mut self, name: impl Into<String>, values: impl IntoIterator<Item = f64>) -> Self {
1393        self.series.push(Rc::new(Series {
1394            name: name.into(),
1395            color: None,
1396            icon: None,
1397            values: values.into_iter().collect(),
1398        }));
1399        self
1400    }
1401
1402    /// The candles of a [`ChartKind::Candlestick`], one per category, each with its open,
1403    /// high, low and close, in that order. Rising candles are outlines and falling ones
1404    /// are solid, in the theme's colors for success and danger.
1405    ///
1406    /// ```
1407    /// use ravenrs_widgets::{Chart, ChartKind};
1408    ///
1409    /// let prices = Chart::new(ChartKind::Candlestick)
1410    ///     .categories(["Mon", "Tue"])
1411    ///     .candles([[100.0, 112.0, 98.0, 110.0], [110.0, 111.0, 101.0, 102.0]]);
1412    /// ```
1413    pub fn candles(mut self, candles: impl IntoIterator<Item = [f64; 4]>) -> Self {
1414        let candles: Vec<[f64; 4]> = candles.into_iter().collect();
1415        // Their names come from the language of the screen, when they are shown.
1416        self.series = (0..4)
1417            .map(|at| {
1418                Rc::new(Series {
1419                    name: String::new(),
1420                    color: None,
1421                    icon: None,
1422                    values: candles.iter().map(|candle| candle[at]).collect(),
1423                })
1424            })
1425            .collect();
1426        self
1427    }
1428
1429    /// Adds a series in a color of your choosing.
1430    pub fn series_color(
1431        mut self,
1432        name: impl Into<String>,
1433        color: Color,
1434        values: impl IntoIterator<Item = f64>,
1435    ) -> Self {
1436        self.series.push(Rc::new(Series {
1437            name: name.into(),
1438            color: Some(color),
1439            icon: None,
1440            values: values.into_iter().collect(),
1441        }));
1442        self
1443    }
1444
1445    /// A title above the chart.
1446    pub fn title(mut self, title: impl Into<String>) -> Self {
1447        self.title = title.into();
1448        self
1449    }
1450
1451    /// Hides the legend, which otherwise appears when there is something to tell apart.
1452    pub fn hide_legend(mut self) -> Self {
1453        self.legend = false;
1454        self
1455    }
1456
1457    /// How numbers appear on the axis (at most two decimals by default).
1458    pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
1459        self.format = Rc::new(format);
1460        self
1461    }
1462
1463    /// Height in logical pixels (default 220).
1464    pub fn height(mut self, height: f32) -> Self {
1465        self.height = height;
1466        self
1467    }
1468
1469    /// Fixed size in logical pixels, for a sparkline in a row.
1470    pub fn size(mut self, width: f32, height: f32) -> Self {
1471        self.width = Some(width);
1472        self.height = height;
1473        self
1474    }
1475
1476    /// Describes the chart for screen readers, in place of its title. Its values
1477    /// are read after it.
1478    pub fn alt(mut self, text: impl Into<String>) -> Self {
1479        self.alt = Some(text.into());
1480        self
1481    }
1482
1483    /// Gives the series called `name` an icon, for the legend and the tooltip.
1484    pub fn series_icon(mut self, name: &str, icon: IconName) -> Self {
1485        for series in &mut self.series {
1486            if series.name == name {
1487                let mut changed = Series {
1488                    name: series.name.clone(),
1489                    color: series.color,
1490                    icon: Some(icon),
1491                    values: Vec::new(),
1492                };
1493                changed.values.clone_from(&series.values);
1494                *series = Rc::new(changed);
1495            }
1496        }
1497        self
1498    }
1499
1500    /// How lines go from point to point (natural by default).
1501    pub fn curve(mut self, curve: ChartCurve) -> Self {
1502        self.look.curve = curve;
1503        self
1504    }
1505
1506    /// How series share a category: side by side by default, or stacked.
1507    pub fn stack(mut self, stack: ChartStack) -> Self {
1508        self.look.stack = stack;
1509        self
1510    }
1511
1512    /// Fills areas from strong at the top to faint at the bottom, and bars from strong
1513    /// at the tip to fainter at the base.
1514    pub fn gradient(mut self) -> Self {
1515        self.look.gradient = true;
1516        self
1517    }
1518
1519    /// How solid an area or a radar polygon is, from 0 to 1.
1520    pub fn fill_opacity(mut self, opacity: f32) -> Self {
1521        self.look.fill_opacity = Some(opacity);
1522        self
1523    }
1524
1525    /// Bars lie down, with their categories on the left.
1526    pub fn horizontal(mut self) -> Self {
1527        self.look.horizontal = true;
1528        self
1529    }
1530
1531    /// Where bars grow from: up from the bottom by default.
1532    pub fn bars_from(mut self, from: ChartBarFrom) -> Self {
1533        self.look.horizontal = matches!(from, ChartBarFrom::Left | ChartBarFrom::Right);
1534        self.look.reversed = matches!(from, ChartBarFrom::Top | ChartBarFrom::Right);
1535        self
1536    }
1537
1538    /// The shortest a bar with a value gets, in logical pixels, however small the value
1539    /// is (half a pixel by default). A value of zero stays as good as nothing.
1540    pub fn min_bar_length(mut self, length: f32) -> Self {
1541        self.look.min_bar = Some(length);
1542        self
1543    }
1544
1545    /// The categories of bars sit on the line of zero, on its other side from the bar,
1546    /// instead of along the edge of the chart: for values both ways from zero.
1547    pub fn labels_at_zero(mut self) -> Self {
1548        self.look.labels_at_zero = true;
1549        self
1550    }
1551
1552    /// The corners of bars. In a stack, only its outer corners are round.
1553    pub fn bar_radius(mut self, radius: f32) -> Self {
1554        self.look.bar_radius = Some(radius);
1555        self
1556    }
1557
1558    /// Gives each category its own color from the theme's chart colors.
1559    pub fn vary_colors(mut self) -> Self {
1560        self.vary_colors = true;
1561        self
1562    }
1563
1564    /// Gives the categories these colors, in order, over those of the series.
1565    pub fn item_colors(mut self, colors: impl IntoIterator<Item = Semantic>) -> Self {
1566        self.item_colors = colors.into_iter().collect();
1567        self
1568    }
1569
1570    /// The color of bars below zero.
1571    pub fn negative_color(mut self, color: Semantic) -> Self {
1572        self.negative = Some(color);
1573        self
1574    }
1575
1576    /// Makes one category stand out: a slice comes forward, a bar gets an outline.
1577    pub fn active(mut self, index: Option<usize>) -> Self {
1578        self.look.active = index;
1579        self
1580    }
1581
1582    /// Draws a dot on each point of a line or a radar.
1583    pub fn dots(mut self) -> Self {
1584        self.look.dots = true;
1585        self
1586    }
1587
1588    /// Draws this icon on each point of a line, in place of a dot.
1589    pub fn dot_icon(mut self, icon: IconName) -> Self {
1590        self.dot_icon = Some(icon);
1591        self
1592    }
1593
1594    /// Writes each value beside the data.
1595    pub fn value_labels(mut self, at: ChartLabelAt) -> Self {
1596        self.look.value_labels = Some(at);
1597        self
1598    }
1599
1600    /// Writes each category beside the data.
1601    pub fn category_labels(mut self, at: ChartLabelAt) -> Self {
1602        self.look.category_labels = Some(at);
1603        self
1604    }
1605
1606    /// How categories are written on the axis, such as a month's first letters.
1607    pub fn tick_format(mut self, format: impl Fn(&str) -> String + 'static) -> Self {
1608        self.look.tick_format = Some(Rc::new(format));
1609        self
1610    }
1611
1612    /// Which gridlines it has (level lines by default).
1613    pub fn grid(mut self, grid: ChartGrid) -> Self {
1614        self.look.grid = grid;
1615        self
1616    }
1617
1618    /// Draws the gridlines in dashes.
1619    pub fn grid_dashed(mut self) -> Self {
1620        self.look.grid_dashed = true;
1621        self
1622    }
1623
1624    /// Upright gridlines evenly spread into this many columns, instead of one at each
1625    /// category or number.
1626    pub fn grid_columns(mut self, columns: usize) -> Self {
1627        self.look.grid_columns = Some(columns);
1628        self
1629    }
1630
1631    /// About how many steps the value axis has (4 by default). Round numbers decide
1632    /// how many exactly.
1633    pub fn value_ticks(mut self, ticks: usize) -> Self {
1634        self.look.value_ticks = Some(ticks);
1635        self
1636    }
1637
1638    /// The first category along the axis starts at its point and the last one ends at
1639    /// its point, so that they line up with the ends of a line or an area. The last one
1640    /// always shows, however many others there is no room for.
1641    pub fn align_axis_ends(mut self) -> Self {
1642        self.look.ends_aligned = true;
1643        self
1644    }
1645
1646    /// The value axis goes exactly from `min` to `max`, and what is past them is cut
1647    /// off. Its numbers are the round ones in between. Not for expanded stacks.
1648    pub fn value_domain(mut self, min: f64, max: f64) -> Self {
1649        self.look.domain = Some((min, max));
1650        self
1651    }
1652
1653    /// A dashed line across the data at this value. The scale stretches to show it,
1654    /// unless [`Self::value_domain`] fixed it.
1655    pub fn reference_line(self, value: f64) -> Self {
1656        self.reference(ChartReference::new(value))
1657    }
1658
1659    /// A dashed line across the data with its own text or color.
1660    pub fn reference(mut self, reference: ChartReference) -> Self {
1661        self.look.references.push(reference);
1662        self
1663    }
1664
1665    /// Leaves out the categories along the axis.
1666    pub fn hide_category_axis(mut self) -> Self {
1667        self.look.category_axis = false;
1668        self
1669    }
1670
1671    /// Writes numbers along the axis, which the tooltip otherwise makes unneeded.
1672    pub fn show_value_axis(mut self) -> Self {
1673        self.look.value_axis = true;
1674        self
1675    }
1676
1677    /// The scale fits the values, instead of starting from zero. Not for bars.
1678    pub fn tight_scale(mut self) -> Self {
1679        self.look.tight_scale = true;
1680        self
1681    }
1682
1683    /// Leaves out the gap between slices, through which what is behind the chart shows.
1684    pub fn no_separator(mut self) -> Self {
1685        self.look.no_separator = true;
1686        self
1687    }
1688
1689    /// How far each slice reaches, in order, as a share from 0 to 1 of the room from
1690    /// the hole to the edge. A slice with no number here reaches the edge.
1691    pub fn slice_radii(mut self, shares: impl IntoIterator<Item = f32>) -> Self {
1692        self.look.slice_radii = shares.into_iter().collect();
1693        self
1694    }
1695
1696    /// Labels asked for outside a pie or a doughnut sit in a column on each side, none
1697    /// over another, each joined to its slice by a line with an elbow. The chart gets
1698    /// smaller to give them the room they need.
1699    pub fn label_guides(mut self) -> Self {
1700        self.look.label_guides = true;
1701        self
1702    }
1703
1704    /// The hole of a doughnut or of rings, as a share of the radius.
1705    pub fn hole(mut self, share: f32) -> Self {
1706        self.look.hole = Some(share);
1707        self
1708    }
1709
1710    /// One ring per series in a pie, instead of only the first series.
1711    pub fn concentric(mut self) -> Self {
1712        self.look.concentric = true;
1713        self
1714    }
1715
1716    /// The shape of the grid of a radar or radial chart.
1717    pub fn polar_grid(mut self, grid: ChartPolarGrid) -> Self {
1718        self.look.polar_grid = Some(grid);
1719        self
1720    }
1721
1722    /// Fills the grid of a radar chart with the first color.
1723    pub fn polar_fill(mut self) -> Self {
1724        self.look.polar_fill = true;
1725        self
1726    }
1727
1728    /// Leaves out the lines from the center of a radar chart.
1729    pub fn no_spokes(mut self) -> Self {
1730        self.look.spokes = false;
1731        self
1732    }
1733
1734    /// How many rings the grid of a radar chart has (5 by default).
1735    pub fn rings(mut self, rings: usize) -> Self {
1736        self.look.rings = rings;
1737        self
1738    }
1739
1740    /// Radar polygons with no fill.
1741    pub fn lines_only(mut self) -> Self {
1742        self.look.lines_only = true;
1743        self
1744    }
1745
1746    /// Writes numbers along the radius of a radar chart.
1747    pub fn radius_axis(mut self) -> Self {
1748        self.look.radius_axis = true;
1749        self
1750    }
1751
1752    /// Where the rings of a radial chart start, in degrees clockwise from the top,
1753    /// and how many degrees the largest covers.
1754    pub fn angles(mut self, start: f32, sweep: f32) -> Self {
1755        self.look.start_angle = start;
1756        self.look.sweep = sweep;
1757        self
1758    }
1759
1760    /// Draws a track behind each ring of a radial chart.
1761    pub fn tracks(mut self) -> Self {
1762        self.look.tracks = true;
1763        self
1764    }
1765
1766    /// A big number and a caption in the middle of a doughnut or of rings.
1767    pub fn center_text(mut self, value: impl Into<String>, caption: impl Into<String>) -> Self {
1768        self.look.center = Some((value.into(), caption.into()));
1769        self
1770    }
1771
1772    /// What it says about the category under the pointer. A plain tooltip by default,
1773    /// except on a [`ChartKind::Scatter`] and a sparkline: with this they also get the
1774    /// cursor, the dots in focus, and the arrow keys.
1775    pub fn tooltip(mut self, tooltip: ChartTooltip) -> Self {
1776        self.tooltip = Some(tooltip);
1777        self.tooltip_asked = true;
1778        self
1779    }
1780
1781    /// Says nothing under the pointer and takes no keyboard focus.
1782    pub fn hide_tooltip(mut self) -> Self {
1783        self.tooltip = None;
1784        self
1785    }
1786
1787    /// Keeps the chart as it is around the category under the pointer: the other
1788    /// bars and slices do not fade, and no slice comes forward. The tooltip and the
1789    /// cursor stay.
1790    pub fn no_hover_focus(mut self) -> Self {
1791        self.hover_focus = false;
1792        self
1793    }
1794
1795    /// The cursor, its dots and the focus go straight from one category to the next,
1796    /// with no glide and no fade, as they do when the system asks for less motion.
1797    pub fn no_hover_motion(mut self) -> Self {
1798        self.hover_motion = false;
1799        self
1800    }
1801
1802    /// The data is whole from the first frame, instead of drawing in the first time
1803    /// the chart is painted.
1804    pub fn no_appear(mut self) -> Self {
1805        self.appear = false;
1806        self
1807    }
1808
1809    /// When the data changes to as many series and categories as before, the chart goes
1810    /// from its old shape to the new one: lines and bars rise or fall to their new values,
1811    /// slices turn and grow, and the value axis slides along with them. A change in the
1812    /// middle starts from where the chart is. Under reduced motion it changes at once.
1813    /// For a filter over the same categories; data that scrolls along, as a live feed
1814    /// does, reads better without it.
1815    pub fn animate_changes(mut self) -> Self {
1816        self.animate_changes = true;
1817        self
1818    }
1819
1820    /// Draws the data in again whenever `key` changes, such as for a new period or
1821    /// another tab. With none, it only draws in the first time the chart is painted.
1822    pub fn appear_key(mut self, key: impl std::hash::Hash) -> Self {
1823        self.appear_key = appear_key_of(key);
1824        self
1825    }
1826
1827    /// How its data draws in, or `None` when it only fades in.
1828    fn draw_in(&self, separator: f32) -> Option<DrawIn> {
1829        match self.kind {
1830            ChartKind::Bar => Some(DrawIn::Grow { horizontal: self.look.horizontal }),
1831            ChartKind::Pie | ChartKind::Doughnut => Some(DrawIn::Sweep { separator }),
1832            ChartKind::Radar => Some(DrawIn::Spread),
1833            ChartKind::RadialBar => None,
1834            _ => Some(DrawIn::Reveal),
1835        }
1836    }
1837
1838    /// Where the legend goes (under the chart by default).
1839    pub fn legend_at(mut self, at: ChartLegend) -> Self {
1840        self.legend_at = at;
1841        self
1842    }
1843
1844    /// Every series' values, as a screen reader hears them.
1845    fn spoken_values(&self, labels: &crate::Labels) -> String {
1846        if self.kind == ChartKind::Candlestick {
1847            return self.spoken_candles(labels);
1848        }
1849        let named = self.series.len() > 1;
1850        // A ring draws no slice for nothing, so there is nothing to hear for it either.
1851        let round = matches!(self.kind, ChartKind::Pie | ChartKind::Doughnut);
1852        let parts: Vec<String> = self
1853            .series
1854            .iter()
1855            .map(|series| {
1856                let mut spoken: Vec<String> = series
1857                    .values
1858                    .iter()
1859                    .enumerate()
1860                    .filter(|(_, value)| !round || **value > 0.0)
1861                    .take(SPOKEN_VALUES)
1862                    .map(|(i, value)| {
1863                        let number = (self.format)(*value);
1864                        match self.categories.get(i) {
1865                            Some(category) => format!("{category}: {number}"),
1866                            None => number,
1867                        }
1868                    })
1869                    .collect();
1870                if series.values.len() > SPOKEN_VALUES {
1871                    spoken.push("…".to_owned());
1872                }
1873                let values = spoken.join(", ");
1874                if named { format!("{}: {values}", series.name) } else { values }
1875            })
1876            .collect();
1877        parts.join("; ")
1878    }
1879
1880    fn candle_names(labels: &crate::Labels) -> [&str; 4] {
1881        [&labels.candle_open, &labels.candle_high, &labels.candle_low, &labels.candle_close]
1882    }
1883
1884    /// Each candle, as a screen reader hears it.
1885    fn spoken_candles(&self, labels: &crate::Labels) -> String {
1886        let names = Self::candle_names(labels);
1887        let mut spoken: Vec<String> = self
1888            .categories
1889            .iter()
1890            .take(SPOKEN_VALUES)
1891            .enumerate()
1892            .map(|(i, category)| {
1893                let values = self.series.iter().zip(names).filter_map(|(series, name)| {
1894                    series.values.get(i).map(|value| format!("{name} {}", (self.format)(*value)))
1895                });
1896                format!("{category}: {}", values.collect::<Vec<_>>().join(", "))
1897            })
1898            .collect();
1899        if self.categories.len() > SPOKEN_VALUES {
1900            spoken.push("…".to_owned());
1901        }
1902        spoken.join("; ")
1903    }
1904
1905    /// Whether this chart is one of the plain pictures.
1906    fn is_plain(&self) -> bool {
1907        matches!(self.kind, ChartKind::Scatter | ChartKind::Sparkline | ChartKind::SparkArea)
1908    }
1909
1910    /// Whether its categories are what tells colors apart, and not its series.
1911    fn colors_by_category(&self) -> bool {
1912        matches!(self.kind, ChartKind::Pie | ChartKind::Doughnut)
1913            || (self.kind == ChartKind::RadialBar && self.look.stack == ChartStack::None)
1914    }
1915
1916    fn color_of_series(&self, index: usize, colors: &[Color]) -> Color {
1917        self.series[index].color.unwrap_or(colors[index % colors.len()])
1918    }
1919
1920    fn category_color(&self, index: usize, colors: &[Color], theme: &Theme) -> Color {
1921        self.item_colors.get(index).map_or(colors[index % colors.len()], |role| theme.color(*role))
1922    }
1923
1924    fn plot_style(&self, theme: &Theme, colors: &[Color]) -> PlotStyle {
1925        let look = &self.look;
1926        let mut style = PlotStyle::new(self.kind);
1927        style.colors = colors.to_vec();
1928        if self.vary_colors {
1929            style.item_colors = colors.to_vec();
1930        }
1931        if !self.item_colors.is_empty() {
1932            style.item_colors = self.item_colors.iter().map(|role| theme.color(*role)).collect();
1933        }
1934        style.ink = theme.color(Semantic::Ink);
1935        style.muted = theme.color(Semantic::InkMuted);
1936        style.grid = theme.color(Semantic::ChartGrid);
1937        style.surface = theme.color(Semantic::SurfaceElevated);
1938        style.track = theme.color(Semantic::Control);
1939        style.size = theme.text(TextStyle::Caption).size;
1940        style.curve = look.curve;
1941        style.stack = look.stack;
1942        style.gradient = look.gradient;
1943        if let Some(opacity) = look.fill_opacity {
1944            style.fill_opacity = opacity;
1945        }
1946        style.horizontal = look.horizontal;
1947        style.reversed = look.reversed;
1948        if let Some(length) = look.min_bar {
1949            style.min_bar = length.max(0.0);
1950        }
1951        style.labels_at_zero = look.labels_at_zero;
1952        let bars = if self.series.len() > 1 { theme.radii.sm } else { theme.radii.md };
1953        style.bar_radius = look.bar_radius.unwrap_or(bars);
1954        style.negative = self.negative.map(|role| theme.color(role));
1955        style.rising = theme.color(Semantic::Success);
1956        style.falling = theme.color(Semantic::Danger);
1957        style.active = look.active;
1958        style.dots = look.dots && self.dot_icon.is_none();
1959        style.value_labels = look.value_labels;
1960        style.category_labels = look.category_labels;
1961        let format = self.format.clone();
1962        style.format = Rc::new(move |value| format(value));
1963        if let Some(format) = &look.tick_format {
1964            let format = format.clone();
1965            style.tick_format = Rc::new(move |text| format(text));
1966        }
1967        style.grid_lines = look.grid;
1968        style.grid_dashed = look.grid_dashed;
1969        style.ends_aligned = look.ends_aligned;
1970        style.domain = look.domain;
1971        style.references = look
1972            .references
1973            .iter()
1974            .map(|r| plot::Reference::new(r.value, r.label.clone(), r.color.map(|role| theme.color(role))))
1975            .collect();
1976        style.grid_columns = look.grid_columns;
1977        if let Some(ticks) = look.value_ticks {
1978            style.value_ticks = ticks;
1979        }
1980        style.category_axis = look.category_axis;
1981        style.value_axis = look.value_axis || self.kind == ChartKind::Candlestick;
1982        style.tight_scale = look.tight_scale;
1983        if look.no_separator {
1984            style.separator = 0.0;
1985        }
1986        style.slice_radii.clone_from(&look.slice_radii);
1987        style.label_guides = look.label_guides;
1988        if let Some(hole) = look.hole {
1989            style.hole = hole;
1990        }
1991        style.concentric = look.concentric;
1992        if let Some(grid) = look.polar_grid {
1993            style.polar_grid = grid;
1994        }
1995        style.polar_fill = look.polar_fill;
1996        style.spokes = look.spokes;
1997        style.rings = look.rings;
1998        style.lines_only = look.lines_only;
1999        style.radius_axis = look.radius_axis;
2000        style.start_angle = look.start_angle;
2001        style.sweep = look.sweep;
2002        style.tracks = look.tracks;
2003        style.center.clone_from(&look.center);
2004        style.rules_apart = true;
2005        style.bars_apart = true;
2006        style
2007    }
2008
2009    fn legend_rows(&self, colors: &[Color], theme: &Theme) -> Vec<KeyRow> {
2010        // Up and down are no series to tell apart.
2011        if self.kind == ChartKind::Candlestick {
2012            return Vec::new();
2013        }
2014        if self.colors_by_category() {
2015            let first = self.series.first().map(|s| s.values.as_slice());
2016            return self
2017                .categories
2018                .iter()
2019                .enumerate()
2020                .filter(|(i, _)| first.is_none_or(|values| values.get(*i).is_some_and(|v| *v > 0.0)))
2021                .map(|(i, name)| {
2022                    KeyRow::named(name.clone(), self.category_color(i, colors, theme), None, None)
2023                })
2024                .collect();
2025        }
2026        if self.series.len() < 2 && self.series.iter().all(|s| s.icon.is_none()) {
2027            return Vec::new();
2028        }
2029        self.series
2030            .iter()
2031            .enumerate()
2032            .map(|(i, series)| {
2033                KeyRow::named(series.name.clone(), self.color_of_series(i, colors), series.icon, None)
2034            })
2035            .collect()
2036    }
2037
2038    /// What a tooltip says about a category.
2039    fn tooltip_panel(
2040        &self,
2041        tooltip: &ChartTooltip,
2042        index: usize,
2043        colors: &[Color],
2044        theme: &Theme,
2045        labels: &crate::Labels,
2046    ) -> TooltipPanel {
2047        // A dot may have only its position to go by.
2048        let named = self.categories.get(index).filter(|name| !name.is_empty()).cloned();
2049        let category = named.unwrap_or_else(|| {
2050            self.positions.get(index).map(|position| (self.format)(*position)).unwrap_or_default()
2051        });
2052        let rows: Vec<KeyRow> = if self.colors_by_category() && !self.look.concentric {
2053            let values = self.series.first().map_or(&[][..], |s| s.values.as_slice());
2054            let value = values.get(index).copied();
2055            let mut row =
2056                KeyRow::named(category.clone(), self.category_color(index, colors, theme), None, value);
2057            if tooltip.percent {
2058                let whole: f64 = values.iter().filter(|v| **v > 0.0).sum();
2059                row.share = value.and_then(|value| percent_text(value, whole));
2060            }
2061            row.value_color = value.and_then(|value| tooltip.color_of_value(0, value, theme));
2062            vec![row]
2063        } else {
2064            let whole: f64 = self
2065                .series
2066                .iter()
2067                .filter_map(|s| s.values.get(index))
2068                .filter(|v| v.is_finite())
2069                .map(|v| v.abs())
2070                .sum();
2071            self.series
2072                .iter()
2073                .enumerate()
2074                .filter_map(|(i, series)| {
2075                    let value = series.values.get(index).copied().filter(|v| v.is_finite())?;
2076                    let color = if self.look.concentric || self.vary_colors || !self.item_colors.is_empty() {
2077                        self.category_color(index, colors, theme)
2078                    } else {
2079                        self.color_of_series(i, colors)
2080                    };
2081                    let mut row = KeyRow::named(series.name.clone(), color, series.icon, Some(value));
2082                    if tooltip.percent {
2083                        row.share = percent_text(value, whole);
2084                    }
2085                    row.value_color = tooltip.color_of_value(i, value, theme);
2086                    row.no_indicator = tooltip.no_indicator.contains(&series.name);
2087                    Some(row)
2088                })
2089                .collect()
2090        };
2091        let mut rows = rows;
2092        if self.kind == ChartKind::Candlestick {
2093            let at = |s: usize| self.series.get(s).and_then(|series| series.values.get(index)).copied();
2094            let rising = at(3) >= at(0);
2095            let color = theme.color(if rising { Semantic::Success } else { Semantic::Danger });
2096            for (row, name) in rows.iter_mut().zip(Self::candle_names(labels)) {
2097                row.name = name.to_owned();
2098                row.color = color;
2099            }
2100        }
2101        let by_category = self.colors_by_category() && !self.look.concentric;
2102        let name = category.clone();
2103        let title = if tooltip.hide_label || by_category || (category.is_empty() && tooltip.label.is_none()) {
2104            None
2105        } else if let Some(label) = &tooltip.label {
2106            Some(label.clone())
2107        } else {
2108            Some(match &tooltip.label_format {
2109                Some(format) => format(&category),
2110                None => category,
2111            })
2112        };
2113        let total = tooltip
2114            .total
2115            .as_ref()
2116            .map(|name| (name.clone(), rows.iter().filter_map(|row| row.value).sum::<f64>()));
2117        TooltipPanel {
2118            title,
2119            rows,
2120            total,
2121            tooltip: tooltip.clone(),
2122            format: self.format.clone(),
2123            index,
2124            category: name,
2125        }
2126    }
2127
2128    /// With [`Self::animate_changes`], puts the data, and the value axis of a chart that
2129    /// has one, where they are on the way to the new values. Asked for on every build, so
2130    /// the states after it keep their places.
2131    fn ease_changes(&self, cx: &mut BuildCx<'_>, data: &mut ChartData, style: &mut PlotStyle) {
2132        let mut target: Vec<f64> = Vec::new();
2133        let ranged =
2134            matches!(self.kind, ChartKind::Line | ChartKind::Area | ChartKind::Bar) && style.domain.is_none();
2135        let range = if self.animate_changes && ranged { plot::value_range(data, style) } else { None };
2136        if self.animate_changes {
2137            target.extend(data.series.iter().flat_map(|s| s.values.iter().copied()));
2138            target.extend(range.into_iter().flat_map(|(low, high)| [low, high]));
2139        }
2140        let now = cx.animated_numbers(&target, CHANGE);
2141        if now.iter().map(|v| v.to_bits()).eq(target.iter().map(|v| v.to_bits())) {
2142            return;
2143        }
2144        let mut values = now.iter().copied();
2145        for series in &mut data.series {
2146            for value in &mut series.values {
2147                *value = values.next().unwrap_or(*value);
2148            }
2149        }
2150        if let (Some(low), Some(high)) = (values.next(), values.next()) {
2151            style.domain = Some((low, high));
2152        }
2153    }
2154
2155    fn build_plot(&self, cx: &mut BuildCx<'_>) -> NodeId {
2156        let theme = *cx.theme();
2157        let labels = cx.labels();
2158        let colors = palette(&theme);
2159        let mut style = self.plot_style(&theme, &colors);
2160        let series = self.series.iter().enumerate().map(|(i, series)| SeriesData {
2161            name: series.name.clone(),
2162            color: self.color_of_series(i, &colors),
2163            values: series.values.clone(),
2164        });
2165        let mut data = ChartData {
2166            categories: self.categories.clone(),
2167            positions: Vec::new(),
2168            series: series.collect(),
2169        };
2170        self.ease_changes(cx, &mut data, &mut style);
2171        let plain = self.is_plain();
2172        let picture = cx.use_state(|| Rc::new(RefCell::new(Picture::default()))).peek().clone();
2173        let active: State<Option<usize>> = cx.use_state(|| None::<usize>);
2174        let hovered = cx.is_hover_within();
2175        let focused = cx.is_focused();
2176        let draw_in = self.draw_in(style.separator).filter(|_| self.appear);
2177        let m = theme.motion;
2178        // Asked for on every build, so the states after it keep their places.
2179        let progress =
2180            cx.appear(self.appear_key, if draw_in.is_some() { m.draw_in } else { Transition::Instant });
2181        let appear = draw_in.map(|how| Appear { how, progress });
2182        // The tooltip waits until the data is whole.
2183        let whole = appear.is_none_or(|appear| appear.progress >= 1.0);
2184        let shown = active.get().filter(|_| (hovered || focused) && self.tooltip.is_some() && whole);
2185        let pointer: State<Option<Point>> = cx.use_state(|| None::<Point>);
2186
2187        // The anchor of the tooltip is where the last picture put this category.
2188        let mut children = Vec::new();
2189        let spot = shown.and_then(|index| {
2190            let picture = picture.borrow();
2191            let place = picture.drawing.anchor_of(index)?;
2192            // It follows the pointer without covering what is in focus: along a column
2193            // or a row it stays beside the strip, and around a circle it goes outward.
2194            // Each way it takes the side with the most room. With the keyboard it stays
2195            // on the category.
2196            let follows = pointer.get().filter(|_| hovered);
2197            let size = picture.size;
2198            let spot = picture.drawing.spots.iter().find(|s| s.index == index);
2199            let round = spot.is_none();
2200            let along = follows.unwrap_or(place);
2201            let (past_x, past_y) = (along.x > size.width / 2.0, along.y > size.height / 2.0);
2202            let is_bar = matches!(self.kind, ChartKind::Bar | ChartKind::Candlestick);
2203            let at = match (follows, spot) {
2204                (Some(p), Some(spot)) if self.look.horizontal && is_bar => {
2205                    Point::new(p.x, if past_y { spot.band.y() } else { spot.band.max_y() })
2206                }
2207                (Some(p), Some(spot)) if is_bar => {
2208                    Point::new(if past_x { spot.band.x() } else { spot.band.max_x() }, p.y)
2209                }
2210                (Some(p), Some(_)) => Point::new(place.x, p.y),
2211                (Some(p), None) => p,
2212                (None, _) => place,
2213            };
2214            let to_the_left = if round { !past_x } else { past_x };
2215            let rises = if round { !past_y } else { past_y };
2216            let placement = match (follows.is_some(), to_the_left, rises) {
2217                (false, false, _) => Placement::RightCenter,
2218                (false, true, _) => Placement::LeftCenter,
2219                (true, false, false) => Placement::Right,
2220                (true, false, true) => Placement::RightEnd,
2221                (true, true, false) => Placement::LeftStart,
2222                (true, true, true) => Placement::LeftEnd,
2223            };
2224            Some((at, placement))
2225        });
2226        let anchor = spot.map(|(at, _)| {
2227            cx.container(
2228                LayoutStyle {
2229                    absolute: Some((at.x, at.y)),
2230                    width: Some(0.0),
2231                    height: Some(0.0),
2232                    ..LayoutStyle::leaf()
2233                },
2234                &[],
2235                Paint::None,
2236            )
2237        });
2238        children.extend(anchor);
2239
2240        let cursor = self.tooltip.as_ref().is_some_and(|t| !t.no_cursor);
2241        let is_bar = matches!(self.kind, ChartKind::Bar | ChartKind::Candlestick);
2242        let is_line = plain || matches!(self.kind, ChartKind::Line | ChartKind::Area);
2243        let is_round = matches!(self.kind, ChartKind::Pie | ChartKind::Doughnut);
2244        let moving =
2245            |transition: Transition| if self.hover_motion { transition } else { Transition::Instant };
2246
2247        // What stands out outlasts the pointer for as long as it takes to fade.
2248        let held = cx.use_state(|| Rc::new(Cell::new(None::<usize>))).peek().clone();
2249        if shown.is_some() {
2250            held.set(shown);
2251        }
2252        let focus = cx.animated(if shown.is_some() { 1.0_f32 } else { 0.0 }, moving(m.base));
2253        // Coming from nothing, it starts in place: it only glides between categories.
2254        let target = held.get().unwrap_or(0) as f32;
2255        let at = cx.animated(target, if focus < 0.02 { Transition::Instant } else { moving(m.snappy) });
2256        // Kept last among the states of this view, as their number follows the data.
2257        let wants_lift = is_round && self.hover_focus && self.tooltip.is_some();
2258        let lifts: Vec<f32> = if wants_lift {
2259            (0..self.categories.len())
2260                .map(|i| cx.animated(if shown == Some(i) { 1.0_f32 } else { 0.0 }, moving(m.snappy)))
2261                .collect()
2262        } else {
2263            Vec::new()
2264        };
2265        let lifting = lifts.iter().any(|lift| *lift > 0.001);
2266        let highlight = (focus > 0.001 || lifting).then(|| Highlight {
2267            at,
2268            focus,
2269            dim: if self.hover_focus && (is_bar || is_round) { FOCUS_DIM } else { 0.0 },
2270            lifts,
2271            separator: style.separator,
2272            strip: is_bar && cursor,
2273            line: is_line && cursor,
2274            dots: is_line || self.kind == ChartKind::Radar,
2275            strip_color: theme.color(Semantic::Control).with_alpha(0.6),
2276            line_color: theme.color(Semantic::InkMuted).with_alpha(CURSOR_ALPHA),
2277            ring_color: theme.color(Semantic::SurfaceElevated),
2278        });
2279        let drawing: DrawingFn = if plain {
2280            self.small_drawing(&theme, &colors, true)
2281        } else {
2282            Rc::new(move |rect, _, measure| plot::draw_measured(&data, rect, &style, measure))
2283        };
2284        let node_style = LayoutStyle { grow: 1.0, align_self: Some(Align::Stretch), ..LayoutStyle::leaf() };
2285        let node = plot_node(
2286            cx,
2287            node_style,
2288            picture.clone(),
2289            drawing,
2290            self.dot_icon,
2291            (highlight, appear),
2292            &children,
2293        );
2294
2295        if self.tooltip.is_some() {
2296            let (write, seen, follow) = (active.clone(), picture.clone(), pointer.clone());
2297            cx.on_pointer_move(
2298                node,
2299                Rc::new(move |position, frame| {
2300                    let local = Point::new(position.x - frame.x(), position.y - frame.y());
2301                    follow.set(Some(local));
2302                    let picture = seen.borrow();
2303                    let hit = picture.drawing.hit(local).map(|hit| hit.index).or_else(|| {
2304                        // A slice that came forward answers for what it covers now.
2305                        let lifted = write.get().filter(|_| wants_lift)?;
2306                        picture.drawing.over_grown_slice(local, lifted, SLICE_LIFT).then_some(lifted)
2307                    });
2308                    if write.get() != hit {
2309                        write.set(hit);
2310                    }
2311                }),
2312            );
2313            cx.cursor(node, CursorIcon::Default);
2314            let (write, seen) = (active.clone(), picture);
2315            cx.on_key(
2316                node,
2317                Rc::new(move |key, _| {
2318                    let count = seen.borrow().drawing.categories();
2319                    if count == 0 {
2320                        return false;
2321                    }
2322                    let last = count - 1;
2323                    let now = write.get();
2324                    let next = match key {
2325                        Key::ArrowRight | Key::ArrowDown => now.map_or(0, |i| (i + 1).min(last)),
2326                        Key::ArrowLeft | Key::ArrowUp => now.map_or(last, |i| i.saturating_sub(1)),
2327                        Key::Home => 0,
2328                        Key::End => last,
2329                        Key::Escape if now.is_some() => {
2330                            write.set(None);
2331                            return true;
2332                        }
2333                        _ => return false,
2334                    };
2335                    write.set(Some(next));
2336                    true
2337                }),
2338            );
2339            cx.focusable(node, theme.radii.sm);
2340        }
2341
2342        if let (Some(tooltip), Some(index), Some((_, placement)), Some(anchor)) =
2343            (&self.tooltip, shown, spot, anchor)
2344        {
2345            let panel = self.tooltip_panel(tooltip, index, &colors, &theme, &labels);
2346            cx.overlay(anchor, placement, &panel, None);
2347        }
2348
2349        let name = self.alt.as_deref().unwrap_or(&self.title);
2350        let values = self.spoken_values(&labels);
2351        let mut semantics = if name.is_empty() {
2352            Semantics::new(Role::Image).name(values)
2353        } else {
2354            Semantics::new(Role::Image).name(name.to_owned()).description(values)
2355        };
2356        if let (Some(tooltip), Some(index)) = (&self.tooltip, shown) {
2357            let panel = self.tooltip_panel(tooltip, index, &colors, &theme, &labels);
2358            semantics = semantics.value(panel.spoken());
2359        }
2360        cx.semantics(node, semantics);
2361        // What does not draw in fades in.
2362        if appear.is_none() {
2363            cx.motion(node, Motion::new().enter(Effect::hidden(), m.base));
2364        }
2365
2366        let mut parts = Vec::new();
2367        // The small kinds have their title and legend in their picture.
2368        if !self.title.is_empty() && !plain {
2369            let ink = theme.color(Semantic::Ink);
2370            parts.push(util::label(cx, &self.title, TextStyle::Callout, Some(Weight::SEMIBOLD), ink));
2371        }
2372        let rows = if self.legend && !plain { self.legend_rows(&colors, &theme) } else { Vec::new() };
2373        let legend = (!rows.is_empty()).then(|| cx.child(&LegendRow { rows }));
2374        if self.legend_at == ChartLegend::Top {
2375            parts.extend(legend);
2376        }
2377        parts.push(node);
2378        if self.legend_at == ChartLegend::Bottom {
2379            parts.extend(legend);
2380        }
2381        cx.container(
2382            LayoutStyle {
2383                width: self.width,
2384                height: Some(self.height),
2385                spacing: 8.0,
2386                align: Align::Stretch,
2387                align_self: self.width.is_none().then_some(Align::Stretch),
2388                ..LayoutStyle::stack(Axis::Vertical)
2389            },
2390            &parts,
2391            Paint::None,
2392        )
2393    }
2394}
2395
2396#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
2397impl TooltipPanel {
2398    /// The same as the panel, for a screen reader.
2399    pub(crate) fn spoken(&self) -> String {
2400        let format = self.tooltip.value_format.as_ref().unwrap_or(&self.format);
2401        let mut parts: Vec<String> = self.title.iter().cloned().collect();
2402        parts.extend(self.rows.iter().map(|row| match row.value {
2403            Some(value) => match &row.share {
2404                Some(share) => format!("{}: {} ({share})", row.name, format(value)),
2405                None => format!("{}: {}", row.name, format(value)),
2406            },
2407            None => row.name.clone(),
2408        }));
2409        if let Some((name, total)) = &self.total {
2410            parts.push(format!("{name}: {}", format(*total)));
2411        }
2412        parts.join(", ")
2413    }
2414}
2415
2416#[cfg_attr(feature = "live", ravenrs_live::expose(part = "part1"))]
2417impl Chart {
2418    /// How a scatter chart or a sparkline is drawn, the same with a tooltip or without.
2419    fn small_drawing(&self, theme: &Theme, colors: &[Color], placed: bool) -> DrawingFn {
2420        let series = self.series.iter().enumerate().map(|(i, series)| SeriesData {
2421            name: series.name.clone(),
2422            color: series.color.unwrap_or(colors[i % colors.len()]),
2423            values: series.values.clone(),
2424        });
2425        let data = ChartData {
2426            categories: self.categories.clone(),
2427            positions: self.positions.clone(),
2428            series: series.collect(),
2429        };
2430        let (kind, legend, title, format) = (self.kind, self.legend, self.title.clone(), self.format.clone());
2431        // The axis text is the caption size, and the rest is sized from it.
2432        let size = theme.text(TextStyle::Caption).size / 0.85;
2433        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
2434        let (grid, separator) = (theme.color(Semantic::Border), theme.color(Semantic::SurfaceElevated));
2435        let (bar_radius, colors) = (theme.radii.sm, colors.to_vec());
2436        Rc::new(move |rect, _, _| {
2437            let options =
2438                ChartOptions { kind, title: &title, legend, size, ink, muted, grid, format: &*format };
2439            let look = ChartStyle { palette: &colors, separator, bar_radius, max_bar_width: MAX_BAR_WIDTH };
2440            if placed {
2441                plot::draw_small(&data, rect, &options, &look)
2442            } else {
2443                chart::draw_styled(&data, rect, &options, &look).into()
2444            }
2445        })
2446    }
2447}
2448
2449impl View for Chart {
2450    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2451        if !self.is_plain() || (self.tooltip_asked && self.tooltip.is_some()) {
2452            return self.build_plot(cx);
2453        }
2454        let theme = *cx.theme();
2455        let colors = palette(&theme);
2456        let drawing = self.small_drawing(&theme, &colors, false);
2457        let style = LayoutStyle {
2458            width: self.width,
2459            height: Some(self.height),
2460            align_self: self.width.is_none().then_some(Align::Stretch),
2461            ..LayoutStyle::leaf()
2462        };
2463        let node = drawing_node(cx, style, drawing);
2464        let values = self.spoken_values(&cx.labels());
2465        let name = self.alt.as_deref().unwrap_or(&self.title);
2466        let semantics = if name.is_empty() {
2467            Semantics::new(Role::Image).name(values)
2468        } else {
2469            Semantics::new(Role::Image).name(name.to_owned()).description(values)
2470        };
2471        cx.semantics(node, semantics);
2472        node
2473    }
2474}
2475
2476/// A bar chart with labels under each bar and faint gridlines.
2477///
2478/// Available with the `charts` feature, on by default.
2479#[derive(Clone, Debug)]
2480#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
2481pub struct BarChart {
2482    data: Vec<(String, f32)>,
2483    height: f32,
2484}
2485
2486#[cfg_attr(feature = "live", ravenrs_live::expose)]
2487impl BarChart {
2488    /// A chart of `(label, value)` bars; negative values are clamped to 0.
2489    pub fn new(data: impl IntoIterator<Item = (impl Into<String>, f32)>) -> Self {
2490        Self { data: data.into_iter().map(|(l, v)| (l.into(), v.max(0.0))).collect(), height: 180.0 }
2491    }
2492
2493    /// Height of the bars in logical pixels, without their labels (default 180).
2494    pub fn height(mut self, height: f32) -> Self {
2495        self.height = height;
2496        self
2497    }
2498}
2499
2500impl View for BarChart {
2501    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2502        let labels_height = cx.theme().text(TextStyle::Caption).line_height + 8.0;
2503        let chart = Chart::new(ChartKind::Bar)
2504            .categories(self.data.iter().map(|(label, _)| label.as_str()))
2505            .series("", self.data.iter().map(|(_, value)| f64::from(*value)))
2506            .height(self.height + labels_height);
2507        cx.child(&chart)
2508    }
2509}
2510
2511/// A smooth area chart (sparkline style) of evenly spaced values.
2512///
2513/// Available with the `charts` feature, on by default.
2514#[derive(Clone, Debug)]
2515#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
2516pub struct AreaChart {
2517    values: Vec<f32>,
2518    height: f32,
2519    tone: Semantic,
2520}
2521
2522#[cfg_attr(feature = "live", ravenrs_live::expose)]
2523impl AreaChart {
2524    /// A chart of these values, evenly spaced from left to right.
2525    pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
2526        Self { values: values.into_iter().collect(), height: 120.0, tone: Semantic::Accent }
2527    }
2528
2529    /// Chart height in logical pixels (default 120).
2530    pub fn height(mut self, height: f32) -> Self {
2531        self.height = height;
2532        self
2533    }
2534
2535    /// The line color role (Accent by default; Success, Danger...).
2536    pub fn tone(mut self, tone: Semantic) -> Self {
2537        self.tone = tone;
2538        self
2539    }
2540}
2541
2542impl View for AreaChart {
2543    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2544        let color = cx.theme().color(self.tone);
2545        let chart = Chart::new(ChartKind::SparkArea)
2546            .series_color("", color, self.values.iter().map(|value| f64::from(*value)))
2547            .height(self.height);
2548        cx.child(&chart)
2549    }
2550}
2551
2552/// A chart's card: its title and description, what changes it, and what it says at
2553/// the bottom, such as how it compares to last month.
2554///
2555/// ```
2556/// use ravenrs_widgets::{Chart, ChartCard, ChartKind, IconName};
2557///
2558/// let card = ChartCard::new(Chart::new(ChartKind::Bar))
2559///     .title("Sales")
2560///     .description("January to June")
2561///     .trend("Up by 5.2% this month", IconName::TrendingUp)
2562///     .note("Showing all channels");
2563/// ```
2564pub struct ChartCard<V> {
2565    content: V,
2566    title: String,
2567    description: String,
2568    action: Option<Rc<dyn View>>,
2569    tabs: Option<ChartTabs>,
2570    trend: Option<(String, IconName)>,
2571    note: Option<String>,
2572    centered: bool,
2573}
2574
2575impl<V> fmt::Debug for ChartCard<V> {
2576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2577        f.debug_struct("ChartCard").field("title", &self.title).finish_non_exhaustive()
2578    }
2579}
2580
2581#[cfg_attr(feature = "live", ravenrs_live::expose)]
2582impl<V: View> ChartCard<V> {
2583    /// A card around `content`.
2584    pub fn new(content: V) -> Self {
2585        Self {
2586            content,
2587            title: String::new(),
2588            description: String::new(),
2589            action: None,
2590            tabs: None,
2591            trend: None,
2592            note: None,
2593            centered: false,
2594        }
2595    }
2596
2597    /// The title.
2598    pub fn title(mut self, title: impl Into<String>) -> Self {
2599        self.title = title.into();
2600        self
2601    }
2602
2603    /// What the chart shows, under the title.
2604    pub fn description(mut self, description: impl Into<String>) -> Self {
2605        self.description = description.into();
2606        self
2607    }
2608
2609    /// Something beside the title that changes the chart, such as a period.
2610    pub fn action(mut self, action: impl View + 'static) -> Self {
2611        self.action = Some(Rc::new(action));
2612        self
2613    }
2614
2615    /// Choices beside the title, each with a big number: what to show, and how much it is.
2616    pub fn tabs(
2617        mut self,
2618        selected: Binding<usize>,
2619        tabs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
2620    ) -> Self {
2621        let tabs = tabs.into_iter().map(|(name, total)| (name.into(), total.into())).collect();
2622        self.tabs = Some((selected, tabs));
2623        self
2624    }
2625
2626    /// A line at the bottom that says what the chart means, with an icon.
2627    pub fn trend(mut self, text: impl Into<String>, icon: IconName) -> Self {
2628        self.trend = Some((text.into(), icon));
2629        self
2630    }
2631
2632    /// A fainter line at the bottom.
2633    pub fn note(mut self, text: impl Into<String>) -> Self {
2634        self.note = Some(text.into());
2635        self
2636    }
2637
2638    /// Centers the title and the bottom lines, as round charts do.
2639    pub fn centered(mut self) -> Self {
2640        self.centered = true;
2641        self
2642    }
2643}
2644
2645struct ChartTab {
2646    selected: Binding<usize>,
2647    index: usize,
2648    name: String,
2649    total: String,
2650}
2651
2652impl View for ChartTab {
2653    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2654        let theme = *cx.theme();
2655        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
2656        let on = self.selected.get() == self.index;
2657        let name = util::label(cx, &self.name, TextStyle::Caption, None, muted);
2658        let total = util::label(cx, &self.total, TextStyle::Title, Some(Weight::SEMIBOLD), ink);
2659        let fill = if on || cx.is_hovered() { theme.color(Semantic::Control) } else { Color::TRANSPARENT };
2660        let node = cx.container(
2661            LayoutStyle {
2662                padding: Insets::symmetric(10.0, 16.0),
2663                spacing: 2.0,
2664                ..LayoutStyle::stack(Axis::Vertical)
2665            },
2666            &[name, total],
2667            Paint::Quad(QuadPaint { corner_radius: theme.radii.md, ..QuadPaint::new(fill) }),
2668        );
2669        let (selected, index) = (self.selected.clone(), self.index);
2670        cx.on_click(node, Rc::new(move || selected.set(index)));
2671        cx.cursor(node, CursorIcon::Pointer);
2672        cx.focusable(node, theme.radii.md);
2673        cx.semantics(
2674            node,
2675            Semantics::new(Role::RadioButton)
2676                .name(format!("{}: {}", self.name, self.total))
2677                .selected(on)
2678                .toggled(on),
2679        );
2680        node
2681    }
2682}
2683
2684impl<V: View> View for ChartCard<V> {
2685    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2686        cx.child(&Card::new(CardBody(self)))
2687    }
2688}
2689
2690struct CardBody<'a, V>(&'a ChartCard<V>);
2691
2692impl<V: View> View for CardBody<'_, V> {
2693    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2694        let card = self.0;
2695        let theme = *cx.theme();
2696        let (ink, muted) = (theme.color(Semantic::Ink), theme.color(Semantic::InkMuted));
2697        let along = if card.centered { Align::Center } else { Align::Start };
2698        let mut heading = Vec::new();
2699        if !card.title.is_empty() {
2700            heading.push(util::label(cx, &card.title, TextStyle::Headline, Some(Weight::SEMIBOLD), ink));
2701        }
2702        if !card.description.is_empty() {
2703            heading.push(util::label(cx, &card.description, TextStyle::Callout, None, muted));
2704        }
2705        let heading = cx.container(
2706            LayoutStyle {
2707                spacing: 2.0,
2708                grow: if card.centered { 0.0 } else { 1.0 },
2709                min_width: Some(0.0),
2710                align: along,
2711                ..LayoutStyle::stack(Axis::Vertical)
2712            },
2713            &heading,
2714            Paint::None,
2715        );
2716        let mut head = vec![heading];
2717        if let Some(action) = &card.action {
2718            head.push(cx.child(&**action));
2719        }
2720        if let Some((selected, tabs)) = &card.tabs {
2721            for (index, (name, total)) in tabs.iter().enumerate() {
2722                head.push(cx.child(&ChartTab {
2723                    selected: selected.clone(),
2724                    index,
2725                    name: name.clone(),
2726                    total: total.clone(),
2727                }));
2728            }
2729        }
2730        let head = cx.container(
2731            LayoutStyle {
2732                spacing: 12.0,
2733                align: Align::Center,
2734                justify: if card.centered { Justify::Center } else { Justify::Start },
2735                wrap: true,
2736                align_self: Some(Align::Stretch),
2737                ..LayoutStyle::stack(Axis::Horizontal)
2738            },
2739            &head,
2740            Paint::None,
2741        );
2742        let content = cx.child(&card.content);
2743        let mut lines = vec![head, content];
2744        let mut foot = Vec::new();
2745        if let Some((text, icon)) = &card.trend {
2746            let label = util::label(cx, text, TextStyle::Callout, Some(Weight::MEDIUM), ink);
2747            let icon = util::icon(cx, *icon, 16.0, ink);
2748            foot.push(cx.container(
2749                LayoutStyle { spacing: 6.0, align: Align::Center, ..LayoutStyle::stack(Axis::Horizontal) },
2750                &[label, icon],
2751                Paint::None,
2752            ));
2753        }
2754        if let Some(note) = &card.note {
2755            foot.push(util::label(cx, note, TextStyle::Callout, None, muted));
2756        }
2757        if !foot.is_empty() {
2758            lines.push(cx.container(
2759                LayoutStyle {
2760                    spacing: 4.0,
2761                    align: along,
2762                    align_self: Some(Align::Stretch),
2763                    ..LayoutStyle::stack(Axis::Vertical)
2764                },
2765                &foot,
2766                Paint::None,
2767            ));
2768        }
2769        cx.container(
2770            LayoutStyle {
2771                spacing: 16.0,
2772                align: Align::Stretch,
2773                align_self: Some(Align::Stretch),
2774                ..LayoutStyle::stack(Axis::Vertical)
2775            },
2776            &lines,
2777            Paint::None,
2778        )
2779    }
2780}
2781
2782/// A QR Code or a linear barcode, drawn so that every module lands on whole
2783/// pixels. Scanners need dark on light, so it stays that way in dark themes.
2784///
2785/// Text the format cannot hold shows why instead of a code nobody can read.
2786/// Available with the `charts` feature, on by default.
2787///
2788/// ```
2789/// use ravenrs_widgets::{Barcode, Symbology};
2790///
2791/// let link = Barcode::qr("https://example.com").alt("Link to the example site");
2792/// let product = Barcode::new(Symbology::Ean13, "789123456789").size(240.0, 110.0);
2793/// ```
2794#[derive(Clone, Debug)]
2795#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
2796pub struct Barcode {
2797    symbology: Symbology,
2798    data: String,
2799    size: Option<(f32, f32)>,
2800    show_text: bool,
2801    alt: Option<String>,
2802}
2803
2804#[cfg_attr(feature = "live", ravenrs_live::expose)]
2805impl Barcode {
2806    /// A code of this format holding `data`.
2807    pub fn new(symbology: Symbology, data: impl Into<String>) -> Self {
2808        Self { symbology, data: data.into(), size: None, show_text: true, alt: None }
2809    }
2810
2811    /// A QR Code holding `data`.
2812    pub fn qr(data: impl Into<String>) -> Self {
2813        Self::new(Symbology::Qr, data)
2814    }
2815
2816    /// Size in logical pixels (160 by 160 for a QR Code, 240 by 110 for the others).
2817    pub fn size(mut self, width: f32, height: f32) -> Self {
2818        self.size = Some((width, height));
2819        self
2820    }
2821
2822    /// Leaves out the text under the bars of a linear code.
2823    pub fn hide_text(mut self) -> Self {
2824        self.show_text = false;
2825        self
2826    }
2827
2828    /// Describes the code for screen readers, in place of the text it holds.
2829    pub fn alt(mut self, text: impl Into<String>) -> Self {
2830        self.alt = Some(text.into());
2831        self
2832    }
2833}
2834
2835/// The largest box that fits in `room` and holds whole pixels per module.
2836fn whole_modules(room: Rect, across: f32, square: bool, scale: f32) -> Rect {
2837    let side = if square { room.width().min(room.height()) } else { room.width() };
2838    let pixels_per_module = (side * scale / across).floor().max(1.0);
2839    let side = pixels_per_module * across / scale;
2840    let height = if square { side } else { room.height() };
2841    let snap = |value: f32| (value * scale).round() / scale;
2842    Rect::new(
2843        snap(room.x() + (room.width() - side) / 2.0),
2844        snap(room.y() + (room.height() - height) / 2.0),
2845        side,
2846        height,
2847    )
2848}
2849
2850fn barcode_drawing(
2851    pattern: &Pattern,
2852    show_text: bool,
2853    text_size: f32,
2854    radius: f32,
2855    rect: Rect,
2856    scale: f32,
2857) -> ChartDrawing {
2858    // Paper first: the code's own quiet zone needs a light background.
2859    let paper = Shape::filled(vector::rectangle(rect, radius), Color::WHITE);
2860    let mut labels = Vec::new();
2861    let mut room = rect;
2862    if let (Pattern::Linear { text, .. }, true) = (pattern, show_text) {
2863        let height = text_size * LABEL_LINE_HEIGHT + BARCODE_PADDING;
2864        room = Rect::new(rect.x(), rect.y(), rect.width(), (rect.height() - height).max(0.0));
2865        labels.push(ChartLabel {
2866            text: text.clone(),
2867            rect: Rect::new(rect.x(), room.max_y(), rect.width(), height),
2868            align: LabelAlign::Center,
2869            size: text_size,
2870            bold: false,
2871            color: Color::BLACK,
2872        });
2873    }
2874    let square = matches!(pattern, Pattern::Matrix { .. });
2875    if !square {
2876        room = Rect::new(
2877            room.x(),
2878            room.y() + BARCODE_PADDING,
2879            room.width(),
2880            (room.height() - BARCODE_PADDING).max(0.0),
2881        );
2882    }
2883    let across = barcode::modules_across(pattern) as f32;
2884    let code = barcode::shape(pattern, whole_modules(room, across, square, scale), Color::BLACK);
2885    ChartDrawing { shapes: vec![paper, code], labels }
2886}
2887
2888impl View for Barcode {
2889    fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
2890        let theme = *cx.theme();
2891        let pattern = match barcode::encode(self.symbology, &self.data) {
2892            Ok(pattern) => pattern,
2893            Err(problem) => {
2894                let color = theme.color(Semantic::Danger);
2895                return util::label(cx, &problem, TextStyle::Caption, None, color);
2896            }
2897        };
2898        let default = if matches!(pattern, Pattern::Matrix { .. }) { (160.0, 160.0) } else { (240.0, 110.0) };
2899        let (width, height) = self.size.unwrap_or(default);
2900        let style = LayoutStyle { width: Some(width), height: Some(height), ..LayoutStyle::leaf() };
2901        let (show_text, text_size, radius) =
2902            (self.show_text, theme.text(TextStyle::Caption).size, theme.radii.sm);
2903        let drawing: DrawingFn = Rc::new(move |rect, scale, _| {
2904            barcode_drawing(&pattern, show_text, text_size, radius, rect, scale).into()
2905        });
2906        let node = drawing_node(cx, style, drawing);
2907        let name = self.alt.clone().unwrap_or_else(|| self.data.clone());
2908        cx.semantics(node, Semantics::new(Role::Image).name(name));
2909        node
2910    }
2911}