1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::geom::arrow::{draw_heads, heads, Arrow};
5use crate::position::identity::PositionIdentity;
6use crate::position::Position;
7use crate::render::backend::{DrawBackend, LineEnd, LineStyle, 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::{Geom, GeomParams};
15
16pub struct GeomCurve {
18 pub color: (u8, u8, u8),
19 pub width: f64,
20 pub alpha: f64,
21 pub curvature: f64,
24 pub ncp: usize,
27 pub arrow: Option<Arrow>,
29 pub lineend: LineEnd,
31}
32
33impl Default for GeomCurve {
34 fn default() -> Self {
35 GeomCurve {
36 color: (0, 0, 0),
37 width: 1.0,
38 alpha: 1.0,
39 curvature: 0.5,
40 ncp: 5,
41 arrow: None,
42 lineend: LineEnd::Butt,
43 }
44 }
45}
46
47impl GeomCurve {
48 pub fn color(mut self, color: (u8, u8, u8)) -> Self {
50 self.color = color;
51 self
52 }
53
54 pub fn linewidth(mut self, width: f64) -> Self {
56 self.width = width;
57 self
58 }
59
60 pub fn alpha(mut self, alpha: f64) -> Self {
62 self.alpha = alpha;
63 self
64 }
65
66 pub fn curvature(mut self, curvature: f64) -> Self {
68 self.curvature = curvature;
69 self
70 }
71
72 pub fn ncp(mut self, ncp: usize) -> Self {
74 self.ncp = ncp;
75 self
76 }
77
78 pub fn arrow(mut self, arrow: Arrow) -> Self {
80 self.arrow = Some(arrow);
81 self
82 }
83
84 pub fn lineend(mut self, lineend: LineEnd) -> Self {
86 self.lineend = lineend;
87 self
88 }
89
90 fn control_point(p0: (f64, f64), p2: (f64, f64), curvature: f64) -> (f64, f64) {
93 let mx = (p0.0 + p2.0) / 2.0;
94 let my = (p0.1 + p2.1) / 2.0;
95 let dx = p2.0 - p0.0;
96 let dy = p2.1 - p0.1;
97 (mx - dy * curvature, my + dx * curvature)
98 }
99
100 fn flatten(p0: (f64, f64), ctrl: (f64, f64), p2: (f64, f64), n: usize) -> Vec<(f64, f64)> {
105 (0..n)
106 .map(|i| {
107 let t = i as f64 / (n - 1) as f64;
108 let u = 1.0 - t;
109 let x = u * u * p0.0 + 2.0 * u * t * ctrl.0 + t * t * p2.0;
110 let y = u * u * p0.1 + 2.0 * u * t * ctrl.1 + t * t * p2.1;
111 (x, y)
112 })
113 .collect()
114 }
115}
116
117impl Geom for GeomCurve {
118 fn draw(
119 &self,
120 data: &DataFrame,
121 coord: &dyn Coord,
122 scales: &ScaleSet,
123 _theme: &Theme,
124 backend: &mut dyn DrawBackend,
125 ) -> Result<(), RenderError> {
126 let x_col = data
127 .column("x")
128 .ok_or(RenderError::MissingAesthetic("x".into()))?;
129 let y_col = data
130 .column("y")
131 .ok_or(RenderError::MissingAesthetic("y".into()))?;
132 let xend_col = data
133 .column("xend")
134 .ok_or(RenderError::MissingAesthetic("xend".into()))?;
135 let yend_col = data
136 .column("yend")
137 .ok_or(RenderError::MissingAesthetic("yend".into()))?;
138 let color_col = data.column("color");
139 let linetype_col = data.column("linetype");
140 let alpha_col = data.column("alpha");
141 let linewidth_col = data.column("linewidth");
142
143 let plot_area = backend.plot_area();
144 let x_scale = scales.get(&Aesthetic::X);
145 let y_scale = scales.get(&Aesthetic::Y);
146
147 for i in 0..data.nrows() {
148 let nx1 = x_scale.map(|s| s.map(&x_col[i])).unwrap_or(0.0);
149 let ny1 = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
150 let nx2 = x_scale.map(|s| s.map(&xend_col[i])).unwrap_or(0.0);
151 let ny2 = y_scale.map(|s| s.map(¥d_col[i])).unwrap_or(0.0);
152
153 let p0 = coord.transform((nx1, ny1), &plot_area);
154 let p2 = coord.transform((nx2, ny2), &plot_area);
155 let ctrl = Self::control_point(p0, p2, self.curvature);
156
157 let line_color = color_col
158 .and_then(|cc| scales.map_color(&Aesthetic::Color, &cc[i]))
159 .unwrap_or(self.color);
160 let lt = linetype_col
161 .and_then(|lc| scales.map_linetype(&lc[i]))
162 .unwrap_or(Linetype::Solid);
163 let alpha = alpha_col
164 .and_then(|ac| scales.map_alpha(&ac[i]).or_else(|| ac[i].as_f64()))
165 .filter(|a| a.is_finite())
166 .unwrap_or(self.alpha);
167 let width = linewidth_col
168 .and_then(|lc| scales.map_linewidth(&lc[i]))
169 .unwrap_or(self.width);
170 super::set_row_extras(backend, data, i);
171
172 backend.draw_quadratic(
173 p0,
174 ctrl,
175 p2,
176 &LineStyle {
177 color: line_color,
178 alpha,
179 width,
180 linetype: lt,
181 lineend: self.lineend,
182 },
183 )?;
184
185 if let Some(arrow) = &self.arrow {
186 let path = Self::flatten(p0, ctrl, p2, 64);
187 let hs = heads(&path, arrow, width);
188 draw_heads(backend, &hs, arrow, line_color, alpha, width)?;
189 }
190 }
191 super::clear_mark(backend);
192 super::clear_row_extras(backend);
193
194 Ok(())
195 }
196
197 fn required_aes(&self) -> Vec<Aesthetic> {
198 vec![Aesthetic::X, Aesthetic::Y, Aesthetic::Xend, Aesthetic::Yend]
199 }
200
201 fn default_stat(&self) -> Box<dyn Stat> {
202 Box::new(StatIdentity)
203 }
204
205 fn default_position(&self) -> Box<dyn Position> {
206 Box::new(PositionIdentity)
207 }
208
209 fn default_params(&self) -> GeomParams {
210 GeomParams::default()
211 }
212
213 fn name(&self) -> &str {
214 "curve"
215 }
216
217 fn consumes_tooltips(&self) -> bool {
218 true
219 }
220
221 fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
222 if let (true, Some(ink)) = (self.color == (0, 0, 0), el.ink) {
223 self.color = ink;
224 }
225 }
226
227 fn set_series_color(&mut self, color: (u8, u8, u8)) {
228 self.color = color;
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use crate::data::Value;
235 use crate::geom::arrow::arrow;
236 use crate::prelude::*;
237
238 fn plot() -> GGPlot {
239 GGPlot::new(vec![
240 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
241 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
242 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
243 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
244 ])
245 .aes(Aes::new().x("x").y("y").xend("xend").yend("yend"))
246 }
247
248 #[test]
249 fn curve_emits_a_native_quadratic_path() {
250 let svg = plot()
251 .geom_curve_with(GeomCurve::default())
252 .render_svg_native_with_size(200, 200)
253 .unwrap();
254 assert!(svg.contains("<path"));
255 assert!(svg.contains(" Q"));
256 assert!(!svg.contains("NaN"));
257 }
258
259 #[test]
260 fn curve_arrow_closed_emits_tagged_polygons() {
261 let svg = plot()
262 .geom_curve_with(GeomCurve::default().arrow(arrow().closed()))
263 .render_svg_native_with_size(200, 200)
264 .unwrap();
265 assert_eq!(svg.matches("data-part=\"arrow\"").count(), 2);
266 }
267
268 #[test]
269 fn lineend_round_gives_stroke_linecap_round_on_the_path() {
270 let svg = plot()
271 .geom_curve_with(GeomCurve::default().lineend(crate::render::backend::LineEnd::Round))
272 .render_svg_native_with_size(200, 200)
273 .unwrap();
274 assert!(svg.contains("stroke-linecap=\"round\""));
275 }
276
277 #[test]
278 fn control_point_matches_the_documented_formula() {
279 let p0 = (0.0, 0.0);
280 let p2 = (10.0, 0.0);
281 let ctrl = GeomCurve::control_point(p0, p2, 0.2);
282 assert!((ctrl.0 - 5.0).abs() < 1e-9);
284 assert!((ctrl.1 - 2.0).abs() < 1e-9);
285 }
286
287 #[test]
290 fn builders_change_the_rendered_curve() {
291 let flat = plot()
292 .geom_curve_with(GeomCurve::default().curvature(0.0))
293 .render_svg_native_with_size(200, 200)
294 .unwrap();
295 let bent = plot()
296 .geom_curve_with(GeomCurve::default().curvature(0.8))
297 .render_svg_native_with_size(200, 200)
298 .unwrap();
299 assert_ne!(flat, bent, "curvature should change the control point");
300
301 let styled = plot()
302 .geom_curve_with(
303 GeomCurve::default()
304 .color((10, 20, 30))
305 .alpha(0.4)
306 .linewidth(5.0)
307 .ncp(12),
310 )
311 .render_svg_native_with_size(200, 200)
312 .unwrap();
313 assert!(styled.contains("stroke=\"#0A141E\""));
314 assert!(styled.contains("stroke-opacity=\"0.400\""));
315 assert!(styled.contains("stroke-width=\"5.00\""));
316 }
317
318 fn plot_with_extras() -> GGPlot {
321 GGPlot::new(vec![
322 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
323 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
324 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
325 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
326 (
327 "orig".into(),
328 vec![Value::Str("Berlin".into()), Value::Str("Paris & Co".into())],
329 ),
330 (
331 "tip".into(),
332 vec![
333 Value::Str("A tooltip".into()),
334 Value::Str("<script>".into()),
335 ],
336 ),
337 ])
338 .aes(
339 Aes::new()
340 .x("x")
341 .y("y")
342 .xend("xend")
343 .yend("yend")
344 .data_attr("orig", "orig")
345 .tooltip("tip"),
346 )
347 }
348
349 #[test]
350 fn data_attr_and_tooltip_both_emit_and_arrow_heads_carry_the_attr_but_no_title() {
351 let svg = plot_with_extras()
352 .geom_curve_with(GeomCurve::default().arrow(crate::geom::arrow::arrow().closed()))
353 .render_svg_native_with_size(200, 200)
354 .unwrap();
355
356 assert!(svg.contains("data-orig=\"Berlin\""));
357 assert!(svg.contains("data-orig=\"Paris & Co\""));
359 assert!(svg.contains("<title>A tooltip</title>"));
360 assert!(svg.contains("<script>"));
362 assert!(!svg.contains("<script>"));
363
364 for part in svg.split("<polygon").skip(1) {
368 let element = part.split("/>").next().unwrap_or("");
369 if element.contains("data-part=\"arrow\"") {
370 assert!(element.contains("data-orig="));
371 assert!(!element.contains("<title>"));
372 }
373 }
374 }
375
376 #[test]
377 fn data_attr_x_gives_a_single_data_x_not_two() {
378 let svg = GGPlot::new(vec![
382 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
383 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
384 (
385 "xlabel".into(),
386 vec![Value::Str("first".into()), Value::Str("second".into())],
387 ),
388 ])
389 .aes(Aes::new().x("x").y("y").data_attr("x", "xlabel"))
390 .geom_point()
391 .render_svg_native_with_size(200, 200)
392 .unwrap();
393
394 assert!(svg.contains("data-x=\"first\""));
395 assert!(svg.contains("data-x=\"second\""));
396 for tag_open in svg.split('<').skip(1) {
397 let attrs_part = tag_open.split('>').next().unwrap_or("");
398 let count = attrs_part.matches("data-x=\"").count();
399 assert!(count <= 1, "data-x duplicated in: <{tag_open}");
400 }
401 }
402
403 #[test]
404 fn layers_without_interactive_aes_are_byte_identical() {
405 let plain = |p: GGPlot| {
406 p.geom_curve_with(GeomCurve::default())
407 .render_svg_native_with_size(200, 200)
408 .unwrap()
409 };
410 let a = plain(plot());
411 let b = plain(plot());
412 assert_eq!(a, b);
413 assert!(!a.contains("data-orig"));
414 }
415
416 #[test]
421 fn data_attr_named_part_is_rejected_so_arrowheads_never_get_a_duplicate_data_part() {
422 let plot = GGPlot::new(vec![
423 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
424 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
425 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
426 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
427 (
428 "p".into(),
429 vec![Value::Str("mine".into()), Value::Str("mine".into())],
430 ),
431 ])
432 .aes(
433 Aes::new()
434 .x("x")
435 .y("y")
436 .xend("xend")
437 .yend("yend")
438 .data_attr("part", "p"),
439 );
440 assert!(plot
443 .warnings
444 .iter()
445 .any(|w| w.contains("part") && w.contains("reserved")));
446
447 let svg = plot
448 .geom_curve_with(GeomCurve::default().arrow(crate::geom::arrow::arrow().closed()))
449 .render_svg_native_with_size(200, 200)
450 .unwrap();
451
452 for part in svg.split("<polygon").skip(1) {
454 let element = part.split("/>").next().unwrap_or("");
455 if element.contains("data-part") {
456 assert_eq!(element.matches("data-part=").count(), 1);
457 assert!(element.contains("data-part=\"arrow\""));
458 }
459 }
460 assert!(!svg.contains("data-part=\"mine\""));
461 }
462}