Skip to main content

ggplot_rs/geom/
line.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::geom::arrow::Arrow;
5use crate::position::identity::PositionIdentity;
6use crate::position::Position;
7use crate::render::backend::{DrawBackend, LineEnd, Linetype};
8use crate::render::RenderError;
9use crate::scale::ScaleSet;
10use crate::stat::identity::StatIdentity;
11use crate::stat::Stat;
12use crate::theme::Theme;
13
14use super::{draw_polyline_with_linewidth, Geom, GeomParams};
15
16/// Line geometry — connects points sorted by x.
17pub struct GeomLine {
18    pub color: (u8, u8, u8),
19    pub width: f64,
20    pub alpha: f64,
21    /// An optional arrowhead drawn at the end(s) of each drawn polyline
22    /// (one head set per group).
23    pub arrow: Option<Arrow>,
24    /// Line cap style (R grid's `lineend`).
25    pub lineend: LineEnd,
26    /// How consecutive points are joined (straight by default; a smooth
27    /// spline through the points with
28    /// [`Interpolate::MonotoneCubic`](super::support::Interpolate::MonotoneCubic)).
29    pub interpolate: super::support::Interpolate,
30}
31
32impl Default for GeomLine {
33    fn default() -> Self {
34        GeomLine {
35            color: (0, 0, 0),
36            width: 1.5,
37            alpha: 1.0,
38            arrow: None,
39            lineend: LineEnd::Butt,
40            interpolate: super::support::Interpolate::Linear,
41        }
42    }
43}
44
45impl GeomLine {
46    /// Fixed stroke colour, used when no `color` aesthetic is mapped.
47    pub fn color(mut self, color: (u8, u8, u8)) -> Self {
48        self.color = color;
49        self
50    }
51
52    /// Sets `width` (the stroke width in pixels).
53    pub fn linewidth(mut self, width: f64) -> Self {
54        self.width = width;
55        self
56    }
57
58    /// Fixed opacity, used when no `alpha` aesthetic is mapped.
59    pub fn alpha(mut self, alpha: f64) -> Self {
60        self.alpha = alpha;
61        self
62    }
63
64    /// Draw an arrowhead at the end(s) — see [`arrow()`](crate::geom::arrow::arrow).
65    pub fn arrow(mut self, arrow: Arrow) -> Self {
66        self.arrow = Some(arrow);
67        self
68    }
69
70    /// Line cap style (R grid's `lineend`).
71    pub fn lineend(mut self, lineend: LineEnd) -> Self {
72        self.lineend = lineend;
73        self
74    }
75}
76
77impl Geom for GeomLine {
78    fn draw(
79        &self,
80        data: &DataFrame,
81        coord: &dyn Coord,
82        scales: &ScaleSet,
83        _theme: &Theme,
84        backend: &mut dyn DrawBackend,
85    ) -> Result<(), RenderError> {
86        let x_col = data
87            .column("x")
88            .ok_or(RenderError::MissingAesthetic("x".into()))?;
89        let y_col = data
90            .column("y")
91            .ok_or(RenderError::MissingAesthetic("y".into()))?;
92        let color_col = data.column("color");
93        let linetype_col = data.column("linetype");
94        let linewidth_col = data.column("linewidth");
95
96        let plot_area = backend.plot_area();
97        let x_scale = scales.get(&Aesthetic::X);
98        let y_scale = scales.get(&Aesthetic::Y);
99
100        // One polyline per group/colour/linetype level — a mapped
101        // `linetype` alone splits the rows too, as in ggplot2 (#220).
102        if color_col.is_some() || linetype_col.is_some() {
103            let groups = super::support::row_groups(data);
104            for indices in &groups {
105                let first_idx = indices[0];
106                let line_color = color_col
107                    .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first_idx]))
108                    .unwrap_or(self.color);
109                // Tag each line with its series (for hover + linked highlighting).
110                let series = color_col
111                    .or(linetype_col)
112                    .map(|c| super::tip_value(&c[first_idx]));
113                super::set_mark(backend, series.clone(), None, series, None);
114                super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
115                super::set_row_extras(backend, data, first_idx);
116
117                let lt = linetype_col
118                    .and_then(|lc| scales.map_linetype(&lc[first_idx]))
119                    .unwrap_or(Linetype::Solid);
120
121                // Sort indices by x value
122                let mut sorted = indices.clone();
123                sorted.sort_by(|&a, &b| {
124                    let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
125                    let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
126                    xa.total_cmp(&xb)
127                });
128
129                let raw: Vec<(f64, f64)> = sorted
130                    .iter()
131                    .map(|&i| {
132                        let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
133                        let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
134                        (nx, ny)
135                    })
136                    .collect();
137                let (raw, origin) = super::support::interpolate(&raw, self.interpolate);
138                let mut points: Vec<(f64, f64)> = raw
139                    .into_iter()
140                    .map(|p| coord.transform(p, &plot_area))
141                    .collect();
142                let mut widths: Vec<f64> = origin
143                    .iter()
144                    .map(|&k| {
145                        linewidth_col
146                            .and_then(|lc| scales.map_linewidth(&lc[sorted[k]]))
147                            .unwrap_or(self.width)
148                    })
149                    .collect();
150                close_for_radar(coord, &mut points);
151                if points.len() > widths.len() {
152                    widths.push(widths[0]);
153                }
154
155                draw_polyline_with_linewidth(
156                    backend,
157                    &points,
158                    &widths,
159                    line_color,
160                    self.alpha,
161                    lt,
162                    self.lineend,
163                    self.arrow.as_ref(),
164                    self.width,
165                )?;
166            }
167            super::clear_mark(backend);
168            super::clear_row_extras(backend);
169        } else {
170            let lt = linetype_col
171                .and_then(|lc| {
172                    if lc.is_empty() {
173                        None
174                    } else {
175                        scales.map_linetype(&lc[0])
176                    }
177                })
178                .unwrap_or(Linetype::Solid);
179
180            // Sort by x value
181            let mut sorted_indices: Vec<usize> = (0..data.nrows()).collect();
182            sorted_indices.sort_by(|&a, &b| {
183                let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
184                let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
185                xa.total_cmp(&xb)
186            });
187
188            let raw: Vec<(f64, f64)> = sorted_indices
189                .iter()
190                .map(|&i| {
191                    let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
192                    let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
193                    (nx, ny)
194                })
195                .collect();
196            let (raw, origin) = super::support::interpolate(&raw, self.interpolate);
197            let mut points: Vec<(f64, f64)> = raw
198                .into_iter()
199                .map(|p| coord.transform(p, &plot_area))
200                .collect();
201            let mut widths: Vec<f64> = origin
202                .iter()
203                .map(|&k| {
204                    linewidth_col
205                        .and_then(|lc| scales.map_linewidth(&lc[sorted_indices[k]]))
206                        .unwrap_or(self.width)
207                })
208                .collect();
209            close_for_radar(coord, &mut points);
210            if points.len() > widths.len() {
211                widths.push(widths[0]);
212            }
213
214            if data.nrows() > 0 {
215                super::set_row_extras(backend, data, 0);
216            }
217            draw_polyline_with_linewidth(
218                backend,
219                &points,
220                &widths,
221                self.color,
222                self.alpha,
223                lt,
224                self.lineend,
225                self.arrow.as_ref(),
226                self.width,
227            )?;
228            super::clear_row_extras(backend);
229        }
230
231        Ok(())
232    }
233
234    fn required_aes(&self) -> Vec<Aesthetic> {
235        vec![Aesthetic::X, Aesthetic::Y]
236    }
237
238    fn default_stat(&self) -> Box<dyn Stat> {
239        Box::new(StatIdentity)
240    }
241
242    fn default_position(&self) -> Box<dyn Position> {
243        Box::new(PositionIdentity)
244    }
245
246    fn default_params(&self) -> GeomParams {
247        GeomParams::default()
248    }
249
250    fn name(&self) -> &str {
251        "line"
252    }
253
254    fn consumes_tooltips(&self) -> bool {
255        true
256    }
257
258    fn keeps_na_as_break(&self) -> bool {
259        true
260    }
261
262    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
263        if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
264            self.color = ink;
265        }
266    }
267
268    fn set_series_color(&mut self, color: (u8, u8, u8)) {
269        self.color = color;
270    }
271}
272
273/// Under `coord_radar` a series is a closed loop around the spokes.
274fn close_for_radar(coord: &dyn Coord, points: &mut Vec<(f64, f64)>) {
275    if coord.is_radar() && points.len() >= 3 {
276        let first = points[0];
277        points.push(first);
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use crate::data::Value;
284    use crate::geom::arrow::arrow;
285    use crate::prelude::*;
286
287    fn plot() -> GGPlot {
288        let n = 6;
289        let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
290        let y: Vec<Value> = (0..n)
291            .map(|i| Value::Float((i as f64 * 0.37).sin()))
292            .collect();
293        GGPlot::new(vec![("x".into(), x), ("y".into(), y)]).aes(Aes::new().x("x").y("y"))
294    }
295
296    #[test]
297    fn default_line_output_has_no_linecap_or_arrow_marks() {
298        let svg = plot()
299            .geom_line_with(GeomLine::default())
300            .render_svg_native_with_size(100, 60)
301            .unwrap();
302        assert!(!svg.contains("stroke-linecap"));
303        assert!(!svg.contains("data-part"));
304    }
305
306    #[test]
307    fn line_with_arrow_tags_one_head_for_the_single_polyline() {
308        let svg = plot()
309            .geom_line_with(GeomLine::default().arrow(arrow().closed()))
310            .render_svg_native_with_size(100, 60)
311            .unwrap();
312        assert_eq!(svg.matches("data-part=\"arrow\"").count(), 1);
313    }
314
315    #[test]
316    fn lineend_round_gives_stroke_linecap_round() {
317        let svg = plot()
318            .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
319            .render_svg_native_with_size(100, 60)
320            .unwrap();
321        assert!(svg.contains("stroke-linecap=\"round\""));
322    }
323
324    /// `.color()`/`.alpha()`/`.linewidth()` builders actually change the
325    /// rendered stroke (the ungrouped — no `color` aesthetic mapped — line).
326    #[test]
327    fn color_alpha_and_linewidth_builders_change_the_rendered_stroke() {
328        let svg = plot()
329            .geom_line_with(
330                GeomLine::default()
331                    .color((10, 20, 30))
332                    .alpha(0.4)
333                    .linewidth(5.0),
334            )
335            .render_svg_native_with_size(100, 60)
336            .unwrap();
337        assert!(svg.contains("stroke=\"#0A141E\""));
338        assert!(svg.contains("stroke-opacity=\"0.400\""));
339        assert!(svg.contains("stroke-width=\"5.00\""));
340    }
341
342    /// The grouped branch (a `color` aesthetic mapped): one polyline per
343    /// group, each tagged with its series for hover grouping.
344    #[test]
345    fn grouped_line_draws_one_polyline_per_colour_group_with_series_tags() {
346        let svg = GGPlot::new(vec![
347            (
348                "x".into(),
349                vec![
350                    Value::Float(0.0),
351                    Value::Float(1.0),
352                    Value::Float(0.0),
353                    Value::Float(1.0),
354                ],
355            ),
356            (
357                "y".into(),
358                vec![
359                    Value::Float(0.0),
360                    Value::Float(1.0),
361                    Value::Float(1.0),
362                    Value::Float(0.0),
363                ],
364            ),
365            (
366                "g".into(),
367                vec![
368                    Value::Str("a".into()),
369                    Value::Str("a".into()),
370                    Value::Str("b".into()),
371                    Value::Str("b".into()),
372                ],
373            ),
374        ])
375        .aes(Aes::new().x("x").y("y").color("g"))
376        .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
377        .render_svg_native_with_size(100, 100)
378        .unwrap();
379
380        assert_eq!(svg.matches("stroke-linecap=\"round\"").count(), 2);
381        assert!(svg.contains("data-series=\"a\""));
382        assert!(svg.contains("data-series=\"b\""));
383    }
384
385    /// `draw_polyline_with_linewidth` (B1-4): when the mapped `linewidth`
386    /// varies within a group, a single stroked polyline can't represent
387    /// that — it falls back to drawing segment-by-segment, one
388    /// `<polyline>` per consecutive point pair, each at its own width.
389    #[test]
390    fn varying_linewidth_within_a_group_draws_segment_by_segment() {
391        let svg = GGPlot::new(vec![
392            (
393                "x".into(),
394                vec![Value::Float(0.0), Value::Float(1.0), Value::Float(2.0)],
395            ),
396            (
397                "y".into(),
398                vec![Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)],
399            ),
400            (
401                "w".into(),
402                vec![Value::Float(1.0), Value::Float(5.0), Value::Float(9.0)],
403            ),
404        ])
405        .aes(Aes::new().x("x").y("y").linewidth("w"))
406        .geom_line()
407        .scale_linewidth(ScaleLinewidthContinuous::new().with_range(2.0, 9.0))
408        .render_svg_native_with_size(100, 100)
409        .unwrap();
410
411        // 3 points -> 2 segments, each its own 2-point polyline at the
412        // width mapped for its *starting* point (1.0 -> width 1, 5.0 ->
413        // width 5) — not one 3-point polyline at a single width.
414        assert_eq!(svg.matches("stroke-width=\"2.00\"").count(), 1);
415        assert_eq!(svg.matches("stroke-width=\"5.50\"").count(), 1);
416        // The last point's mapped width (9) is never used as a stroke
417        // width here — there's no arrow to resolve it for.
418        assert!(!svg.contains("stroke-width=\"9.00\""));
419    }
420}