use crate::aes::Aesthetic;
use crate::data::{DataFrame, Value};
use crate::position::DODGE_OFFSET_COL;
use crate::scale::{Scale, ScaleSet};
pub(crate) fn row_groups(data: &DataFrame) -> Vec<Vec<usize>> {
let cols: Vec<&[Value]> = ["group", "color", "fill", "linetype"]
.iter()
.filter_map(|c| data.column(c))
.collect();
let n = data.nrows();
if cols.is_empty() {
return if n == 0 {
vec![]
} else {
vec![(0..n).collect()]
};
}
let mut groups: indexmap::IndexMap<Vec<crate::data::GroupKey<'_>>, Vec<usize>> =
indexmap::IndexMap::new();
for i in 0..n {
let key = cols.iter().map(|c| c[i].group_key()).collect();
groups.entry(key).or_default().push(i);
}
groups.into_values().collect()
}
pub(crate) struct XMapper<'a> {
scale: Option<&'a dyn Scale>,
offsets: Option<&'a [Value]>,
band: f64,
}
impl<'a> XMapper<'a> {
pub(crate) fn new(data: &'a DataFrame, scales: &'a ScaleSet) -> Self {
let scale = scales.get(&Aesthetic::X);
let offsets = data.column(DODGE_OFFSET_COL);
let band = match (scale, offsets) {
(Some(s), Some(_)) if s.is_discrete() => 1.0 / s.breaks().len().max(1) as f64,
_ => 0.0,
};
XMapper {
scale,
offsets,
band,
}
}
pub(crate) fn map(&self, value: &Value, i: usize) -> f64 {
let base = self.scale.map(|s| s.map(value)).unwrap_or(0.0);
let off = self
.offsets
.and_then(|o| o.get(i))
.and_then(|v| v.as_f64())
.filter(|v| v.is_finite())
.unwrap_or(0.0);
base + off * self.band
}
}
pub(crate) fn mark_interval(
backend: &mut dyn crate::render::backend::DrawBackend,
data: &DataFrame,
i: usize,
x: &Value,
lo: &Value,
hi: &Value,
) {
let series = super::series_key(data, i);
let y = data.column("y").and_then(|c| c.get(i));
let range = format!("[{}, {}]", super::tip_value(lo), super::tip_value(hi));
let body = match y {
Some(y) => format!("{}: {} {range}", super::tip_value(x), super::tip_value(y)),
None => format!("{}: {range}", super::tip_value(x)),
};
let tip = Some(match &series {
Some(s) => format!("{s}: {body}"),
None => body,
});
let value = match y {
Some(y) => super::raw_value(y),
None => super::raw_value(lo)
.zip(super::raw_value(hi))
.map(|(a, b)| format!("{a} {b}")),
};
super::set_mark(backend, tip, Some(super::tip_value(x)), series, value);
}
pub(crate) fn map_position(scale: Option<&dyn Scale>, value: &Value) -> Option<f64> {
let s = scale?;
if s.is_discrete() {
if let Some(f) = value.as_f64() {
let n = s.breaks().len().max(1) as f64;
return Some((f - 0.5) / n);
}
}
Some(s.map(value))
}
pub(crate) fn clip_segment_unit(a: (f64, f64), b: (f64, f64)) -> Option<((f64, f64), (f64, f64))> {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let mut t0: f64 = 0.0;
let mut t1: f64 = 1.0;
for (p, q) in [(-dx, a.0), (dx, 1.0 - a.0), (-dy, a.1), (dy, 1.0 - a.1)] {
if p.abs() < 1e-15 {
if q < 0.0 {
return None;
}
} else {
let r = q / p;
if p < 0.0 {
t0 = t0.max(r);
} else {
t1 = t1.min(r);
}
}
}
if t0 > t1 || !(t0.is_finite() && t1.is_finite()) {
return None;
}
Some((
(a.0 + t0 * dx, a.1 + t0 * dy),
(a.0 + t1 * dx, a.1 + t1 * dy),
))
}
pub(crate) fn clip_polyline_unit(pts: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
let mut runs: Vec<Vec<(f64, f64)>> = Vec::new();
let mut cur: Vec<(f64, f64)> = Vec::new();
for w in pts.windows(2) {
let (a, b) = (w[0], w[1]);
let finite = a.0.is_finite() && a.1.is_finite() && b.0.is_finite() && b.1.is_finite();
match clip_segment_unit(a, b).filter(|_| finite) {
Some((p, q)) => {
let joins = cur
.last()
.is_some_and(|l| (l.0 - p.0).abs() < 1e-12 && (l.1 - p.1).abs() < 1e-12);
if !joins {
if cur.len() >= 2 {
runs.push(std::mem::take(&mut cur));
}
cur.clear();
cur.push(p);
}
cur.push(q);
}
None => {
if cur.len() >= 2 {
runs.push(std::mem::take(&mut cur));
}
cur.clear();
}
}
}
if cur.len() >= 2 {
runs.push(cur);
}
runs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clip_inside_segment_is_unchanged() {
let (p, q) = clip_segment_unit((0.1, 0.2), (0.8, 0.9)).unwrap();
for (got, want) in [(p.0, 0.1), (p.1, 0.2), (q.0, 0.8), (q.1, 0.9)] {
assert!((got - want).abs() < 1e-12);
}
}
#[test]
fn clip_crossing_segment() {
let (p, q) = clip_segment_unit((-1.0, 0.5), (2.0, 0.5)).unwrap();
assert!((p.0 - 0.0).abs() < 1e-12 && (q.0 - 1.0).abs() < 1e-12);
assert!(clip_segment_unit((-1.0, 2.0), (2.0, 2.0)).is_none());
}
#[test]
fn clip_polyline_splits_runs() {
let pts = [(0.1, 0.5), (0.3, 0.5), (0.5, 1.5), (0.7, 0.5), (0.9, 0.5)];
let runs = clip_polyline_unit(&pts);
assert_eq!(runs.len(), 2);
for r in &runs {
assert!(r
.iter()
.all(|(x, y)| (0.0..=1.0).contains(x) && (0.0..=1.0).contains(y)));
}
}
#[test]
fn clip_polyline_breaks_on_nan() {
let pts = [
(0.1, 0.1),
(0.2, 0.2),
(f64::NAN, 0.3),
(0.4, 0.4),
(0.5, 0.5),
];
let runs = clip_polyline_unit(&pts);
assert_eq!(runs.len(), 2);
}
#[test]
fn row_groups_by_colour() {
let mut df = DataFrame::new();
df.add_column(
"color".into(),
vec!["a", "b", "a"]
.into_iter()
.map(|s| Value::Str(s.into()))
.collect(),
);
assert_eq!(row_groups(&df), vec![vec![0, 2], vec![1]]);
let mut plain = DataFrame::new();
plain.add_column("x".into(), vec![Value::Float(1.0); 3]);
assert_eq!(row_groups(&plain), vec![vec![0, 1, 2]]);
}
}