1use super::frame::Bx;
2use super::trend::supports_trendline;
3use super::{
4 Chart, ChartData, ChartKind, Ctx, DOWN, HAlign, LABEL_SIZE, MAX_CATS, MAX_SERIES, MAX_TRENDLINES, Prim, Rgba, SeriesData, TEXT, Theme, UP,
5 VAlign, auto_name, series_color,
6};
7use crate::model::LegendPos;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Swatch {
12 Box,
13 Line,
14 LineMarker,
15 Marker,
16}
17
18pub(super) fn plot_kind(chart: &Chart, s: &SeriesData, i: usize) -> ChartKind {
21 crate::chart::resolve::plot_kind(chart.kind, Some(s.kind), i)
22}
23
24pub(super) fn point_color(data: &ChartData, j: usize) -> Rgba {
26 match (j, data.series.first()) {
27 (0, Some(s)) => s.color,
28 _ => j.checked_rem(data.palette.len()).and_then(|k| data.palette.get(k)).copied().unwrap_or_else(|| series_color(&Theme::default(), j)),
29 }
30}
31
32fn first_len(data: &ChartData) -> usize {
33 data.series.first().map(|s| s.values.len()).unwrap_or(0)
34}
35
36pub(super) fn cat_label(data: &ChartData, j: usize) -> String {
37 data.categories.get(j).cloned().unwrap_or_else(|| (j + 1).to_string())
38}
39
40pub fn legend_entries(chart: &Chart, data: &ChartData) -> Vec<(String, Rgba, Swatch)> {
42 if chart.legend == LegendPos::None {
43 return vec![];
44 }
45 match chart.kind {
46 ChartKind::Pie | ChartKind::Doughnut | ChartKind::Treemap | ChartKind::Sunburst => {
49 let n = first_len(data).min(MAX_CATS);
50 (0..n).map(|j| (cat_label(data, j), point_color(data, j), Swatch::Box)).collect()
51 }
52 ChartKind::Histogram | ChartKind::Funnel | ChartKind::Stock => vec![],
53 ChartKind::Waterfall => {
54 if data.series.is_empty() {
55 return vec![];
56 }
57 let total = data.series.first().map(|s| s.color).unwrap_or(TEXT);
58 vec![("Increase".into(), UP, Swatch::Box), ("Decrease".into(), DOWN, Swatch::Box), ("Total".into(), total, Swatch::Box)]
59 }
60 _ => {
61 let series = data.series.iter().take(MAX_SERIES);
62 let mut out: Vec<(String, Rgba, Swatch)> = series
63 .clone()
64 .enumerate()
65 .map(|(i, s)| {
66 let sw = match plot_kind(chart, s, i) {
67 ChartKind::Line | ChartKind::LineStacked | ChartKind::Radar | ChartKind::ScatterLines => Swatch::Line,
68 ChartKind::LineMarkers => Swatch::LineMarker,
69 ChartKind::Scatter | ChartKind::Bubble => Swatch::Marker,
70 _ => Swatch::Box,
71 };
72 (s.name.clone(), s.color, sw)
73 })
74 .collect();
75 for (_, s) in series.enumerate().filter(|(i, s)| supports_trendline(plot_kind(chart, s, *i))) {
77 for t in s.trendlines.iter().take(MAX_TRENDLINES) {
78 out.push((t.spec.name.clone().unwrap_or_else(|| auto_name(t.spec.kind, &s.name)), t.color, Swatch::Line));
79 }
80 }
81 out
82 }
83 }
84}
85
86fn swatch_w(sw: Swatch, s: f32) -> f32 {
89 if sw == Swatch::Box { s } else { s * 2.0 }
90}
91
92pub(super) fn draw_swatch(ctx: &mut Ctx, sw: Swatch, x: f32, yc: f32, s: f32, color: Rgba) {
93 match sw {
94 Swatch::Box => ctx.rect(x, yc - s / 2.0, s, s, color, None),
95 Swatch::Line => ctx.hline(x, x + 2.0 * s, yc, color, 2.0),
96 Swatch::LineMarker => {
97 ctx.hline(x, x + 2.0 * s, yc, color, 2.0);
98 ctx.out.push(Prim::Circle { cx: x + s, cy: yc, r: s * 0.35, fill: Some(color), stroke: None });
99 }
100 Swatch::Marker => ctx.out.push(Prim::Circle { cx: x + s, cy: yc, r: s * 0.4, fill: Some(color), stroke: None }),
101 }
102}
103
104pub(super) fn draw_legend(ctx: &mut Ctx, entries: &[(String, Rgba, Swatch)], pos: LegendPos, area: Bx) -> Bx {
105 let size = LABEL_SIZE;
106 let row_h = size * 1.6;
107 let s = size * 0.8;
108 let gap = 4.0;
109 let spacing = size * 1.3;
110 let item_w = |ctx: &Ctx, e: &(String, Rgba, Swatch)| swatch_w(e.2, s) + gap + ctx.tw(&e.0, size);
111 match pos {
112 LegendPos::None => area,
113 LegendPos::Top | LegendPos::Bottom => {
114 let max_rows = ((area.h * 0.3 / row_h).floor() as usize).max(1);
115 let mut rows: Vec<Vec<(usize, f32)>> = vec![vec![]];
116 let mut cur = 0.0f32;
117 for (i, e) in entries.iter().enumerate() {
118 let iw = item_w(ctx, e).min(area.w);
119 let need = if cur > 0.0 { spacing + iw } else { iw };
120 if cur > 0.0 && cur + need > area.w {
121 if rows.len() >= max_rows {
122 break;
123 }
124 rows.push(vec![]);
125 cur = 0.0;
126 }
127 cur += if cur > 0.0 { spacing + iw } else { iw };
128 if let Some(r) = rows.last_mut() {
129 r.push((i, iw));
130 }
131 }
132 let lh = rows.len() as f32 * row_h;
133 let y0 = if pos == LegendPos::Bottom { area.bottom() - lh } else { area.y };
134 for (ri, row) in rows.iter().enumerate() {
135 let total: f32 = row.iter().map(|(_, w)| w).sum::<f32>() + spacing * row.len().saturating_sub(1) as f32;
136 let mut x = area.x + (area.w - total).max(0.0) / 2.0;
137 let yc = y0 + ri as f32 * row_h + row_h / 2.0;
138 for &(i, iw) in row {
139 if let Some(e) = entries.get(i) {
140 draw_swatch(ctx, e.2, x, yc, s, e.1);
141 let sx = swatch_w(e.2, s) + gap;
142 let t = ctx.fit(&e.0, size, (iw - sx).max(0.0) + 0.5);
143 ctx.text(x + sx, yc, &t, size, TEXT, (HAlign::Left, VAlign::Middle));
144 }
145 x += iw + spacing;
146 }
147 }
148 let used = lh + 4.0;
149 if pos == LegendPos::Bottom { Bx { h: area.h - used, ..area } } else { Bx { y: area.y + used, h: area.h - used, ..area } }
150 }
151 LegendPos::Left | LegendPos::Right => {
152 let colw = entries.iter().map(|e| item_w(ctx, e)).fold(0.0f32, f32::max).min(area.w * 0.35);
153 let max_rows = ((area.h / row_h).floor() as usize).max(1);
154 let n = entries.len().min(max_rows);
155 let lh = n as f32 * row_h;
156 let y0 = area.y + (area.h - lh).max(0.0) / 2.0;
157 let x0 = if pos == LegendPos::Right { area.right() - colw } else { area.x };
158 for (i, e) in entries.iter().take(n).enumerate() {
159 let yc = y0 + i as f32 * row_h + row_h / 2.0;
160 draw_swatch(ctx, e.2, x0, yc, s, e.1);
161 let sx = swatch_w(e.2, s) + gap;
162 let t = ctx.fit(&e.0, size, (colw - sx).max(0.0) + 0.5);
163 ctx.text(x0 + sx, yc, &t, size, TEXT, (HAlign::Left, VAlign::Middle));
164 }
165 let used = colw + 8.0;
166 if pos == LegendPos::Right { Bx { w: area.w - used, ..area } } else { Bx { x: area.x + used, w: area.w - used, ..area } }
167 }
168 }
169}