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 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 pub fn color(mut self, color: (u8, u8, u8)) -> Self {
48 self.color = color;
49 self
50 }
51
52 pub fn linewidth(mut self, width: f64) -> Self {
54 self.width = width;
55 self
56 }
57
58 pub fn alpha(mut self, alpha: f64) -> Self {
60 self.alpha = alpha;
61 self
62 }
63
64 pub fn arrow(mut self, arrow: Arrow) -> Self {
66 self.arrow = Some(arrow);
67 self
68 }
69
70 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 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 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 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 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
273fn 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 #[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 #[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 #[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 assert_eq!(svg.matches("stroke-width=\"2.00\"").count(), 1);
415 assert_eq!(svg.matches("stroke-width=\"5.50\"").count(), 1);
416 assert!(!svg.contains("stroke-width=\"9.00\""));
419 }
420}