1use 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
37const CHANGE: Transition = Transition::tween(0.75, Easing::Bezier(CubicBezier::new(0.25, 1.0, 0.5, 1.0)));
40const MAX_BAR_WIDTH: f32 = 44.0;
42const LABEL_LINE_HEIGHT: f32 = 1.3;
43const 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;
52const ACTIVE_HALO: f32 = 5.0;
54const HALO_ALPHA: f32 = 0.22;
56const FOCUS_DIM: f32 = 0.45;
58const SLICE_LIFT: f32 = 6.0;
61const LIFT_SOLID_FROM: f32 = 4.0;
63const CURSOR_ALPHA: f32 = 0.55;
65const DOT_ICON: f32 = 18.0;
66const CANDLE_OUTLINE: f32 = 1.5;
67
68type DrawingFn = Rc<dyn Fn(Rect, f32, plot::Measure<'_>) -> Plot>;
71pub(crate) type ValueFormat = Rc<dyn Fn(f64) -> String>;
72type TooltipContent = Rc<dyn Fn(usize, &str) -> AnyView>;
74type ValueColor = Rc<dyn Fn(usize, f64) -> Option<Semantic>>;
76type TickFormat = Rc<dyn Fn(&str) -> String>;
77type ChartTabs = (Binding<usize>, Vec<(String, String)>);
79
80const PICTURES_KEPT_BYTES: usize = 128 << 20;
83
84struct KeptPicture {
86 layers: Vec<vector::Layer>,
87 size: (u32, u32),
88 image: Option<ImageData>,
89 bytes: usize,
90}
91
92thread_local! {
93 static PICTURES: RefCell<Vec<KeptPicture>> = const { RefCell::new(Vec::new()) };
96}
97
98fn 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
115fn 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 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#[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 widths: HashMap<u32, HashMap<String, f32>>,
150 parts: Option<Parts>,
153}
154
155#[derive(Default)]
158struct Parts {
159 backdrop: Option<(Rect, ImageData)>,
160 data: Option<(Rect, ImageData)>,
161 notes: Option<(Rect, ImageData)>,
162}
163
164const WIDTHS_KEPT: usize = 1 << 15;
167
168#[cfg_attr(feature = "live", ravenrs_live::expose)]
169impl Picture {
170 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 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#[derive(Clone, Copy, Debug, PartialEq)]
255enum DrawIn {
256 Reveal,
258 Grow { horizontal: bool },
260 Sweep { separator: f32 },
262 Spread,
264}
265
266#[derive(Clone, Copy, Debug)]
268struct Appear {
269 how: DrawIn,
270 progress: f32,
271}
272
273const LABELS_COME_IN: f32 = 0.7;
276const REVEAL_BLEED: f32 = ACTIVE_DOT;
279
280pub(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
286fn 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
298fn swept(from: f32, to: f32, start: f32, t: f32) -> (f32, f32) {
300 (start + (from - start) * t, start + (to - start) * t)
301}
302
303pub(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
313fn 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
341fn 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
366fn 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
388fn paint_drawing_in(appear: Appear, picture: &mut Picture, frame: Rect, cx: &mut PaintCx<'_>) {
390 let t = appear.progress.clamp(0.0, 1.0);
391 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 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#[derive(Clone)]
477struct Highlight {
478 at: f32,
480 focus: f32,
482 dim: f32,
484 lifts: Vec<f32>,
486 separator: f32,
488 strip: bool,
490 line: bool,
492 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 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 fn opacity(&self) -> f32 {
513 1.0 - self.dim * self.focus
514 }
515}
516
517fn 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
583fn paint_bars(highlight: Option<&Highlight>, bars: &[plot::Bar], frame: Rect, cx: &mut PaintCx<'_>) {
586 for bar in bars {
587 let shape = bar.rect;
588 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
613fn 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
646fn 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 let outer = if sector.inner >= outermost { sector.outer + SLICE_LIFT * lift } else { sector.outer };
656 let path = plot::gapped_slice(
658 sector.center,
659 sector.inner,
660 outer,
661 sector.from,
662 sector.to,
663 highlight.separator,
664 );
665 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
707fn 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
757fn 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
763pub(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
773pub(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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
788#[non_exhaustive]
789#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
790pub enum ChartIndicator {
791 #[default]
793 Dot,
794 Line,
796 Dashed,
798 None,
800}
801
802#[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 pub fn new() -> Self {
839 Self::default()
840 }
841
842 pub fn indicator(mut self, indicator: ChartIndicator) -> Self {
844 self.indicator = indicator;
845 self
846 }
847
848 pub fn hide_label(mut self) -> Self {
850 self.hide_label = true;
851 self
852 }
853
854 pub fn label(mut self, label: impl Into<String>) -> Self {
856 self.label = Some(label.into());
857 self
858 }
859
860 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 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 pub fn show_total(mut self, name: impl Into<String>) -> Self {
874 self.total = Some(name.into());
875 self
876 }
877
878 pub fn icons(mut self) -> Self {
880 self.icons = true;
881 self
882 }
883
884 pub fn show_percent(mut self) -> Self {
887 self.percent = true;
888 self
889 }
890
891 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 pub fn no_indicator_for(mut self, series: impl Into<String>) -> Self {
901 self.no_indicator.push(series.into());
902 self
903 }
904
905 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 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
926pub(crate) struct KeyRow {
928 pub(crate) name: String,
929 color: Color,
930 icon: Option<IconName>,
931 value: Option<f64>,
932 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
945fn 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 cx.semantics(node, Semantics::hidden());
1066 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#[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 pub fn new(value: f64) -> Self {
1104 Self { value, label: None, color: None }
1105 }
1106
1107 pub fn label(mut self, label: impl Into<String>) -> Self {
1109 self.label = Some(label.into());
1110 self
1111 }
1112
1113 pub fn color(mut self, color: Semantic) -> Self {
1115 self.color = Some(color);
1116 self
1117 }
1118}
1119
1120#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1122#[non_exhaustive]
1123#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
1124pub enum ChartBarFrom {
1125 #[default]
1127 Bottom,
1128 Top,
1130 Left,
1132 Right,
1134}
1135
1136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1138#[non_exhaustive]
1139#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
1140pub enum ChartLegend {
1141 #[default]
1143 Bottom,
1144 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#[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 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#[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 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 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 pub fn positions(mut self, positions: impl IntoIterator<Item = f64>) -> Self {
1386 self.positions = positions.into_iter().collect();
1387 self
1388 }
1389
1390 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 pub fn candles(mut self, candles: impl IntoIterator<Item = [f64; 4]>) -> Self {
1414 let candles: Vec<[f64; 4]> = candles.into_iter().collect();
1415 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 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 pub fn title(mut self, title: impl Into<String>) -> Self {
1447 self.title = title.into();
1448 self
1449 }
1450
1451 pub fn hide_legend(mut self) -> Self {
1453 self.legend = false;
1454 self
1455 }
1456
1457 pub fn format(mut self, format: impl Fn(f64) -> String + 'static) -> Self {
1459 self.format = Rc::new(format);
1460 self
1461 }
1462
1463 pub fn height(mut self, height: f32) -> Self {
1465 self.height = height;
1466 self
1467 }
1468
1469 pub fn size(mut self, width: f32, height: f32) -> Self {
1471 self.width = Some(width);
1472 self.height = height;
1473 self
1474 }
1475
1476 pub fn alt(mut self, text: impl Into<String>) -> Self {
1479 self.alt = Some(text.into());
1480 self
1481 }
1482
1483 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 pub fn curve(mut self, curve: ChartCurve) -> Self {
1502 self.look.curve = curve;
1503 self
1504 }
1505
1506 pub fn stack(mut self, stack: ChartStack) -> Self {
1508 self.look.stack = stack;
1509 self
1510 }
1511
1512 pub fn gradient(mut self) -> Self {
1515 self.look.gradient = true;
1516 self
1517 }
1518
1519 pub fn fill_opacity(mut self, opacity: f32) -> Self {
1521 self.look.fill_opacity = Some(opacity);
1522 self
1523 }
1524
1525 pub fn horizontal(mut self) -> Self {
1527 self.look.horizontal = true;
1528 self
1529 }
1530
1531 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 pub fn min_bar_length(mut self, length: f32) -> Self {
1541 self.look.min_bar = Some(length);
1542 self
1543 }
1544
1545 pub fn labels_at_zero(mut self) -> Self {
1548 self.look.labels_at_zero = true;
1549 self
1550 }
1551
1552 pub fn bar_radius(mut self, radius: f32) -> Self {
1554 self.look.bar_radius = Some(radius);
1555 self
1556 }
1557
1558 pub fn vary_colors(mut self) -> Self {
1560 self.vary_colors = true;
1561 self
1562 }
1563
1564 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 pub fn negative_color(mut self, color: Semantic) -> Self {
1572 self.negative = Some(color);
1573 self
1574 }
1575
1576 pub fn active(mut self, index: Option<usize>) -> Self {
1578 self.look.active = index;
1579 self
1580 }
1581
1582 pub fn dots(mut self) -> Self {
1584 self.look.dots = true;
1585 self
1586 }
1587
1588 pub fn dot_icon(mut self, icon: IconName) -> Self {
1590 self.dot_icon = Some(icon);
1591 self
1592 }
1593
1594 pub fn value_labels(mut self, at: ChartLabelAt) -> Self {
1596 self.look.value_labels = Some(at);
1597 self
1598 }
1599
1600 pub fn category_labels(mut self, at: ChartLabelAt) -> Self {
1602 self.look.category_labels = Some(at);
1603 self
1604 }
1605
1606 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 pub fn grid(mut self, grid: ChartGrid) -> Self {
1614 self.look.grid = grid;
1615 self
1616 }
1617
1618 pub fn grid_dashed(mut self) -> Self {
1620 self.look.grid_dashed = true;
1621 self
1622 }
1623
1624 pub fn grid_columns(mut self, columns: usize) -> Self {
1627 self.look.grid_columns = Some(columns);
1628 self
1629 }
1630
1631 pub fn value_ticks(mut self, ticks: usize) -> Self {
1634 self.look.value_ticks = Some(ticks);
1635 self
1636 }
1637
1638 pub fn align_axis_ends(mut self) -> Self {
1642 self.look.ends_aligned = true;
1643 self
1644 }
1645
1646 pub fn value_domain(mut self, min: f64, max: f64) -> Self {
1649 self.look.domain = Some((min, max));
1650 self
1651 }
1652
1653 pub fn reference_line(self, value: f64) -> Self {
1656 self.reference(ChartReference::new(value))
1657 }
1658
1659 pub fn reference(mut self, reference: ChartReference) -> Self {
1661 self.look.references.push(reference);
1662 self
1663 }
1664
1665 pub fn hide_category_axis(mut self) -> Self {
1667 self.look.category_axis = false;
1668 self
1669 }
1670
1671 pub fn show_value_axis(mut self) -> Self {
1673 self.look.value_axis = true;
1674 self
1675 }
1676
1677 pub fn tight_scale(mut self) -> Self {
1679 self.look.tight_scale = true;
1680 self
1681 }
1682
1683 pub fn no_separator(mut self) -> Self {
1685 self.look.no_separator = true;
1686 self
1687 }
1688
1689 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 pub fn label_guides(mut self) -> Self {
1700 self.look.label_guides = true;
1701 self
1702 }
1703
1704 pub fn hole(mut self, share: f32) -> Self {
1706 self.look.hole = Some(share);
1707 self
1708 }
1709
1710 pub fn concentric(mut self) -> Self {
1712 self.look.concentric = true;
1713 self
1714 }
1715
1716 pub fn polar_grid(mut self, grid: ChartPolarGrid) -> Self {
1718 self.look.polar_grid = Some(grid);
1719 self
1720 }
1721
1722 pub fn polar_fill(mut self) -> Self {
1724 self.look.polar_fill = true;
1725 self
1726 }
1727
1728 pub fn no_spokes(mut self) -> Self {
1730 self.look.spokes = false;
1731 self
1732 }
1733
1734 pub fn rings(mut self, rings: usize) -> Self {
1736 self.look.rings = rings;
1737 self
1738 }
1739
1740 pub fn lines_only(mut self) -> Self {
1742 self.look.lines_only = true;
1743 self
1744 }
1745
1746 pub fn radius_axis(mut self) -> Self {
1748 self.look.radius_axis = true;
1749 self
1750 }
1751
1752 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 pub fn tracks(mut self) -> Self {
1762 self.look.tracks = true;
1763 self
1764 }
1765
1766 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 pub fn tooltip(mut self, tooltip: ChartTooltip) -> Self {
1776 self.tooltip = Some(tooltip);
1777 self.tooltip_asked = true;
1778 self
1779 }
1780
1781 pub fn hide_tooltip(mut self) -> Self {
1783 self.tooltip = None;
1784 self
1785 }
1786
1787 pub fn no_hover_focus(mut self) -> Self {
1791 self.hover_focus = false;
1792 self
1793 }
1794
1795 pub fn no_hover_motion(mut self) -> Self {
1798 self.hover_motion = false;
1799 self
1800 }
1801
1802 pub fn no_appear(mut self) -> Self {
1805 self.appear = false;
1806 self
1807 }
1808
1809 pub fn animate_changes(mut self) -> Self {
1816 self.animate_changes = true;
1817 self
1818 }
1819
1820 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 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 pub fn legend_at(mut self, at: ChartLegend) -> Self {
1840 self.legend_at = at;
1841 self
1842 }
1843
1844 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 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 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 fn is_plain(&self) -> bool {
1907 matches!(self.kind, ChartKind::Scatter | ChartKind::Sparkline | ChartKind::SparkArea)
1908 }
1909
1910 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 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 fn tooltip_panel(
2040 &self,
2041 tooltip: &ChartTooltip,
2042 index: usize,
2043 colors: &[Color],
2044 theme: &Theme,
2045 labels: &crate::Labels,
2046 ) -> TooltipPanel {
2047 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 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 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 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 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 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 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 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 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 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 if appear.is_none() {
2363 cx.motion(node, Motion::new().enter(Effect::hidden(), m.base));
2364 }
2365
2366 let mut parts = Vec::new();
2367 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 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 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 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#[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 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 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#[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 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 pub fn height(mut self, height: f32) -> Self {
2531 self.height = height;
2532 self
2533 }
2534
2535 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
2552pub 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 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 pub fn title(mut self, title: impl Into<String>) -> Self {
2599 self.title = title.into();
2600 self
2601 }
2602
2603 pub fn description(mut self, description: impl Into<String>) -> Self {
2605 self.description = description.into();
2606 self
2607 }
2608
2609 pub fn action(mut self, action: impl View + 'static) -> Self {
2611 self.action = Some(Rc::new(action));
2612 self
2613 }
2614
2615 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 pub fn trend(mut self, text: impl Into<String>, icon: IconName) -> Self {
2628 self.trend = Some((text.into(), icon));
2629 self
2630 }
2631
2632 pub fn note(mut self, text: impl Into<String>) -> Self {
2634 self.note = Some(text.into());
2635 self
2636 }
2637
2638 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#[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 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 pub fn qr(data: impl Into<String>) -> Self {
2813 Self::new(Symbology::Qr, data)
2814 }
2815
2816 pub fn size(mut self, width: f32, height: f32) -> Self {
2818 self.size = Some((width, height));
2819 self
2820 }
2821
2822 pub fn hide_text(mut self) -> Self {
2824 self.show_text = false;
2825 self
2826 }
2827
2828 pub fn alt(mut self, text: impl Into<String>) -> Self {
2830 self.alt = Some(text.into());
2831 self
2832 }
2833}
2834
2835fn 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 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}