1use std::f32::consts::{PI, TAU};
4
5use crate::model::{Chart, ChartKind, LineDash, QuartileMethod, Theme, TrendlineKind};
6
7use crate::chart::axis::{Fmt, Scale, clean, nice_scale};
8use crate::chart::resolve::{MAX_POINTS, MAX_SERIES, MAX_TRENDLINES, series_color};
9use crate::chart::trend::{auto_name, fit_trendline, moving_average};
10use crate::chart::{ChartData, HAlign, Measure, Prim, Rgba, SeriesData, VAlign};
11
12mod cartesian;
13mod frame;
14mod kinds;
15mod legend;
16mod lines;
17mod table;
18mod trend;
19
20use cartesian::{cartesian, scatter};
21use frame::{Bx, axis_titles, chart_title};
22use kinds::{box_whisker, doughnut, funnel, histogram, pie, radar, stock, treemap, waterfall};
23use legend::draw_legend;
24pub use legend::{Swatch, legend_entries};
25pub use trend::supports_trendline;
26
27pub(crate) const TEXT: Rgba = [0x59, 0x59, 0x59, 0xFF];
28const DARK_TEXT: Rgba = [0x26, 0x26, 0x26, 0xFF];
29const GRID: Rgba = [0xD9, 0xD9, 0xD9, 0xFF];
30const AXIS: Rgba = [0xBF, 0xBF, 0xBF, 0xFF];
31const WHITE: Rgba = [0xFF, 0xFF, 0xFF, 0xFF];
32const STOCK: Rgba = [0x40, 0x40, 0x40, 0xFF];
33pub(crate) const UP: Rgba = [0x3E, 0x9B, 0x5F, 0xFF];
34pub(crate) const DOWN: Rgba = [0xD6, 0x4F, 0x45, 0xFF];
35
36const TITLE_SIZE: f32 = 14.0;
37const LABEL_SIZE: f32 = 9.0;
38const AXIS_TITLE_SIZE: f32 = 10.0;
39const MAX_CATS: usize = 20_000;
41const MAX_LINE_PTS: usize = 2000;
43const MAX_MARKERS: usize = 5000;
45const MAX_LABELS: usize = 200;
47pub(crate) const MAX_WEDGES: usize = 2_048;
52
53struct Ctx<'a> {
54 out: Vec<Prim>,
55 m: &'a dyn Measure,
56}
57
58impl Ctx<'_> {
59 fn tw(&self, s: &str, size: f32) -> f32 {
60 let w = self.m.text_width(s, size, false);
61 if w.is_finite() { w.max(0.0) } else { 0.0 }
62 }
63 fn fit(&self, s: &str, size: f32, maxw: f32) -> String {
64 if self.tw(s, size) <= maxw {
65 return s.to_string();
66 }
67 let chars: Vec<char> = s.chars().collect();
68 let mut n = chars.len().min(400);
69 while n > 0 {
70 n -= 1;
71 let t: String = chars.iter().take(n).collect::<String>() + "…";
72 if self.tw(&t, size) <= maxw {
73 return if n == 0 { String::new() } else { t };
74 }
75 }
76 String::new()
77 }
78 fn text(&mut self, x: f32, y: f32, s: &str, size: f32, color: Rgba, (align, valign): (HAlign, VAlign)) {
79 if s.is_empty() {
80 return;
81 }
82 self.out.push(Prim::Text { x, y, text: s.to_string(), size, color, bold: false, align, valign, rotation: 0.0 });
83 }
84 fn rect(&mut self, x: f32, y: f32, w: f32, h: f32, fill: Rgba, stroke: Option<(Rgba, f32)>) {
85 self.out.push(Prim::Rect { x, y, w, h, fill: Some(fill), stroke, radius: 0.0 });
86 }
87 fn line(&mut self, pts: Vec<[f32; 2]>, color: Rgba, width: f32) {
88 if pts.len() >= 2 {
89 self.out.push(Prim::Line { pts, color, width, dash: false });
90 }
91 }
92 fn hline(&mut self, x0: f32, x1: f32, y: f32, color: Rgba, width: f32) {
93 self.line(vec![[x0, y], [x1, y]], color, width);
94 }
95 fn vline(&mut self, x: f32, y0: f32, y1: f32, color: Rgba, width: f32) {
96 self.line(vec![[x, y0], [x, y1]], color, width);
97 }
98}
99
100fn sanitize_vals(v: &[Option<f64>]) -> Vec<Option<f64>> {
101 v.iter().take(MAX_POINTS).map(|x| x.filter(|n| n.is_finite()).map(clean)).collect()
102}
103
104fn sanitize(d: &ChartData) -> ChartData {
105 ChartData {
106 palette: d.palette.iter().take(MAX_POINTS).copied().collect(),
107 categories: d.categories.iter().take(MAX_POINTS).cloned().collect(),
108 category_levels: d.category_levels.iter().take(3).map(|l| l.iter().take(MAX_POINTS).cloned().collect()).collect(),
109 series: d
110 .series
111 .iter()
112 .take(MAX_SERIES)
113 .map(|s| SeriesData {
114 name: s.name.clone(),
115 values: sanitize_vals(&s.values),
116 x: s.x.as_deref().map(sanitize_vals),
117 sizes: s.sizes.as_deref().map(sanitize_vals),
118 color: s.color,
119 kind: s.kind,
120 secondary: s.secondary,
121 smooth: s.smooth,
122 trendlines: s.trendlines.iter().take(MAX_TRENDLINES).cloned().collect(),
123 number_format: s.number_format.clone(),
124 })
125 .collect(),
126 }
127}
128
129pub fn render(chart: &Chart, data: &ChartData, w: f32, h: f32, m: &dyn Measure) -> Vec<Prim> {
131 if !(w.is_finite() && h.is_finite()) || w < 1.0 || h < 1.0 || w > 1e7 || h > 1e7 {
132 return vec![];
133 }
134 let data = sanitize(data);
135 let mut ctx = Ctx { out: Vec::new(), m };
136 ctx.out.push(Prim::Rect { x: 0.0, y: 0.0, w, h, fill: Some(WHITE), stroke: Some((GRID, 1.0)), radius: 0.0 });
137 let pad = (w.min(h) * 0.04).clamp(2.0, 10.0);
138 let mut area = Bx { x: pad, y: pad, w: w - 2.0 * pad, h: h - 2.0 * pad };
139 if let Some(t) = chart_title(chart, &data) {
140 let size = TITLE_SIZE.min(h * 0.12).max(4.0);
141 let t = ctx.fit(&t, size, area.w);
142 ctx.text(w / 2.0, area.y, &t, size, TEXT, (HAlign::Center, VAlign::Top));
143 let used = size * 1.3 + 4.0;
144 area.y += used;
145 area.h -= used;
146 }
147 let entries = legend_entries(chart, &data);
148 if !entries.is_empty() && area.ok() {
149 area = draw_legend(&mut ctx, &entries, chart.legend, area);
150 }
151 if area.ok() && !data.series.is_empty() {
152 draw_kind(&mut ctx, chart, &data, area);
153 }
154 finalize(ctx.out, w, h)
155}
156
157fn draw_kind(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) {
158 use ChartKind::*;
159 match chart.kind {
160 Pie => pie(ctx, chart, data, area, 0.0, false),
161 Doughnut => doughnut(ctx, chart, data, area),
162 Sunburst => pie(ctx, chart, data, area, 0.2, true),
163 Treemap => treemap(ctx, chart, data, area),
164 Funnel => funnel(ctx, chart, data, area),
165 Radar => radar(ctx, chart, data, area),
166 _ => {
167 let area = axis_titles(ctx, chart, area);
168 if !area.ok() {
169 return;
170 }
171 match chart.kind {
172 BarClustered | BarStacked | BarStacked100 => cartesian(ctx, chart, data, area, true),
173 Scatter | ScatterLines | Bubble => scatter(ctx, chart, data, area),
174 Histogram => histogram(ctx, chart, data, area),
175 Waterfall => waterfall(ctx, chart, data, area),
176 BoxWhisker => box_whisker(ctx, chart, data, area),
177 Stock => stock(ctx, chart, data, area),
178 _ => cartesian(ctx, chart, data, area, false),
179 }
180 }
181 }
182}
183
184fn fin(v: f32) -> bool {
187 v.is_finite()
188}
189
190fn clamp_stroke(s: Option<(Rgba, f32)>) -> Option<(Rgba, f32)> {
191 s.filter(|(_, w)| fin(*w) && *w > 0.0).map(|(c, w)| (c, w.min(50.0)))
192}
193
194pub(crate) fn finalize(prims: Vec<Prim>, w: f32, h: f32) -> Vec<Prim> {
196 let cx = |v: f32| v.clamp(0.0, w);
197 let cy = |v: f32| v.clamp(0.0, h);
198 let pts_ok = |pts: &[[f32; 2]]| pts.iter().all(|p| fin(p[0]) && fin(p[1]));
199 prims
200 .into_iter()
201 .filter_map(|p| match p {
202 Prim::Rect { x, y, w: rw, h: rh, fill, stroke, radius } => {
203 if !(fin(x) && fin(y) && fin(rw) && fin(rh)) {
204 return None;
205 }
206 let (x0, x1) = (cx(x.min(x + rw)), cx(x.max(x + rw)));
207 let (y0, y1) = (cy(y.min(y + rh)), cy(y.max(y + rh)));
208 if x1 - x0 <= 0.0 || y1 - y0 <= 0.0 {
209 return None;
210 }
211 let radius = if fin(radius) { radius.clamp(0.0, (x1 - x0).min(y1 - y0) / 2.0) } else { 0.0 };
212 Some(Prim::Rect { x: x0, y: y0, w: x1 - x0, h: y1 - y0, fill, stroke: clamp_stroke(stroke), radius })
213 }
214 Prim::Line { pts, color, width, dash } => {
215 if !pts_ok(&pts) || pts.len() < 2 || !fin(width) {
216 return None;
217 }
218 let pts = pts.into_iter().map(|p| [cx(p[0]), cy(p[1])]).collect();
219 Some(Prim::Line { pts, color, width: width.clamp(0.1, 50.0), dash })
220 }
221 Prim::Polygon { pts, fill, stroke } => {
222 if !pts_ok(&pts) || pts.len() < 3 {
223 return None;
224 }
225 let pts = pts.into_iter().map(|p| [cx(p[0]), cy(p[1])]).collect();
226 Some(Prim::Polygon { pts, fill, stroke: clamp_stroke(stroke) })
227 }
228 Prim::Wedge { cx: x, cy: y, r_outer, r_inner, a0, a1, fill, stroke } => {
229 if ![x, y, r_outer, r_inner, a0, a1].iter().all(|v| fin(*v)) {
230 return None;
231 }
232 let (x, y) = (cx(x), cy(y));
233 let maxr = x.min(w - x).min(y).min(h - y);
234 let ro = r_outer.min(maxr);
235 if ro <= 0.0 {
236 return None;
237 }
238 Some(Prim::Wedge { cx: x, cy: y, r_outer: ro, r_inner: r_inner.clamp(0.0, ro), a0, a1, fill, stroke: clamp_stroke(stroke) })
239 }
240 Prim::Circle { cx: x, cy: y, r, fill, stroke } => {
241 if !(fin(x) && fin(y) && fin(r)) {
242 return None;
243 }
244 let (x, y) = (cx(x), cy(y));
245 let r = r.min(x.min(w - x).min(y).min(h - y));
246 if r <= 0.0 {
247 return None;
248 }
249 Some(Prim::Circle { cx: x, cy: y, r, fill, stroke: clamp_stroke(stroke) })
250 }
251 Prim::Text { x, y, text, size, color, bold, align, valign, rotation } => {
252 if !(fin(x) && fin(y) && fin(size) && fin(rotation)) || text.is_empty() {
253 return None;
254 }
255 Some(Prim::Text { x: cx(x), y: cy(y), text, size: size.clamp(1.0, 200.0), color, bold, align, valign, rotation })
256 }
257 })
258 .collect()
259}