1use crate::aes::Aesthetic;
2use crate::data::{DataFrame, Value};
3use crate::scale::ScaleSet;
4
5use super::Stat;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum Connection {
10 #[default]
12 Hv,
13 Vh,
15 Mid,
17 Linear,
19}
20
21#[derive(Clone, Debug, Default, PartialEq)]
24pub struct StatConnect {
25 pub connection: Connection,
26}
27
28impl StatConnect {
29 pub fn new(connection: Connection) -> Self {
30 StatConnect { connection }
31 }
32}
33
34impl Stat for StatConnect {
35 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
36 let (Some(xc), Some(yc)) = (data.column("x"), data.column("y")) else {
37 return DataFrame::new();
38 };
39 let mut idx: Vec<usize> = (0..data.nrows())
40 .filter(|&i| xc[i].as_f64().is_some() && yc[i].as_f64().is_some())
41 .collect();
42 idx.sort_by(|&a, &b| xc[a].as_f64().unwrap().total_cmp(&xc[b].as_f64().unwrap()));
43 let mut pts: Vec<(usize, f64, f64)> = Vec::new();
45 for (k, &i) in idx.iter().enumerate() {
46 let (x, y) = (xc[i].as_f64().unwrap(), yc[i].as_f64().unwrap());
47 if let Some(&prev) = k.checked_sub(1).map(|p| &idx[p]) {
48 let (px, py) = (xc[prev].as_f64().unwrap(), yc[prev].as_f64().unwrap());
49 match self.connection {
50 Connection::Hv => pts.push((prev, x, py)),
51 Connection::Vh => pts.push((prev, px, y)),
52 Connection::Mid => {
53 let m = (px + x) / 2.0;
54 pts.push((prev, m, py));
55 pts.push((prev, m, y));
56 }
57 Connection::Linear => {}
58 }
59 }
60 let interior = k > 0 && k + 1 < idx.len();
63 if !(self.connection == Connection::Mid && interior) {
64 pts.push((i, x, y));
65 }
66 }
67 let mut out = DataFrame::new();
68 for name in data.column_names() {
69 let src = data.column(name).unwrap();
70 let col: Vec<Value> = match name {
71 "x" => pts.iter().map(|p| Value::Float(p.1)).collect(),
72 "y" => pts.iter().map(|p| Value::Float(p.2)).collect(),
73 _ => pts.iter().map(|p| src[p.0].clone()).collect(),
74 };
75 out.add_column(name.to_string(), col);
76 }
77 out
78 }
79
80 fn required_aes(&self) -> Vec<Aesthetic> {
81 vec![Aesthetic::X, Aesthetic::Y]
82 }
83
84 fn name(&self) -> &str {
85 "connect"
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 fn run(c: Connection) -> Vec<(f64, f64)> {
94 let mut df = DataFrame::new();
95 df.add_column(
96 "x".into(),
97 vec![Value::Float(3.0), Value::Float(1.0), Value::Float(2.0)],
98 );
99 df.add_column(
100 "y".into(),
101 vec![Value::Float(30.0), Value::Float(10.0), Value::Float(20.0)],
102 );
103 let out = StatConnect::new(c).compute_group(&df, &ScaleSet::new());
104 let x = out.column("x").unwrap();
105 let y = out.column("y").unwrap();
106 (0..out.nrows())
107 .map(|i| (x[i].as_f64().unwrap(), y[i].as_f64().unwrap()))
108 .collect()
109 }
110
111 #[test]
112 fn connections_match_ggplot2() {
113 assert_eq!(
117 run(Connection::Hv),
118 [
119 (1.0, 10.0),
120 (2.0, 10.0),
121 (2.0, 20.0),
122 (3.0, 20.0),
123 (3.0, 30.0)
124 ]
125 );
126 assert_eq!(
127 run(Connection::Vh),
128 [
129 (1.0, 10.0),
130 (1.0, 20.0),
131 (2.0, 20.0),
132 (2.0, 30.0),
133 (3.0, 30.0)
134 ]
135 );
136 assert_eq!(
137 run(Connection::Mid),
138 [
139 (1.0, 10.0),
140 (1.5, 10.0),
141 (1.5, 20.0),
142 (2.5, 20.0),
143 (2.5, 30.0),
144 (3.0, 30.0)
145 ]
146 );
147 assert_eq!(
148 run(Connection::Linear),
149 [(1.0, 10.0), (2.0, 20.0), (3.0, 30.0)]
150 );
151 }
152}