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
16pub struct GeomLine {
18 pub color: (u8, u8, u8),
19 pub width: f64,
20 pub alpha: f64,
21 pub arrow: Option<Arrow>,
24 pub lineend: LineEnd,
26}
27
28impl Default for GeomLine {
29 fn default() -> Self {
30 GeomLine {
31 color: (0, 0, 0),
32 width: 1.5,
33 alpha: 1.0,
34 arrow: None,
35 lineend: LineEnd::Butt,
36 }
37 }
38}
39
40impl GeomLine {
41 pub fn color(mut self, color: (u8, u8, u8)) -> Self {
43 self.color = color;
44 self
45 }
46
47 pub fn linewidth(mut self, width: f64) -> Self {
49 self.width = width;
50 self
51 }
52
53 pub fn alpha(mut self, alpha: f64) -> Self {
55 self.alpha = alpha;
56 self
57 }
58
59 pub fn arrow(mut self, arrow: Arrow) -> Self {
61 self.arrow = Some(arrow);
62 self
63 }
64
65 pub fn lineend(mut self, lineend: LineEnd) -> Self {
67 self.lineend = lineend;
68 self
69 }
70}
71
72impl Geom for GeomLine {
73 fn draw(
74 &self,
75 data: &DataFrame,
76 coord: &dyn Coord,
77 scales: &ScaleSet,
78 _theme: &Theme,
79 backend: &mut dyn DrawBackend,
80 ) -> Result<(), RenderError> {
81 let x_col = data
82 .column("x")
83 .ok_or(RenderError::MissingAesthetic("x".into()))?;
84 let y_col = data
85 .column("y")
86 .ok_or(RenderError::MissingAesthetic("y".into()))?;
87 let color_col = data.column("color");
88 let linetype_col = data.column("linetype");
89 let linewidth_col = data.column("linewidth");
90
91 let plot_area = backend.plot_area();
92 let x_scale = scales.get(&Aesthetic::X);
93 let y_scale = scales.get(&Aesthetic::Y);
94
95 if color_col.is_some() || linetype_col.is_some() {
98 let groups = super::support::row_groups(data);
99 for indices in &groups {
100 let first_idx = indices[0];
101 let line_color = color_col
102 .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[first_idx]))
103 .unwrap_or(self.color);
104 let series = color_col
106 .or(linetype_col)
107 .map(|c| super::tip_value(&c[first_idx]));
108 super::set_mark(backend, series.clone(), None, series, None);
109 super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
110 super::set_row_extras(backend, data, first_idx);
111
112 let lt = linetype_col
113 .and_then(|lc| scales.map_linetype(&lc[first_idx]))
114 .unwrap_or(Linetype::Solid);
115
116 let mut sorted = indices.clone();
118 sorted.sort_by(|&a, &b| {
119 let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
120 let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
121 xa.total_cmp(&xb)
122 });
123
124 let mut points: Vec<(f64, f64)> = sorted
125 .iter()
126 .map(|&i| {
127 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
128 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
129 coord.transform((nx, ny), &plot_area)
130 })
131 .collect();
132 let mut widths: Vec<f64> = sorted
133 .iter()
134 .map(|&i| {
135 linewidth_col
136 .and_then(|lc| scales.map_linewidth(&lc[i]))
137 .unwrap_or(self.width)
138 })
139 .collect();
140 close_for_radar(coord, &mut points);
141 if points.len() > widths.len() {
142 widths.push(widths[0]);
143 }
144
145 draw_polyline_with_linewidth(
146 backend,
147 &points,
148 &widths,
149 line_color,
150 self.alpha,
151 lt,
152 self.lineend,
153 self.arrow.as_ref(),
154 self.width,
155 )?;
156 }
157 super::clear_mark(backend);
158 super::clear_row_extras(backend);
159 } else {
160 let lt = linetype_col
161 .and_then(|lc| {
162 if lc.is_empty() {
163 None
164 } else {
165 scales.map_linetype(&lc[0])
166 }
167 })
168 .unwrap_or(Linetype::Solid);
169
170 let mut sorted_indices: Vec<usize> = (0..data.nrows()).collect();
172 sorted_indices.sort_by(|&a, &b| {
173 let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
174 let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
175 xa.total_cmp(&xb)
176 });
177
178 let mut points: Vec<(f64, f64)> = sorted_indices
179 .iter()
180 .map(|&i| {
181 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
182 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
183 coord.transform((nx, ny), &plot_area)
184 })
185 .collect();
186 let mut widths: Vec<f64> = sorted_indices
187 .iter()
188 .map(|&i| {
189 linewidth_col
190 .and_then(|lc| scales.map_linewidth(&lc[i]))
191 .unwrap_or(self.width)
192 })
193 .collect();
194 close_for_radar(coord, &mut points);
195 if points.len() > widths.len() {
196 widths.push(widths[0]);
197 }
198
199 if data.nrows() > 0 {
200 super::set_row_extras(backend, data, 0);
201 }
202 draw_polyline_with_linewidth(
203 backend,
204 &points,
205 &widths,
206 self.color,
207 self.alpha,
208 lt,
209 self.lineend,
210 self.arrow.as_ref(),
211 self.width,
212 )?;
213 super::clear_row_extras(backend);
214 }
215
216 Ok(())
217 }
218
219 fn required_aes(&self) -> Vec<Aesthetic> {
220 vec![Aesthetic::X, Aesthetic::Y]
221 }
222
223 fn default_stat(&self) -> Box<dyn Stat> {
224 Box::new(StatIdentity)
225 }
226
227 fn default_position(&self) -> Box<dyn Position> {
228 Box::new(PositionIdentity)
229 }
230
231 fn default_params(&self) -> GeomParams {
232 GeomParams::default()
233 }
234
235 fn name(&self) -> &str {
236 "line"
237 }
238
239 fn consumes_tooltips(&self) -> bool {
240 true
241 }
242
243 fn keeps_na_as_break(&self) -> bool {
244 true
245 }
246
247 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
248 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
249 self.color = ink;
250 }
251 }
252
253 fn set_series_color(&mut self, color: (u8, u8, u8)) {
254 self.color = color;
255 }
256}
257
258fn close_for_radar(coord: &dyn Coord, points: &mut Vec<(f64, f64)>) {
260 if coord.is_radar() && points.len() >= 3 {
261 let first = points[0];
262 points.push(first);
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use crate::data::Value;
269 use crate::geom::arrow::arrow;
270 use crate::prelude::*;
271
272 fn plot() -> GGPlot {
273 let n = 6;
274 let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
275 let y: Vec<Value> = (0..n)
276 .map(|i| Value::Float((i as f64 * 0.37).sin()))
277 .collect();
278 GGPlot::new(vec![("x".into(), x), ("y".into(), y)]).aes(Aes::new().x("x").y("y"))
279 }
280
281 #[test]
282 fn default_line_output_has_no_linecap_or_arrow_marks() {
283 let svg = plot()
284 .geom_line_with(GeomLine::default())
285 .render_svg_native_with_size(100, 60)
286 .unwrap();
287 assert!(!svg.contains("stroke-linecap"));
288 assert!(!svg.contains("data-part"));
289 }
290
291 #[test]
292 fn line_with_arrow_tags_one_head_for_the_single_polyline() {
293 let svg = plot()
294 .geom_line_with(GeomLine::default().arrow(arrow().closed()))
295 .render_svg_native_with_size(100, 60)
296 .unwrap();
297 assert_eq!(svg.matches("data-part=\"arrow\"").count(), 1);
298 }
299
300 #[test]
301 fn lineend_round_gives_stroke_linecap_round() {
302 let svg = plot()
303 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
304 .render_svg_native_with_size(100, 60)
305 .unwrap();
306 assert!(svg.contains("stroke-linecap=\"round\""));
307 }
308
309 #[test]
312 fn color_alpha_and_linewidth_builders_change_the_rendered_stroke() {
313 let svg = plot()
314 .geom_line_with(
315 GeomLine::default()
316 .color((10, 20, 30))
317 .alpha(0.4)
318 .linewidth(5.0),
319 )
320 .render_svg_native_with_size(100, 60)
321 .unwrap();
322 assert!(svg.contains("stroke=\"#0A141E\""));
323 assert!(svg.contains("stroke-opacity=\"0.400\""));
324 assert!(svg.contains("stroke-width=\"5.00\""));
325 }
326
327 #[test]
330 fn grouped_line_draws_one_polyline_per_colour_group_with_series_tags() {
331 let svg = GGPlot::new(vec![
332 (
333 "x".into(),
334 vec![
335 Value::Float(0.0),
336 Value::Float(1.0),
337 Value::Float(0.0),
338 Value::Float(1.0),
339 ],
340 ),
341 (
342 "y".into(),
343 vec![
344 Value::Float(0.0),
345 Value::Float(1.0),
346 Value::Float(1.0),
347 Value::Float(0.0),
348 ],
349 ),
350 (
351 "g".into(),
352 vec![
353 Value::Str("a".into()),
354 Value::Str("a".into()),
355 Value::Str("b".into()),
356 Value::Str("b".into()),
357 ],
358 ),
359 ])
360 .aes(Aes::new().x("x").y("y").color("g"))
361 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
362 .render_svg_native_with_size(100, 100)
363 .unwrap();
364
365 assert_eq!(svg.matches("stroke-linecap=\"round\"").count(), 2);
366 assert!(svg.contains("data-series=\"a\""));
367 assert!(svg.contains("data-series=\"b\""));
368 }
369
370 #[test]
375 fn varying_linewidth_within_a_group_draws_segment_by_segment() {
376 let svg = GGPlot::new(vec![
377 (
378 "x".into(),
379 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(2.0)],
380 ),
381 (
382 "y".into(),
383 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)],
384 ),
385 (
386 "w".into(),
387 vec![Value::Float(1.0), Value::Float(5.0), Value::Float(9.0)],
388 ),
389 ])
390 .aes(Aes::new().x("x").y("y").linewidth("w"))
391 .geom_line()
392 .scale_linewidth(ScaleLinewidthContinuous::new().with_range(2.0, 9.0))
393 .render_svg_native_with_size(100, 100)
394 .unwrap();
395
396 assert_eq!(svg.matches("stroke-width=\"2.00\"").count(), 1);
400 assert_eq!(svg.matches("stroke-width=\"5.50\"").count(), 1);
401 assert!(!svg.contains("stroke-width=\"9.00\""));
404 }
405}