1use crate::aes::Aesthetic;
6use crate::data::{DataFrame, Value};
7use crate::position::DODGE_OFFSET_COL;
8use crate::scale::{Scale, ScaleSet};
9
10pub(crate) fn row_groups(data: &DataFrame) -> Vec<Vec<usize>> {
14 let cols: Vec<&[Value]> = ["group", "color", "fill", "linetype"]
15 .iter()
16 .filter_map(|c| data.column(c))
17 .collect();
18 let n = data.nrows();
19 if cols.is_empty() {
20 return if n == 0 {
21 vec![]
22 } else {
23 vec![(0..n).collect()]
24 };
25 }
26 let mut groups: indexmap::IndexMap<Vec<crate::data::GroupKey<'_>>, Vec<usize>> =
27 indexmap::IndexMap::new();
28 for i in 0..n {
29 let key = cols.iter().map(|c| c[i].group_key()).collect();
30 groups.entry(key).or_default().push(i);
31 }
32 groups.into_values().collect()
33}
34
35pub(crate) struct XMapper<'a> {
39 scale: Option<&'a dyn Scale>,
40 offsets: Option<&'a [Value]>,
41 counts: Option<&'a [Value]>,
42 band: f64,
43}
44
45impl<'a> XMapper<'a> {
46 pub(crate) fn new(data: &'a DataFrame, scales: &'a ScaleSet) -> Self {
47 let scale = scales.get(&Aesthetic::X);
48 let offsets = data.column(DODGE_OFFSET_COL);
49 let band = match (scale, offsets) {
50 (Some(s), Some(_)) if s.is_discrete() => 1.0 / s.breaks().len().max(1) as f64,
51 _ => 0.0,
52 };
53 XMapper {
54 scale,
55 offsets,
56 counts: data.column(crate::position::DODGE_N_COL),
57 band,
58 }
59 }
60
61 pub(crate) fn slot_n(&self, i: usize) -> f64 {
64 if self.band == 0.0 {
65 return 1.0;
66 }
67 self.counts
68 .and_then(|c| c.get(i))
69 .and_then(|v| v.as_f64())
70 .filter(|n| n.is_finite() && *n >= 1.0)
71 .unwrap_or(1.0)
72 }
73
74 pub(crate) fn map(&self, value: &Value, i: usize) -> f64 {
76 let base = self.scale.map(|s| s.map(value)).unwrap_or(0.0);
77 let off = self
78 .offsets
79 .and_then(|o| o.get(i))
80 .and_then(|v| v.as_f64())
81 .filter(|v| v.is_finite())
82 .unwrap_or(0.0);
83 base + off * self.band
84 }
85}
86
87pub(crate) fn mark_interval(
91 backend: &mut dyn crate::render::backend::DrawBackend,
92 scales: &crate::scale::ScaleSet,
93 data: &DataFrame,
94 i: usize,
95 x: &Value,
96 lo: &Value,
97 hi: &Value,
98) {
99 let series = super::series_key(data, i);
100 let y = data.column("y").and_then(|c| c.get(i));
101 let range = format!("[{}, {}]", super::tip_value(lo), super::tip_value(hi));
102 let body = match y {
103 Some(y) => format!("{}: {} {range}", super::tip_value(x), super::tip_value(y)),
104 None => format!("{}: {range}", super::tip_value(x)),
105 };
106 let tip = Some(match &series {
107 Some(s) => format!("{s}: {body}"),
108 None => body,
109 });
110 let value = match y {
111 Some(y) => super::raw_value(y),
112 None => super::raw_value(lo)
113 .zip(super::raw_value(hi))
114 .map(|(a, b)| format!("{a} {b}")),
115 };
116 super::set_mark(backend, tip, Some(super::tip_value(x)), series, value);
117 super::set_mark_formatted(backend, scales, Some(x), &crate::aes::Aesthetic::Y, y);
121}
122
123pub(crate) fn map_position(scale: Option<&dyn Scale>, value: &Value) -> Option<f64> {
127 let s = scale?;
128 if s.is_discrete() {
129 if let Some(f) = value.as_f64() {
130 let n = s.breaks().len().max(1) as f64;
131 return Some((f - 0.5) / n);
132 }
133 }
134 Some(s.map(value))
135}
136
137pub(crate) fn clip_segment_unit(a: (f64, f64), b: (f64, f64)) -> Option<((f64, f64), (f64, f64))> {
140 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
141 let mut t0: f64 = 0.0;
142 let mut t1: f64 = 1.0;
143 for (p, q) in [(-dx, a.0), (dx, 1.0 - a.0), (-dy, a.1), (dy, 1.0 - a.1)] {
144 if p.abs() < 1e-15 {
145 if q < 0.0 {
146 return None;
147 }
148 } else {
149 let r = q / p;
150 if p < 0.0 {
151 t0 = t0.max(r);
152 } else {
153 t1 = t1.min(r);
154 }
155 }
156 }
157 if t0 > t1 || !(t0.is_finite() && t1.is_finite()) {
158 return None;
159 }
160 Some((
161 (a.0 + t0 * dx, a.1 + t0 * dy),
162 (a.0 + t1 * dx, a.1 + t1 * dy),
163 ))
164}
165
166pub(crate) fn clip_polyline_unit(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
170 let mut runs: Vec<Vec<(f64, f64)>> = Vec::new();
171 let mut cur: Vec<(f64, f64)> = Vec::new();
172 for w in pts.windows(2) {
173 let (a, b) = (w[0], w[1]);
174 let finite = a.0.is_finite() && a.1.is_finite() && b.0.is_finite() && b.1.is_finite();
175 match clip_segment_unit(a, b).filter(|_| finite) {
176 Some((p, q)) => {
177 let joins = cur
178 .last()
179 .is_some_and(|l| (l.0 - p.0).abs() < 1e-12 && (l.1 - p.1).abs() < 1e-12);
180 if !joins {
181 if cur.len() >= 2 {
182 runs.push(std::mem::take(&mut cur));
183 }
184 cur.clear();
185 cur.push(p);
186 }
187 cur.push(q);
188 }
189 None => {
190 if cur.len() >= 2 {
191 runs.push(std::mem::take(&mut cur));
192 }
193 cur.clear();
194 }
195 }
196 }
197 if cur.len() >= 2 {
198 runs.push(cur);
199 }
200 runs
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn clip_inside_segment_is_unchanged() {
209 let (p, q) = clip_segment_unit((0.1, 0.2), (0.8, 0.9)).unwrap();
210 for (got, want) in [(p.0, 0.1), (p.1, 0.2), (q.0, 0.8), (q.1, 0.9)] {
211 assert!((got - want).abs() < 1e-12);
212 }
213 }
214
215 #[test]
216 fn clip_crossing_segment() {
217 let (p, q) = clip_segment_unit((-1.0, 0.5), (2.0, 0.5)).unwrap();
218 assert!((p.0 - 0.0).abs() < 1e-12 && (q.0 - 1.0).abs() < 1e-12);
219 assert!(clip_segment_unit((-1.0, 2.0), (2.0, 2.0)).is_none());
220 }
221
222 #[test]
223 fn clip_polyline_splits_runs() {
224 let pts = [(0.1, 0.5), (0.3, 0.5), (0.5, 1.5), (0.7, 0.5), (0.9, 0.5)];
226 let runs = clip_polyline_unit(&pts);
227 assert_eq!(runs.len(), 2);
228 for r in &runs {
229 assert!(r
230 .iter()
231 .all(|(x, y)| (0.0..=1.0).contains(x) && (0.0..=1.0).contains(y)));
232 }
233 }
234
235 #[test]
236 fn clip_polyline_breaks_on_nan() {
237 let pts = [
238 (0.1, 0.1),
239 (0.2, 0.2),
240 (f64::NAN, 0.3),
241 (0.4, 0.4),
242 (0.5, 0.5),
243 ];
244 let runs = clip_polyline_unit(&pts);
245 assert_eq!(runs.len(), 2);
246 }
247
248 #[test]
249 fn row_groups_by_colour() {
250 let mut df = DataFrame::new();
251 df.add_column(
252 "color".into(),
253 vec!["a", "b", "a"]
254 .into_iter()
255 .map(|s| Value::Str(s.into()))
256 .collect(),
257 );
258 assert_eq!(row_groups(&df), vec![vec![0, 2], vec![1]]);
259 let mut plain = DataFrame::new();
260 plain.add_column("x".into(), vec![Value::Float(1.0); 3]);
261 assert_eq!(row_groups(&plain), vec![vec![0, 1, 2]]);
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
268pub enum Interpolate {
269 #[default]
271 Linear,
272 MonotoneCubic,
276 CatmullRom,
278}
279
280const CURVE_STEPS: usize = 16;
282
283pub(crate) fn interpolate(
288 points: &[(f64, f64)],
289 method: Interpolate,
290) -> (Vec<(f64, f64)>, Vec<usize>) {
291 if method == Interpolate::Linear || points.len() < 3 {
292 return (points.to_vec(), (0..points.len()).collect());
293 }
294 let mut out = Vec::with_capacity(points.len() * CURVE_STEPS);
295 let mut origin = Vec::with_capacity(points.len() * CURVE_STEPS);
296 let finite = |p: &(f64, f64)| p.0.is_finite() && p.1.is_finite();
297 let mut start = 0;
298 while start < points.len() {
299 if !finite(&points[start]) {
300 out.push(points[start]);
301 origin.push(start);
302 start += 1;
303 continue;
304 }
305 let mut end = start;
306 while end < points.len() && finite(&points[end]) {
307 end += 1;
308 }
309 let run = &points[start..end];
310 let curve = match method {
311 Interpolate::MonotoneCubic => monotone_run(run),
312 _ => catmull_rom_run(run),
313 };
314 for (p, k) in curve {
315 out.push(p);
316 origin.push(start + k);
317 }
318 start = end;
319 }
320 (out, origin)
321}
322
323fn slope3(p0: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
325 let (h0, h1) = (p1.0 - p0.0, p2.0 - p1.0);
326 if h0 <= 0.0 || h1 <= 0.0 {
327 return 0.0;
328 }
329 let (s0, s1) = ((p1.1 - p0.1) / h0, (p2.1 - p1.1) / h1);
330 let p = (s0 * h1 + s1 * h0) / (h0 + h1);
331 (s0.signum() + s1.signum()) * s0.abs().min(s1.abs()).min(0.5 * p.abs())
332}
333
334fn monotone_run(run: &[(f64, f64)]) -> Vec<((f64, f64), usize)> {
335 let n = run.len();
336 if n < 3 {
337 return run.iter().enumerate().map(|(k, &p)| (p, k)).collect();
338 }
339 let mut m = vec![0.0; n];
340 for k in 1..n - 1 {
341 m[k] = slope3(run[k - 1], run[k], run[k + 1]);
342 }
343 let end_slope = |a: (f64, f64), b: (f64, f64), t: f64| {
345 let h = b.0 - a.0;
346 if h > 0.0 {
347 (3.0 * (b.1 - a.1) / h - t) / 2.0
348 } else {
349 t
350 }
351 };
352 m[0] = end_slope(run[0], run[1], m[1]);
353 m[n - 1] = end_slope(run[n - 2], run[n - 1], m[n - 2]);
354 let mut out = vec![(run[0], 0)];
355 for k in 0..n - 1 {
356 let ((x0, y0), (x1, y1)) = (run[k], run[k + 1]);
357 let h = x1 - x0;
358 if h <= 0.0 {
359 out.push((run[k + 1], k));
360 continue;
361 }
362 for s in 1..=CURVE_STEPS {
363 let t = s as f64 / CURVE_STEPS as f64;
364 let (t2, t3) = (t * t, t * t * t);
365 let y = (2.0 * t3 - 3.0 * t2 + 1.0) * y0
366 + (t3 - 2.0 * t2 + t) * h * m[k]
367 + (-2.0 * t3 + 3.0 * t2) * y1
368 + (t3 - t2) * h * m[k + 1];
369 out.push(((x0 + h * t, y), k));
370 }
371 }
372 out
373}
374
375fn catmull_rom_run(run: &[(f64, f64)]) -> Vec<((f64, f64), usize)> {
376 let n = run.len();
377 if n < 3 {
378 return run.iter().enumerate().map(|(k, &p)| (p, k)).collect();
379 }
380 let at = |i: isize| run[i.clamp(0, n as isize - 1) as usize];
381 let mut out = vec![(run[0], 0)];
382 for k in 0..n - 1 {
383 let (p0, p1, p2, p3) = (
384 at(k as isize - 1),
385 at(k as isize),
386 at(k as isize + 1),
387 at(k as isize + 2),
388 );
389 for s in 1..=CURVE_STEPS {
390 let t = s as f64 / CURVE_STEPS as f64;
391 let (t2, t3) = (t * t, t * t * t);
392 let c = |a: f64, b: f64, c: f64, d: f64| {
393 0.5 * (2.0 * b
394 + (-a + c) * t
395 + (2.0 * a - 5.0 * b + 4.0 * c - d) * t2
396 + (-a + 3.0 * b - 3.0 * c + d) * t3)
397 };
398 out.push(((c(p0.0, p1.0, p2.0, p3.0), c(p0.1, p1.1, p2.1, p3.1)), k));
399 }
400 }
401 out
402}
403
404#[cfg(test)]
405mod interpolate_tests {
406 use super::*;
407
408 #[test]
409 fn monotone_passes_through_points_without_overshoot() {
410 let pts = [(0.0, 0.0), (1.0, 1.0), (2.0, 1.0), (3.0, 5.0), (4.0, 0.0)];
411 let (out, origin) = interpolate(&pts, Interpolate::MonotoneCubic);
412 assert_eq!(out.len(), 1 + 4 * CURVE_STEPS);
413 assert_eq!(origin.len(), out.len());
414 for p in pts {
415 assert!(out
416 .iter()
417 .any(|q| (q.0 - p.0).abs() < 1e-12 && (q.1 - p.1).abs() < 1e-12));
418 }
419 for (q, &k) in out.iter().zip(&origin) {
421 let (a, b) = (pts[k].1, pts[k + 1].1);
422 assert!(
423 q.1 >= a.min(b) - 1e-9 && q.1 <= a.max(b) + 1e-9,
424 "{q:?} on {k}"
425 );
426 }
427 }
428
429 #[test]
430 fn catmull_rom_passes_through_points_and_breaks_on_nan() {
431 let pts = [
432 (0.0, 0.0),
433 (1.0, 2.0),
434 (2.0, 0.0),
435 (f64::NAN, f64::NAN),
436 (3.0, 1.0),
437 (4.0, 3.0),
438 (5.0, 2.0),
439 ];
440 let (out, origin) = interpolate(&pts, Interpolate::CatmullRom);
441 assert_eq!(out.iter().filter(|p| p.0.is_nan()).count(), 1);
442 assert!(out
443 .iter()
444 .any(|q| (q.0 - 1.0).abs() < 1e-12 && (q.1 - 2.0).abs() < 1e-12));
445 assert_eq!(origin[out.iter().position(|p| p.0.is_nan()).unwrap()], 3);
446 let (lin, _) = interpolate(&pts, Interpolate::Linear);
447 assert_eq!(lin.len(), pts.len());
448 }
449}