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 let Some(cc) = color_col {
97 let mut groups: Vec<(String, Vec<usize>)> = Vec::new();
98 for (i, v) in cc.iter().enumerate() {
99 let key = v.to_group_key();
100 if let Some(entry) = groups.iter_mut().find(|(k, _)| k == &key) {
101 entry.1.push(i);
102 } else {
103 groups.push((key, vec![i]));
104 }
105 }
106
107 for (_, indices) in &groups {
108 let first_idx = indices[0];
109 let line_color = scales
110 .map_color(&Aesthetic::Color, &cc[first_idx])
111 .unwrap_or(self.color);
112 let series = Some(super::tip_value(&cc[first_idx]));
114 super::set_mark(backend, series.clone(), None, series, None);
115 super::set_mark_formatted(backend, scales, None, &Aesthetic::Y, None);
116 super::set_row_extras(backend, data, first_idx);
117
118 let lt = linetype_col
119 .and_then(|lc| scales.map_linetype(&lc[first_idx]))
120 .unwrap_or(Linetype::Solid);
121
122 let mut sorted = indices.clone();
124 sorted.sort_by(|&a, &b| {
125 let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
126 let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
127 xa.total_cmp(&xb)
128 });
129
130 let mut points: Vec<(f64, f64)> = sorted
131 .iter()
132 .map(|&i| {
133 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
134 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
135 coord.transform((nx, ny), &plot_area)
136 })
137 .collect();
138 let mut widths: Vec<f64> = sorted
139 .iter()
140 .map(|&i| {
141 linewidth_col
142 .and_then(|lc| scales.map_linewidth(&lc[i]))
143 .unwrap_or(self.width)
144 })
145 .collect();
146 close_for_radar(coord, &mut points);
147 if points.len() > widths.len() {
148 widths.push(widths[0]);
149 }
150
151 draw_polyline_with_linewidth(
152 backend,
153 &points,
154 &widths,
155 line_color,
156 self.alpha,
157 lt,
158 self.lineend,
159 self.arrow.as_ref(),
160 self.width,
161 )?;
162 }
163 super::clear_mark(backend);
164 super::clear_row_extras(backend);
165 } else {
166 let lt = linetype_col
167 .and_then(|lc| {
168 if lc.is_empty() {
169 None
170 } else {
171 scales.map_linetype(&lc[0])
172 }
173 })
174 .unwrap_or(Linetype::Solid);
175
176 let mut sorted_indices: Vec<usize> = (0..data.nrows()).collect();
178 sorted_indices.sort_by(|&a, &b| {
179 let xa = x_scale.map(|s| s.map(&x_col[a])).unwrap_or(0.0);
180 let xb = x_scale.map(|s| s.map(&x_col[b])).unwrap_or(0.0);
181 xa.total_cmp(&xb)
182 });
183
184 let mut points: Vec<(f64, f64)> = sorted_indices
185 .iter()
186 .map(|&i| {
187 let nx = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
188 let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
189 coord.transform((nx, ny), &plot_area)
190 })
191 .collect();
192 let mut widths: Vec<f64> = sorted_indices
193 .iter()
194 .map(|&i| {
195 linewidth_col
196 .and_then(|lc| scales.map_linewidth(&lc[i]))
197 .unwrap_or(self.width)
198 })
199 .collect();
200 close_for_radar(coord, &mut points);
201 if points.len() > widths.len() {
202 widths.push(widths[0]);
203 }
204
205 if data.nrows() > 0 {
206 super::set_row_extras(backend, data, 0);
207 }
208 draw_polyline_with_linewidth(
209 backend,
210 &points,
211 &widths,
212 self.color,
213 self.alpha,
214 lt,
215 self.lineend,
216 self.arrow.as_ref(),
217 self.width,
218 )?;
219 super::clear_row_extras(backend);
220 }
221
222 Ok(())
223 }
224
225 fn required_aes(&self) -> Vec<Aesthetic> {
226 vec![Aesthetic::X, Aesthetic::Y]
227 }
228
229 fn default_stat(&self) -> Box<dyn Stat> {
230 Box::new(StatIdentity)
231 }
232
233 fn default_position(&self) -> Box<dyn Position> {
234 Box::new(PositionIdentity)
235 }
236
237 fn default_params(&self) -> GeomParams {
238 GeomParams::default()
239 }
240
241 fn name(&self) -> &str {
242 "line"
243 }
244
245 fn consumes_tooltips(&self) -> bool {
246 true
247 }
248
249 fn keeps_na_as_break(&self) -> bool {
250 true
251 }
252
253 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
254 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
255 self.color = ink;
256 }
257 }
258
259 fn set_series_color(&mut self, color: (u8, u8, u8)) {
260 self.color = color;
261 }
262}
263
264fn close_for_radar(coord: &dyn Coord, points: &mut Vec<(f64, f64)>) {
266 if coord.is_radar() && points.len() >= 3 {
267 let first = points[0];
268 points.push(first);
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use crate::data::Value;
275 use crate::geom::arrow::arrow;
276 use crate::prelude::*;
277
278 fn plot() -> GGPlot {
279 let n = 6;
280 let x: Vec<Value> = (0..n).map(|i| Value::Float(i as f64)).collect();
281 let y: Vec<Value> = (0..n)
282 .map(|i| Value::Float((i as f64 * 0.37).sin()))
283 .collect();
284 GGPlot::new(vec![("x".into(), x), ("y".into(), y)]).aes(Aes::new().x("x").y("y"))
285 }
286
287 #[test]
288 fn default_line_output_has_no_linecap_or_arrow_marks() {
289 let svg = plot()
290 .geom_line_with(GeomLine::default())
291 .render_svg_native_with_size(100, 60)
292 .unwrap();
293 assert!(!svg.contains("stroke-linecap"));
294 assert!(!svg.contains("data-part"));
295 }
296
297 #[test]
298 fn line_with_arrow_tags_one_head_for_the_single_polyline() {
299 let svg = plot()
300 .geom_line_with(GeomLine::default().arrow(arrow().closed()))
301 .render_svg_native_with_size(100, 60)
302 .unwrap();
303 assert_eq!(svg.matches("data-part=\"arrow\"").count(), 1);
304 }
305
306 #[test]
307 fn lineend_round_gives_stroke_linecap_round() {
308 let svg = plot()
309 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
310 .render_svg_native_with_size(100, 60)
311 .unwrap();
312 assert!(svg.contains("stroke-linecap=\"round\""));
313 }
314
315 #[test]
318 fn color_alpha_and_linewidth_builders_change_the_rendered_stroke() {
319 let svg = plot()
320 .geom_line_with(
321 GeomLine::default()
322 .color((10, 20, 30))
323 .alpha(0.4)
324 .linewidth(5.0),
325 )
326 .render_svg_native_with_size(100, 60)
327 .unwrap();
328 assert!(svg.contains("stroke=\"#0A141E\""));
329 assert!(svg.contains("stroke-opacity=\"0.400\""));
330 assert!(svg.contains("stroke-width=\"5.00\""));
331 }
332
333 #[test]
336 fn grouped_line_draws_one_polyline_per_colour_group_with_series_tags() {
337 let svg = GGPlot::new(vec![
338 (
339 "x".into(),
340 vec![
341 Value::Float(0.0),
342 Value::Float(1.0),
343 Value::Float(0.0),
344 Value::Float(1.0),
345 ],
346 ),
347 (
348 "y".into(),
349 vec![
350 Value::Float(0.0),
351 Value::Float(1.0),
352 Value::Float(1.0),
353 Value::Float(0.0),
354 ],
355 ),
356 (
357 "g".into(),
358 vec![
359 Value::Str("a".into()),
360 Value::Str("a".into()),
361 Value::Str("b".into()),
362 Value::Str("b".into()),
363 ],
364 ),
365 ])
366 .aes(Aes::new().x("x").y("y").color("g"))
367 .geom_line_with(GeomLine::default().lineend(crate::render::backend::LineEnd::Round))
368 .render_svg_native_with_size(100, 100)
369 .unwrap();
370
371 assert_eq!(svg.matches("stroke-linecap=\"round\"").count(), 2);
372 assert!(svg.contains("data-series=\"a\""));
373 assert!(svg.contains("data-series=\"b\""));
374 }
375
376 #[test]
381 fn varying_linewidth_within_a_group_draws_segment_by_segment() {
382 let svg = GGPlot::new(vec![
383 (
384 "x".into(),
385 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(2.0)],
386 ),
387 (
388 "y".into(),
389 vec![Value::Float(0.0), Value::Float(1.0), Value::Float(0.0)],
390 ),
391 (
392 "w".into(),
393 vec![Value::Float(1.0), Value::Float(5.0), Value::Float(9.0)],
394 ),
395 ])
396 .aes(Aes::new().x("x").y("y").linewidth("w"))
397 .geom_line()
398 .scale_linewidth(ScaleLinewidthContinuous::new().with_range(2.0, 9.0))
399 .render_svg_native_with_size(100, 100)
400 .unwrap();
401
402 assert_eq!(svg.matches("stroke-width=\"2.00\"").count(), 1);
406 assert_eq!(svg.matches("stroke-width=\"5.50\"").count(), 1);
407 assert!(!svg.contains("stroke-width=\"9.00\""));
410 }
411}