Skip to main content

rgpui_component/plot/shape/
line.rs

1// @reference: https://d3js.org/d3-shape/line
2
3use rgpui::{
4    Background, BorderStyle, Bounds, Hsla, PaintQuad, Path, PathBuilder, Pixels, Point, Window, px,
5    quad, size,
6};
7
8use crate::plot::{StrokeStyle, origin_point};
9
10#[allow(clippy::type_complexity)]
11pub struct Line<T> {
12    data: Vec<T>,
13    x: Box<dyn Fn(&T) -> Option<f32>>,
14    y: Box<dyn Fn(&T) -> Option<f32>>,
15    stroke: Background,
16    stroke_width: Pixels,
17    stroke_style: StrokeStyle,
18    dot: bool,
19    dot_size: Pixels,
20    dot_fill_color: Hsla,
21    dot_stroke_color: Option<Hsla>,
22}
23
24impl<T> Default for Line<T> {
25    fn default() -> Self {
26        Self {
27            data: Vec::new(),
28            x: Box::new(|_| None),
29            y: Box::new(|_| None),
30            stroke: Default::default(),
31            stroke_width: px(1.),
32            stroke_style: Default::default(),
33            dot: false,
34            dot_size: px(4.),
35            dot_fill_color: rgpui::transparent_black(),
36            dot_stroke_color: None,
37        }
38    }
39}
40
41impl<T> Line<T> {
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set the data of the Line.
47    pub fn data<I>(mut self, data: I) -> Self
48    where
49        I: IntoIterator<Item = T>,
50    {
51        self.data = data.into_iter().collect();
52        self
53    }
54
55    /// Set the x of the Line.
56    pub fn x<F>(mut self, x: F) -> Self
57    where
58        F: Fn(&T) -> Option<f32> + 'static,
59    {
60        self.x = Box::new(x);
61        self
62    }
63
64    /// Set the y of the Line.
65    pub fn y<F>(mut self, y: F) -> Self
66    where
67        F: Fn(&T) -> Option<f32> + 'static,
68    {
69        self.y = Box::new(y);
70        self
71    }
72
73    /// Set the stroke color of the Line.
74    pub fn stroke(mut self, stroke: impl Into<Background>) -> Self {
75        self.stroke = stroke.into();
76        self
77    }
78
79    /// Set the stroke width of the Line.
80    pub fn stroke_width(mut self, stroke_width: impl Into<Pixels>) -> Self {
81        self.stroke_width = stroke_width.into();
82        self
83    }
84
85    /// Set the stroke style of the Line.
86    pub fn stroke_style(mut self, stroke_style: StrokeStyle) -> Self {
87        self.stroke_style = stroke_style;
88        self
89    }
90
91    /// Show dots on the Line.
92    pub fn dot(mut self) -> Self {
93        self.dot = true;
94        self
95    }
96
97    /// Set the size of the dots on the Line.
98    pub fn dot_size(mut self, dot_size: impl Into<Pixels>) -> Self {
99        self.dot_size = dot_size.into();
100        self
101    }
102
103    /// Set the fill color of the dots on the Line.
104    pub fn dot_fill_color(mut self, dot_fill_color: impl Into<Hsla>) -> Self {
105        self.dot_fill_color = dot_fill_color.into();
106        self
107    }
108
109    /// Set the stroke color of the dots on the Line.
110    pub fn dot_stroke_color(mut self, dot_stroke_color: impl Into<Hsla>) -> Self {
111        self.dot_stroke_color = Some(dot_stroke_color.into());
112        self
113    }
114
115    /// Paint the dots on the Line.
116    fn paint_dot(&self, dot: Point<Pixels>) -> PaintQuad {
117        quad(
118            rgpui::bounds(dot, size(self.dot_size, self.dot_size)),
119            self.dot_size / 2.,
120            self.dot_fill_color,
121            px(1.),
122            self.dot_stroke_color.unwrap_or(self.dot_fill_color),
123            BorderStyle::default(),
124        )
125    }
126
127    fn path(&self, bounds: &Bounds<Pixels>) -> (Option<Path<Pixels>>, Vec<PaintQuad>) {
128        let origin = bounds.origin;
129        let mut builder = PathBuilder::stroke(self.stroke_width);
130        let mut dots = vec![];
131        let mut paint_dots = vec![];
132
133        for v in self.data.iter() {
134            let x_tick = (self.x)(v);
135            let y_tick = (self.y)(v);
136
137            if let (Some(x), Some(y)) = (x_tick, y_tick) {
138                let pos = origin_point(px(x), px(y), origin);
139
140                if self.dot {
141                    let dot_radius = self.dot_size.as_f32() / 2.;
142                    let dot_pos = origin_point(px(x - dot_radius), px(y - dot_radius), origin);
143                    paint_dots.push(self.paint_dot(dot_pos));
144                }
145
146                dots.push(pos);
147            }
148        }
149
150        if dots.is_empty() {
151            return (None, paint_dots);
152        }
153
154        if dots.len() == 1 {
155            builder.move_to(dots[0]);
156            return (builder.build().ok(), paint_dots);
157        }
158
159        match self.stroke_style {
160            StrokeStyle::Natural => {
161                builder.move_to(dots[0]);
162                let n = dots.len();
163                for i in 0..n - 1 {
164                    let p0 = if i == 0 { dots[0] } else { dots[i - 1] };
165                    let p1 = dots[i];
166                    let p2 = dots[i + 1];
167                    let p3 = if i + 2 < n { dots[i + 2] } else { dots[n - 1] };
168
169                    // Catmull-Rom to Bezier
170                    let c1 = Point::new(p1.x + (p2.x - p0.x) / 6.0, p1.y + (p2.y - p0.y) / 6.0);
171                    let c2 = Point::new(p2.x - (p3.x - p1.x) / 6.0, p2.y - (p3.y - p1.y) / 6.0);
172
173                    builder.cubic_bezier_to(p2, c1, c2);
174                }
175            }
176            StrokeStyle::Linear => {
177                builder.move_to(dots[0]);
178                for p in &dots[1..] {
179                    builder.line_to(*p);
180                }
181            }
182            StrokeStyle::StepAfter => {
183                builder.move_to(dots[0]);
184                for (i, p) in dots.windows(2).enumerate() {
185                    builder.line_to(Point::new(p[1].x, p[0].y));
186                    // Don't draw the vertical line for the last point
187                    if i < dots.len() - 2 {
188                        builder.line_to(p[1]);
189                    }
190                }
191            }
192        }
193
194        (builder.build().ok(), paint_dots)
195    }
196
197    /// Paint the Line.
198    pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window) {
199        let (path, dots) = self.path(bounds);
200        if let Some(path) = path {
201            window.paint_path(path, self.stroke);
202        }
203        for dot in dots {
204            window.paint_quad(dot);
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    use rgpui::{Bounds, point, px};
214
215    #[test]
216    fn test_line_path() {
217        let data = vec![1., 2., 3.];
218        let line = Line::new()
219            .data(data.clone())
220            .x(|v| Some(*v))
221            .y(|v| Some(*v * 2.));
222
223        let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(100.)));
224        let (path, dots) = line.path(&bounds);
225
226        assert!(path.is_some());
227        assert!(dots.is_empty());
228
229        let line_with_dots = Line::new()
230            .data(data)
231            .x(|v| Some(*v))
232            .y(|v| Some(*v * 2.))
233            .dot();
234
235        let (_, dots) = line_with_dots.path(&bounds);
236        assert_eq!(dots.len(), 3);
237    }
238}