orbital_charts/engine/
curve.rs1use crate::CurveType;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct PlotPoint {
8 pub x: f64,
10 pub y: Option<f64>,
12}
13
14#[derive(Clone, Debug, PartialEq)]
16pub struct LinePath {
17 pub d: String,
19 pub markers: Vec<(f64, f64)>,
21}
22
23pub fn build_line_path(points: &[PlotPoint], curve: CurveType, connect_nulls: bool) -> LinePath {
25 let segments = split_segments(points, connect_nulls);
26 let mut d = String::new();
27 let mut markers = Vec::new();
28
29 for segment in segments {
30 if segment.is_empty() {
31 continue;
32 }
33 let coords: Vec<(f64, f64)> = segment
34 .iter()
35 .filter_map(|p| p.y.map(|y| (p.x, y)))
36 .collect();
37 if coords.is_empty() {
38 continue;
39 }
40 markers.extend(coords.iter().copied());
41 let segment_d = match curve {
42 CurveType::Linear => linear_path(&coords),
43 CurveType::Step => step_path(&coords),
44 CurveType::Monotone => monotone_path(&coords),
45 CurveType::Natural => natural_path(&coords),
46 };
47 if !d.is_empty() && !segment_d.is_empty() {
48 d.push(' ');
49 }
50 d.push_str(&segment_d);
51 }
52
53 LinePath { d, markers }
54}
55
56pub fn build_area_path(line: &LinePath, baseline_y: f64) -> String {
58 if line.d.is_empty() || line.markers.is_empty() {
59 return String::new();
60 }
61 let first = line.markers[0];
62 let last = line.markers[line.markers.len() - 1];
63 format!(
64 "{line_d} L {last_x} {base} L {first_x} {base} Z",
65 line_d = line.d,
66 last_x = last.0,
67 base = baseline_y,
68 first_x = first.0,
69 )
70}
71
72fn split_segments(points: &[PlotPoint], connect_nulls: bool) -> Vec<Vec<PlotPoint>> {
73 if connect_nulls {
74 return vec![points.to_vec()];
75 }
76 let mut segments = Vec::new();
77 let mut current = Vec::new();
78 for p in points {
79 if p.y.is_none() {
80 if !current.is_empty() {
81 segments.push(current);
82 current = Vec::new();
83 }
84 } else {
85 current.push(*p);
86 }
87 }
88 if !current.is_empty() {
89 segments.push(current);
90 }
91 segments
92}
93
94fn linear_path(coords: &[(f64, f64)]) -> String {
95 let mut d = String::new();
96 for (i, (x, y)) in coords.iter().enumerate() {
97 if i == 0 {
98 d.push_str(&format!("M {x} {y}"));
99 } else {
100 d.push_str(&format!(" L {x} {y}"));
101 }
102 }
103 d
104}
105
106fn step_path(coords: &[(f64, f64)]) -> String {
107 let mut d = String::new();
108 for (i, (x, y)) in coords.iter().enumerate() {
109 if i == 0 {
110 d.push_str(&format!("M {x} {y}"));
111 } else {
112 d.push_str(&format!(" H {x} V {y}"));
113 }
114 }
115 d
116}
117
118fn monotone_path(coords: &[(f64, f64)]) -> String {
119 if coords.len() < 2 {
120 return linear_path(coords);
121 }
122 let mut d = format!("M {} {}", coords[0].0, coords[0].1);
123 for i in 0..coords.len() - 1 {
124 let (x0, y0) = coords[i];
125 let (x1, y1) = coords[i + 1];
126 let cx = (x0 + x1) / 2.0;
127 d.push_str(&format!(" C {cx} {y0}, {cx} {y1}, {x1} {y1}"));
128 }
129 d
130}
131
132fn natural_path(coords: &[(f64, f64)]) -> String {
133 monotone_path(coords)
134}
135
136pub fn data_fingerprint(values: &[f64]) -> u64 {
138 use std::hash::{Hash, Hasher};
139 let mut hasher = std::collections::hash_map::DefaultHasher::new();
140 values.len().hash(&mut hasher);
141 for v in values {
142 v.to_bits().hash(&mut hasher);
143 }
144 hasher.finish()
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn null_breaks_path_without_connect_nulls() {
153 let points = vec![
154 PlotPoint {
155 x: 0.0,
156 y: Some(1.0),
157 },
158 PlotPoint { x: 1.0, y: None },
159 PlotPoint {
160 x: 2.0,
161 y: Some(3.0),
162 },
163 ];
164 let path = build_line_path(&points, CurveType::Linear, false);
165 assert_eq!(path.markers.len(), 2);
166 }
167
168 #[test]
169 fn connect_nulls_keeps_single_segment() {
170 let points = vec![
171 PlotPoint {
172 x: 0.0,
173 y: Some(1.0),
174 },
175 PlotPoint { x: 1.0, y: None },
176 PlotPoint {
177 x: 2.0,
178 y: Some(3.0),
179 },
180 ];
181 let path = build_line_path(&points, CurveType::Linear, true);
182 assert!(path.d.starts_with('M'));
183 }
184
185 #[test]
186 fn area_path_closes_to_baseline() {
187 let line = build_line_path(
188 &[
189 PlotPoint {
190 x: 0.0,
191 y: Some(10.0),
192 },
193 PlotPoint {
194 x: 50.0,
195 y: Some(20.0),
196 },
197 ],
198 CurveType::Linear,
199 false,
200 );
201 let area = build_area_path(&line, 100.0);
202 assert!(area.ends_with('Z'));
203 }
204}