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 if let (true, Some(w)) = (self.width == GeomLine::default().width, el.linewidth) {
268 self.width = w;
269 }
270 }
271
272 fn set_series_color(&mut self, color: (u8, u8, u8)) {
273 self.color = color;
274 }
275}
276
277fn close_for_radar(coord: &dyn Coord, points: &mut Vec<(f64, f64)>) {
279 if coord.is_radar() && points.len() >= 3 {
280 let first = points[0];
281 points.push(first);
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use crate::data::Value;
288 use crate::geom::arrow::arrow;
289 use crate::prelude::*;
290
291 fn plot() -> GGPlot {
292 let n = 6;
293 let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
294 let y: Vec<Value> = (0..n)
295 .map(|i| Value::Float((i as f64 * 0.37).sin()))
296 .collect();
297 GGPlot::new(vec![("x".into(), x), ("y".into(), y)]).aes(Aes::new().x("x").y("y"))
298 }
299
300 #[test]
301 fn default_line_output_has_no_linecap_or_arrow_marks() {
302 let svg = plot()
303 .geom_line_with(GeomLine::default())
304 .render_svg_native_with_size(100, 60)
305 .unwrap();
306 assert!(!svg.contains("stroke-linecap"));
307 assert!(!svg.contains("data-part"));
308 }
309
310 #[test]
311 fn line_with_arrow_tags_one_head_for_the_single_polyline() {
312 let svg = plot()
313 .geom_line_with(GeomLine::default().arrow(arrow().closed()))
314 .render_svg_native_with_size(100, 60)
315 .unwrap();
316 assert_eq!(svg.matches("data-part=\"arrow\"").count(), 1);
317 }
318
319 #[test]
320 fn lineend_round_gives_stroke_linecap_round() {
321 let svg = plot()
322 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
323 .render_svg_native_with_size(100, 60)
324 .unwrap();
325 assert!(svg.contains("stroke-linecap=\"round\""));
326 }
327
328 #[test]
331 fn color_alpha_and_linewidth_builders_change_the_rendered_stroke() {
332 let svg = plot()
333 .geom_line_with(
334 GeomLine::default()
335 .color((10, 20, 30))
336 .alpha(0.4)
337 .linewidth(5.0),
338 )
339 .render_svg_native_with_size(100, 60)
340 .unwrap();
341 assert!(svg.contains("stroke=\"#0A141E\""));
342 assert!(svg.contains("stroke-opacity=\"0.400\""));
343 assert!(svg.contains("stroke-width=\"5.00\""));
344 }
345
346 #[test]
349 fn grouped_line_draws_one_polyline_per_colour_group_with_series_tags() {
350 let svg = GGPlot::new(vec![
351 (
352 "x".into(),
353 vec![
354 Value::Float(0.0),
355 Value::Float(1.0),
356 Value::Float(0.0),
357 Value::Float(1.0),
358 ],
359 ),
360 (
361 "y".into(),
362 vec![
363 Value::Float(0.0),
364 Value::Float(1.0),
365 Value::Float(1.0),
366 Value::Float(0.0),
367 ],
368 ),
369 (
370 "g".into(),
371 vec![
372 Value::Str("a".into()),
373 Value::Str("a".into()),
374 Value::Str("b".into()),
375 Value::Str("b".into()),
376 ],
377 ),
378 ])
379 .aes(Aes::new().x("x").y("y").color("g"))
380 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
381 .render_svg_native_with_size(100, 100)
382 .unwrap();
383
384 assert_eq!(svg.matches("stroke-linecap=\"round\"").count(), 2);
385 assert!(svg.contains("data-series=\"a\""));
386 assert!(svg.contains("data-series=\"b\""));
387 }
388
389 #[test]
394 fn varying_linewidth_within_a_group_draws_segment_by_segment() {
395 let svg = GGPlot::new(vec![
396 (
397 "x".into(),
398 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(2.0)],
399 ),
400 (
401 "y".into(),
402 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)],
403 ),
404 (
405 "w".into(),
406 vec![Value::Float(1.0), Value::Float(5.0), Value::Float(9.0)],
407 ),
408 ])
409 .aes(Aes::new().x("x").y("y").linewidth("w"))
410 .geom_line()
411 .scale_linewidth(ScaleLinewidthContinuous::new().with_range(2.0, 9.0))
412 .render_svg_native_with_size(100, 100)
413 .unwrap();
414
415 assert_eq!(svg.matches("stroke-width=\"2.00\"").count(), 1);
419 assert_eq!(svg.matches("stroke-width=\"5.50\"").count(), 1);
420 assert!(!svg.contains("stroke-width=\"9.00\""));
423 }
424}