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 set_series_color(&mut self, color: (u8, u8, u8)) {
222 self.color = color;
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use crate::data::Value;
229 use crate::geom::arrow::arrow;
230 use crate::prelude::*;
231
232 fn plot() -> GGPlot {
233 GGPlot::new(vec![
234 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
235 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
236 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
237 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
238 ])
239 .aes(Aes::new().x("x").y("y").xend("xend").yend("yend"))
240 }
241
242 #[test]
243 fn curve_emits_a_native_quadratic_path() {
244 let svg = plot()
245 .geom_curve_with(GeomCurve::default())
246 .render_svg_native_with_size(200, 200)
247 .unwrap();
248 assert!(svg.contains("<path"));
249 assert!(svg.contains(" Q"));
250 assert!(!svg.contains("NaN"));
251 }
252
253 #[test]
254 fn curve_arrow_closed_emits_tagged_polygons() {
255 let svg = plot()
256 .geom_curve_with(GeomCurve::default().arrow(arrow().closed()))
257 .render_svg_native_with_size(200, 200)
258 .unwrap();
259 assert_eq!(svg.matches("data-part=\"arrow\"").count(), 2);
260 }
261
262 #[test]
263 fn lineend_round_gives_stroke_linecap_round_on_the_path() {
264 let svg = plot()
265 .geom_curve_with(GeomCurve::default().lineend(crate::render::backend::LineEnd::Round))
266 .render_svg_native_with_size(200, 200)
267 .unwrap();
268 assert!(svg.contains("stroke-linecap=\"round\""));
269 }
270
271 #[test]
272 fn control_point_matches_the_documented_formula() {
273 let p0 = (0.0, 0.0);
274 let p2 = (10.0, 0.0);
275 let ctrl = GeomCurve::control_point(p0, p2, 0.2);
276 assert!((ctrl.0 - 5.0).abs() < 1e-9);
278 assert!((ctrl.1 - 2.0).abs() < 1e-9);
279 }
280
281 #[test]
284 fn builders_change_the_rendered_curve() {
285 let flat = plot()
286 .geom_curve_with(GeomCurve::default().curvature(0.0))
287 .render_svg_native_with_size(200, 200)
288 .unwrap();
289 let bent = plot()
290 .geom_curve_with(GeomCurve::default().curvature(0.8))
291 .render_svg_native_with_size(200, 200)
292 .unwrap();
293 assert_ne!(flat, bent, "curvature should change the control point");
294
295 let styled = plot()
296 .geom_curve_with(
297 GeomCurve::default()
298 .color((10, 20, 30))
299 .alpha(0.4)
300 .linewidth(5.0)
301 .ncp(12),
304 )
305 .render_svg_native_with_size(200, 200)
306 .unwrap();
307 assert!(styled.contains("stroke=\"#0A141E\""));
308 assert!(styled.contains("stroke-opacity=\"0.400\""));
309 assert!(styled.contains("stroke-width=\"5.00\""));
310 }
311
312 fn plot_with_extras() -> GGPlot {
315 GGPlot::new(vec![
316 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
317 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
318 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
319 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
320 (
321 "orig".into(),
322 vec![Value::Str("Berlin".into()), Value::Str("Paris & Co".into())],
323 ),
324 (
325 "tip".into(),
326 vec![
327 Value::Str("A tooltip".into()),
328 Value::Str("<script>".into()),
329 ],
330 ),
331 ])
332 .aes(
333 Aes::new()
334 .x("x")
335 .y("y")
336 .xend("xend")
337 .yend("yend")
338 .data_attr("orig", "orig")
339 .tooltip("tip"),
340 )
341 }
342
343 #[test]
344 fn data_attr_and_tooltip_both_emit_and_arrow_heads_carry_the_attr_but_no_title() {
345 let svg = plot_with_extras()
346 .geom_curve_with(GeomCurve::default().arrow(crate::geom::arrow::arrow().closed()))
347 .render_svg_native_with_size(200, 200)
348 .unwrap();
349
350 assert!(svg.contains("data-orig=\"Berlin\""));
351 assert!(svg.contains("data-orig=\"Paris & Co\""));
353 assert!(svg.contains("<title>A tooltip</title>"));
354 assert!(svg.contains("<script>"));
356 assert!(!svg.contains("<script>"));
357
358 for part in svg.split("<polygon").skip(1) {
362 let element = part.split("/>").next().unwrap_or("");
363 if element.contains("data-part=\"arrow\"") {
364 assert!(element.contains("data-orig="));
365 assert!(!element.contains("<title>"));
366 }
367 }
368 }
369
370 #[test]
371 fn data_attr_x_gives_a_single_data_x_not_two() {
372 let svg = GGPlot::new(vec![
376 ("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
377 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
378 (
379 "xlabel".into(),
380 vec![Value::Str("first".into()), Value::Str("second".into())],
381 ),
382 ])
383 .aes(Aes::new().x("x").y("y").data_attr("x", "xlabel"))
384 .geom_point()
385 .render_svg_native_with_size(200, 200)
386 .unwrap();
387
388 assert!(svg.contains("data-x=\"first\""));
389 assert!(svg.contains("data-x=\"second\""));
390 for tag_open in svg.split('<').skip(1) {
391 let attrs_part = tag_open.split('>').next().unwrap_or("");
392 let count = attrs_part.matches("data-x=\"").count();
393 assert!(count <= 1, "data-x duplicated in: <{tag_open}");
394 }
395 }
396
397 #[test]
398 fn layers_without_interactive_aes_are_byte_identical() {
399 let plain = |p: GGPlot| {
400 p.geom_curve_with(GeomCurve::default())
401 .render_svg_native_with_size(200, 200)
402 .unwrap()
403 };
404 let a = plain(plot());
405 let b = plain(plot());
406 assert_eq!(a, b);
407 assert!(!a.contains("data-orig"));
408 }
409
410 #[test]
415 fn data_attr_named_part_is_rejected_so_arrowheads_never_get_a_duplicate_data_part() {
416 let plot = GGPlot::new(vec![
417 ("x".into(), vec![Value::Float(0.0), Value::Float(0.2)]),
418 ("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
419 ("xend".into(), vec![Value::Float(1.0), Value::Float(0.8)]),
420 ("yend".into(), vec![Value::Float(0.5), Value::Float(0.3)]),
421 (
422 "p".into(),
423 vec![Value::Str("mine".into()), Value::Str("mine".into())],
424 ),
425 ])
426 .aes(
427 Aes::new()
428 .x("x")
429 .y("y")
430 .xend("xend")
431 .yend("yend")
432 .data_attr("part", "p"),
433 );
434 assert!(plot
437 .warnings
438 .iter()
439 .any(|w| w.contains("part") && w.contains("reserved")));
440
441 let svg = plot
442 .geom_curve_with(GeomCurve::default().arrow(crate::geom::arrow::arrow().closed()))
443 .render_svg_native_with_size(200, 200)
444 .unwrap();
445
446 for part in svg.split("<polygon").skip(1) {
448 let element = part.split("/>").next().unwrap_or("");
449 if element.contains("data-part") {
450 assert_eq!(element.matches("data-part=").count(), 1);
451 assert!(element.contains("data-part=\"arrow\""));
452 }
453 }
454 assert!(!svg.contains("data-part=\"mine\""));
455 }
456}