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