ggplot_rs/render/backend.rs
1use super::{Rect, RenderError};
2
3/// Point shape types.
4#[derive(Clone, Debug, Copy, PartialEq, Eq)]
5pub enum PointShape {
6 Circle,
7 Triangle,
8 Square,
9 Diamond,
10 Cross,
11 Plus,
12}
13
14impl PointShape {
15 /// All shapes in order (for discrete scale mapping).
16 pub const ALL: &[PointShape] = &[
17 PointShape::Circle,
18 PointShape::Triangle,
19 PointShape::Square,
20 PointShape::Diamond,
21 PointShape::Cross,
22 PointShape::Plus,
23 ];
24}
25
26/// Line type patterns.
27///
28/// The named variants are ggplot2's fixed patterns; [`Linetype::Custom`]
29/// takes a caller-supplied dash/gap sequence (#220), e.g.
30/// `Linetype::Custom(vec![(0.0, 10.0)])` together with
31/// [`LineEnd::Round`] draws a row of round dots every 10 px (zero-length
32/// dashes are drawn as round caps), as flow-diagram edges use.
33///
34/// `Linetype` is no longer `Copy` (a `Custom` pattern owns its `Vec`) —
35/// clone it where a copy was taken before.
36#[derive(Clone, Debug, PartialEq)]
37pub enum Linetype {
38 Solid,
39 Dashed,
40 Dotted,
41 DashDot,
42 LongDash,
43 TwoDash,
44 /// Arbitrary `(dash, gap)` pairs in px, rendered through the same
45 /// `stroke-dasharray` / dashed-stroking path as the named variants.
46 ///
47 /// Degenerate input never panics: pairs with a non-finite value are
48 /// dropped, negative lengths are clamped to `0`, and a pattern with no
49 /// usable pairs (empty, all-NaN, or a cycle of zero total length) draws
50 /// solid. See [`Linetype::dash_pattern`].
51 Custom(Vec<(f64, f64)>),
52}
53
54impl Linetype {
55 /// All named linetypes in order (for discrete scale mapping).
56 /// [`Linetype::Custom`] is not part of the cycle.
57 pub const ALL: &[Linetype] = &[
58 Linetype::Solid,
59 Linetype::Dashed,
60 Linetype::Dotted,
61 Linetype::DashDot,
62 Linetype::LongDash,
63 Linetype::TwoDash,
64 ];
65
66 /// Get the dash pattern as (draw_len, gap_len) pairs in pixels, exactly
67 /// as stored. For [`Linetype::Custom`] this is the caller's raw input;
68 /// renderers use the sanitised [`Linetype::dash_pattern`] instead.
69 pub fn pattern(&self) -> &[(f64, f64)] {
70 match self {
71 Linetype::Solid => &[],
72 Linetype::Dashed => &[(6.0, 3.0)],
73 Linetype::Dotted => &[(2.0, 2.0)],
74 Linetype::DashDot => &[(6.0, 2.0), (2.0, 2.0)],
75 Linetype::LongDash => &[(10.0, 4.0)],
76 Linetype::TwoDash => &[(8.0, 3.0), (3.0, 3.0)],
77 Linetype::Custom(pairs) => pairs,
78 }
79 }
80
81 /// The dash pattern a renderer should draw: [`Linetype::pattern`] with
82 /// non-finite pairs dropped and negative lengths clamped to `0`. Returns
83 /// an empty vector (solid) when nothing usable is left or one full
84 /// dash+gap cycle is shorter than 0.01 px (or overflows to infinity), so no backend ever sees a
85 /// NaN, a negative length, or a zero-length cycle (which would never
86 /// advance a dashed stroker).
87 pub fn dash_pattern(&self) -> Vec<(f64, f64)> {
88 let pairs: Vec<(f64, f64)> = self
89 .pattern()
90 .iter()
91 .filter(|(d, g)| d.is_finite() && g.is_finite())
92 .map(|&(d, g)| (d.max(0.0), g.max(0.0)))
93 .collect();
94 let cycle: f64 = pairs.iter().map(|(d, g)| d + g).sum();
95 // No gap at all is a solid line too: emit nothing rather than a
96 // dasharray with only zero gaps.
97 let gaps: f64 = pairs.iter().map(|(_, g)| g).sum();
98 if pairs.is_empty() || !cycle.is_finite() || cycle < 0.01 || gaps <= 0.0 {
99 Vec::new()
100 } else {
101 pairs
102 }
103 }
104}
105
106/// Style for drawing points/circles.
107#[derive(Clone, Debug)]
108pub struct PointStyle {
109 pub color: (u8, u8, u8),
110 pub alpha: f64,
111 pub filled: bool,
112 pub shape: PointShape,
113}
114
115impl Default for PointStyle {
116 fn default() -> Self {
117 PointStyle {
118 color: (0, 0, 0),
119 alpha: 1.0,
120 filled: true,
121 shape: PointShape::Circle,
122 }
123 }
124}
125
126/// Line cap style (R grid's `lineend`), applied to the stroked ends of a
127/// polyline/path. `Butt` is SVG's own default, so it is written on the wire
128/// only when it differs (byte-identical default output).
129#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
130pub enum LineEnd {
131 #[default]
132 Butt,
133 Round,
134 Square,
135}
136
137/// Style for drawing lines.
138#[derive(Clone, Debug)]
139pub struct LineStyle {
140 pub color: (u8, u8, u8),
141 pub alpha: f64,
142 pub width: f64,
143 pub linetype: Linetype,
144 pub lineend: LineEnd,
145}
146
147impl Default for LineStyle {
148 fn default() -> Self {
149 LineStyle {
150 color: (0, 0, 0),
151 alpha: 1.0,
152 width: 1.0,
153 linetype: Linetype::Solid,
154 lineend: LineEnd::Butt,
155 }
156 }
157}
158
159/// Style for drawing rectangles/polygons.
160#[derive(Clone, Debug)]
161pub struct RectStyle {
162 pub fill: Option<(u8, u8, u8)>,
163 pub stroke: Option<(u8, u8, u8)>,
164 pub stroke_width: f64,
165 pub alpha: f64,
166 /// Whether to clip this rect to the plot area. Default `true` for data elements.
167 /// Set to `false` for non-data elements (backgrounds, strips, legends).
168 pub clip: bool,
169 /// Corner radius in px, for a rounded rectangle (ggplot2 `geom_label(label.r)`,
170 /// ggchicklet `radius`). `0.0` (the default) draws a plain rectangle; backends
171 /// that cannot draw rounded corners natively fall back to a plain rectangle.
172 pub corner_radius: f64,
173}
174
175impl Default for RectStyle {
176 fn default() -> Self {
177 RectStyle {
178 fill: Some((128, 128, 128)),
179 stroke: None,
180 stroke_width: 1.0,
181 alpha: 1.0,
182 clip: true,
183 corner_radius: 0.0,
184 }
185 }
186}
187
188/// A pattern fill for an area that has no single colour to draw — today
189/// just a scale's `na_pattern` (#206): `ScaleFillBinned::na_pattern`,
190/// drawn instead of `na_value`'s flat colour by `geom_sf`/`geom_polygon`/
191/// `geom_rect`/`geom_tile` and the legend's "no data" key, so a classed
192/// choropleth's no-data cells read as "we have no reading here" rather
193/// than a greyed-out *value* easily mistaken for one.
194///
195/// `fg` stripes run at `angle_deg` (0 = horizontal, measured the usual
196/// screen way, clockwise from the x axis), each `size_px` wide, over a
197/// `bg` field — `fg` fills the first 30% of each cycle, `bg` the rest, a
198/// thin hatch rather than an even 50/50 stripe (ggplot2/R's own
199/// `pattern_stripe(density = 0.3)` default). More pattern kinds can join
200/// this enum later without breaking callers that match it today only via
201/// [`FillPattern::bg`]/[`FillPattern::color_at`].
202#[derive(Clone, Copy, Debug, PartialEq)]
203pub enum FillPattern {
204 Stripe {
205 fg: (u8, u8, u8),
206 bg: (u8, u8, u8),
207 /// The width of one fg+bg cycle, in output pixels. `<= 0.0` is
208 /// treated as `1.0` (an unreasonably fine hatch rather than a
209 /// division by zero/negative-period wraparound).
210 size_px: f64,
211 /// Stripe direction in degrees (0 = horizontal).
212 angle_deg: f64,
213 },
214}
215
216impl FillPattern {
217 /// The fraction of each cycle `fg` covers (the rest is `bg`) — a thin
218 /// hatch, not a 50/50 stripe.
219 const STRIPE_FG_FRACTION: f64 = 0.3;
220
221 /// A reasonable default stripe: `fg`/`bg` as given, 4px period, 45°.
222 pub fn stripe(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> FillPattern {
223 FillPattern::Stripe {
224 fg,
225 bg,
226 size_px: 4.0,
227 angle_deg: 45.0,
228 }
229 }
230
231 /// The flat colour a backend with no pattern primitive falls back to
232 /// (`plotters` — "falls back to bg", #206) — the pattern's background,
233 /// so a no-data cell still reads as *muted*, just without the hatch.
234 pub fn bg(&self) -> (u8, u8, u8) {
235 match self {
236 FillPattern::Stripe { bg, .. } => *bg,
237 }
238 }
239
240 /// This pattern's colour at screen point `(x, y)` — a raster backend
241 /// (pixel/`canvas`) fills per pixel with this ("raster stripes", #206)
242 /// rather than approximating with [`FillPattern::bg`].
243 pub fn color_at(&self, x: f64, y: f64) -> (u8, u8, u8) {
244 match self {
245 FillPattern::Stripe {
246 fg,
247 bg,
248 size_px,
249 angle_deg,
250 } => {
251 let (s, c) = angle_deg.to_radians().sin_cos();
252 let rx = x * c + y * s;
253 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
254 let frac = (rx / size).rem_euclid(1.0);
255 if frac < Self::STRIPE_FG_FRACTION {
256 *fg
257 } else {
258 *bg
259 }
260 }
261 }
262 }
263}
264
265/// Number of line segments approximating each 90° corner of a rounded rect in
266/// backends with no native rounded-rect primitive (pixel, plotters). Higher
267/// is smoother; 8 is visually indistinguishable from a true arc at the sizes
268/// these rects are drawn at (node/label boxes, a few tens to ~200px).
269///
270/// Only [`pixel_backend`](super::pixel_backend) (feature `canvas`) and
271/// [`plotters_backend`](super::plotters_backend) (feature `plotters`) call
272/// [`rounded_rect_points`] — `svg_backend` emits a native `rx`/`ry` instead —
273/// so both it and the function are cfg-gated the same way those backends
274/// are, rather than left for `--no-default-features` to flag as dead code.
275#[cfg(any(
276 feature = "canvas",
277 all(feature = "plotters", not(target_arch = "wasm32"))
278))]
279const ROUNDED_RECT_CORNER_SEGMENTS: usize = 8;
280
281/// A closed polygon approximating a rectangle with rounded corners, for
282/// backends that must rasterize/triangulate rather than emit a native rounded
283/// rect (see [`DrawBackend::draw_rect`]'s `corner_radius`, via [`RectStyle`]).
284///
285/// `radius` is clamped to half the shorter side, so an oversized radius draws
286/// a "stadium" (fully rounded) shape rather than overshooting. A
287/// non-positive radius returns the plain 4-corner rectangle.
288///
289/// Corners go clockwise in screen coordinates (y down), starting at the
290/// top-right, so the first and last points sit either side of the top-right
291/// corner — callers that stroke the result as a closed loop (last point back
292/// to the first) get a seamless boundary.
293#[cfg(any(
294 feature = "canvas",
295 all(feature = "plotters", not(target_arch = "wasm32"))
296))]
297pub(crate) fn rounded_rect_points(
298 x0: f64,
299 y0: f64,
300 x1: f64,
301 y1: f64,
302 radius: f64,
303) -> Vec<(f64, f64)> {
304 let (x0, x1) = (x0.min(x1), x0.max(x1));
305 let (y0, y1) = (y0.min(y1), y0.max(y1));
306 let r = radius.max(0.0).min((x1 - x0) / 2.0).min((y1 - y0) / 2.0);
307 if r <= 0.0 {
308 return vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)];
309 }
310 use std::f64::consts::{FRAC_PI_2, PI};
311 let n = ROUNDED_RECT_CORNER_SEGMENTS;
312 // (corner centre, start angle, end angle), clockwise, starting top-right.
313 let corners = [
314 (x1 - r, y0 + r, -FRAC_PI_2, 0.0), // top-right
315 (x1 - r, y1 - r, 0.0, FRAC_PI_2), // bottom-right
316 (x0 + r, y1 - r, FRAC_PI_2, PI), // bottom-left
317 (x0 + r, y0 + r, PI, 3.0 * FRAC_PI_2), // top-left
318 ];
319 let mut pts = Vec::with_capacity(4 * (n + 1));
320 for (cx, cy, a0, a1) in corners {
321 for i in 0..=n {
322 let t = a0 + (a1 - a0) * (i as f64 / n as f64);
323 pts.push((cx + r * t.cos(), cy + r * t.sin()));
324 }
325 }
326 pts
327}
328
329#[cfg(all(
330 test,
331 any(
332 feature = "canvas",
333 all(feature = "plotters", not(target_arch = "wasm32"))
334 )
335))]
336mod rounded_rect_tests {
337 use super::rounded_rect_points;
338
339 #[test]
340 fn zero_radius_is_a_plain_rectangle() {
341 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 0.0);
342 assert_eq!(
343 pts,
344 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
345 );
346 }
347
348 #[test]
349 fn negative_radius_also_falls_back_to_a_plain_rectangle() {
350 // `radius.max(0.0)` guards a caller passing a negative radius
351 // (e.g. a miscomputed value) — it must not panic or produce a
352 // garbage polygon, just the same plain-rect fallback as 0.0.
353 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, -5.0);
354 assert_eq!(
355 pts,
356 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
357 );
358 }
359
360 #[test]
361 fn x_and_y_are_order_independent() {
362 // Callers may pass (x1, y1) before (x0, y0) (e.g. a box whose
363 // ymax < ymin before min/max normalisation elsewhere) — the
364 // function itself must normalise, not assume sorted input.
365 let forward = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 3.0);
366 let swapped = rounded_rect_points(10.0, 20.0, 0.0, 0.0, 3.0);
367 assert_eq!(forward, swapped);
368 }
369
370 #[test]
371 fn radius_is_clamped_to_half_the_shorter_side() {
372 // A radius bigger than the box collapses to a "stadium": every point
373 // must stay within the rect's bounds.
374 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 1000.0);
375 for (x, y) in &pts {
376 assert!((0.0..=10.0).contains(x), "x={x} out of [0,10]");
377 assert!((0.0..=20.0).contains(y), "y={y} out of [0,20]");
378 }
379 }
380
381 #[test]
382 fn corners_are_inset_by_radius_from_the_rect_corners() {
383 let pts = rounded_rect_points(0.0, 0.0, 100.0, 50.0, 10.0);
384 // The arc midpoint of the top-right corner sits at 45° in from the
385 // corner, inset by r*(1 - cos(45°)) from the straight edges.
386 assert!(pts.len() > 4);
387 for (x, y) in &pts {
388 assert!(*x >= 0.0 && *x <= 100.0);
389 assert!(*y >= 0.0 && *y <= 50.0);
390 }
391 }
392}
393
394/// Style for drawing text.
395#[derive(Clone, Debug)]
396pub struct TextStyle {
397 pub color: (u8, u8, u8),
398 pub size: f64,
399 pub anchor: TextAnchor,
400 pub angle: f64,
401 /// Font family (e.g., "serif", "monospace"). None defaults to "sans-serif".
402 pub family: Option<String>,
403 /// Font face (R's `element_text(face = ...)`).
404 pub face: FontFace,
405}
406
407/// Font face / weight (R's `element_text(face = ...)`).
408#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
409pub enum FontFace {
410 #[default]
411 Plain,
412 Bold,
413 Italic,
414}
415
416impl Default for TextStyle {
417 fn default() -> Self {
418 TextStyle {
419 color: (50, 50, 50),
420 size: 12.0,
421 anchor: TextAnchor::Middle,
422 angle: 0.0,
423 family: None,
424 face: crate::render::backend::FontFace::Plain,
425 }
426 }
427}
428
429#[derive(Clone, Debug)]
430pub enum TextAnchor {
431 Start,
432 Middle,
433 End,
434}
435
436/// Our rendering abstraction, independent of plotters details.
437pub trait DrawBackend {
438 fn draw_circle(
439 &mut self,
440 center: (f64, f64),
441 radius: f64,
442 style: &PointStyle,
443 ) -> Result<(), RenderError>;
444 fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError>;
445 fn draw_rect(
446 &mut self,
447 top_left: (f64, f64),
448 bottom_right: (f64, f64),
449 style: &RectStyle,
450 ) -> Result<(), RenderError>;
451 fn draw_text(
452 &mut self,
453 text: &str,
454 pos: (f64, f64),
455 style: &TextStyle,
456 ) -> Result<(), RenderError>;
457 fn draw_polygon(&mut self, points: &[(f64, f64)], style: &RectStyle)
458 -> Result<(), RenderError>;
459
460 /// Draw a polygon with holes: `rings[0]` is the exterior, every other
461 /// ring a hole cut out of it under the even-odd fill rule (`geom_sf`'s
462 /// `Polygon`, OGC/`sf` convention — see
463 /// [`spatial::Geometry::Polygon`](crate::spatial::Geometry::Polygon)).
464 /// One `stroke`/`stroke_width` draws every ring's outline, exterior and
465 /// holes alike (as `ggplot2`'s `geom_sf` does).
466 ///
467 /// Default: no real hole cut-out (for backends that pre-date this
468 /// method) — the exterior ring filled and stroked, every other ring
469 /// stroked only. Backends with a resolution-independent fill (SVG) or a
470 /// scanline rasteriser (pixel/plotters) should override this to use
471 /// their fill rule across every ring at once.
472 fn draw_polygon_rings(
473 &mut self,
474 rings: &[Vec<(f64, f64)>],
475 style: &RectStyle,
476 ) -> Result<(), RenderError> {
477 for (i, ring) in rings.iter().enumerate() {
478 let ring_style = if i == 0 {
479 style.clone()
480 } else {
481 RectStyle {
482 fill: None,
483 ..style.clone()
484 }
485 };
486 self.draw_polygon(ring, &ring_style)?;
487 }
488 Ok(())
489 }
490
491 /// Like [`draw_rect`](Self::draw_rect), but filled with `pattern`
492 /// instead of `style.fill` (`geom_rect`/`geom_tile`'s no-data cells,
493 /// #206) — `style.stroke`/`stroke_width`/`clip`/`corner_radius` still
494 /// apply, only the fill differs.
495 ///
496 /// Default: flattens `pattern` to its [`FillPattern::bg`] and draws an
497 /// ordinary [`draw_rect`](Self::draw_rect) — the `plotters` backend
498 /// has no pattern-fill primitive and keeps this default ("falls back
499 /// to bg"). The native SVG backend overrides this to register a
500 /// `<defs><pattern>` (deduped, this backend's own id prefix) and fill
501 /// with its `url(#…)` reference; the pixel/`canvas` backend overrides
502 /// it to rasterise the actual stripes per pixel ("raster stripes").
503 fn draw_rect_pattern(
504 &mut self,
505 top_left: (f64, f64),
506 bottom_right: (f64, f64),
507 pattern: &FillPattern,
508 style: &RectStyle,
509 ) -> Result<(), RenderError> {
510 self.draw_rect(
511 top_left,
512 bottom_right,
513 &RectStyle {
514 fill: Some(pattern.bg()),
515 ..style.clone()
516 },
517 )
518 }
519
520 /// Like [`draw_polygon`](Self::draw_polygon), but filled with
521 /// `pattern` instead of `style.fill` (#206). Default/override split:
522 /// see [`draw_rect_pattern`](Self::draw_rect_pattern).
523 fn draw_polygon_pattern(
524 &mut self,
525 points: &[(f64, f64)],
526 pattern: &FillPattern,
527 style: &RectStyle,
528 ) -> Result<(), RenderError> {
529 self.draw_polygon(
530 points,
531 &RectStyle {
532 fill: Some(pattern.bg()),
533 ..style.clone()
534 },
535 )
536 }
537
538 /// Like [`draw_polygon_rings`](Self::draw_polygon_rings), but filled
539 /// with `pattern` instead of `style.fill` (`geom_sf`/`geom_polygon`'s
540 /// no-data features, #206) — holes still cut out under the even-odd
541 /// rule, exactly as a flat fill would. Default/override split: see
542 /// [`draw_rect_pattern`](Self::draw_rect_pattern).
543 fn draw_polygon_rings_pattern(
544 &mut self,
545 rings: &[Vec<(f64, f64)>],
546 pattern: &FillPattern,
547 style: &RectStyle,
548 ) -> Result<(), RenderError> {
549 self.draw_polygon_rings(
550 rings,
551 &RectStyle {
552 fill: Some(pattern.bg()),
553 ..style.clone()
554 },
555 )
556 }
557
558 /// Like [`draw_polygon_rings`](Self::draw_polygon_rings)/
559 /// [`draw_polygon_rings_pattern`](Self::draw_polygon_rings_pattern),
560 /// but lets the caller ask for a compact path encoding at a given
561 /// decimal `precision` — relative commands after the first `M`
562 /// instead of absolute `L` per point, geometrically identical within
563 /// that precision (#214). `geom_sf` ties `precision` to its own
564 /// `simplify_px` (more simplification tolerates coarser precision);
565 /// every other caller of the two methods above is untouched, so their
566 /// output doesn't move.
567 ///
568 /// Default: `precision` is ignored and this just dispatches to
569 /// [`draw_polygon_rings`](Self::draw_polygon_rings)/
570 /// [`draw_polygon_rings_pattern`](Self::draw_polygon_rings_pattern) —
571 /// correct (if not byte-compact) for every backend, including ones
572 /// that pre-date this method. Only the native SVG backend, which
573 /// actually writes path text, overrides it to shrink the encoding
574 /// when `precision` is `Some`.
575 fn draw_polygon_rings_precise(
576 &mut self,
577 rings: &[Vec<(f64, f64)>],
578 pattern: Option<&FillPattern>,
579 precision: Option<u8>,
580 style: &RectStyle,
581 ) -> Result<(), RenderError> {
582 let _ = precision;
583 match pattern {
584 Some(p) => self.draw_polygon_rings_pattern(rings, p, style),
585 None => self.draw_polygon_rings(rings, style),
586 }
587 }
588
589 /// Like [`draw_text`](Self::draw_text), but with an optional `<title>`
590 /// tooltip carrying the *full* text when `text` itself has been
591 /// shortened (e.g. a truncated-with-ellipsis legend label, #159).
592 /// Default: ignores `title` and draws `text` plain — backends without
593 /// native tooltip support (anything but SVG) have no better option.
594 /// SVG overrides this to emit the `<title>` child, escaped the same
595 /// way every other text run is.
596 fn draw_text_with_title(
597 &mut self,
598 text: &str,
599 pos: (f64, f64),
600 style: &TextStyle,
601 _title: Option<&str>,
602 ) -> Result<(), RenderError> {
603 self.draw_text(text, pos, style)
604 }
605
606 /// Draw one line as consecutive runs, each optionally an `http(s)`
607 /// hyperlink (ggspatial-style attribution: "© BKG" linked, a licence
608 /// code linked, plain connective text between them —
609 /// `annotation_attribution`'s `.link()`/`.runs()`, #196). `pos`/`style`
610 /// place and style the line as a whole, same as [`draw_text`](Self::draw_text)
611 /// — `style.anchor` aligns the *total* run sequence, not each run.
612 ///
613 /// Default: concatenates the runs' text and draws it plain with
614 /// [`draw_text`](Self::draw_text), dropping every link — the sensible
615 /// fallback for a backend with no link primitive (pixel/`canvas`,
616 /// `plotters`). The native SVG backend overrides this to wrap each
617 /// linked run in `<a href="…" target="_blank" rel="noopener">` with an
618 /// underline, inside one `<text>` element so the anchor still aligns
619 /// the whole line.
620 fn draw_text_runs(
621 &mut self,
622 runs: &[(&str, Option<&str>)],
623 pos: (f64, f64),
624 style: &TextStyle,
625 ) -> Result<(), RenderError> {
626 let joined: String = runs.iter().map(|(t, _)| *t).collect();
627 self.draw_text(&joined, pos, style)
628 }
629 fn plot_area(&self) -> Rect;
630 fn total_area(&self) -> Rect;
631
632 /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
633 /// Backends that produce interactive output (SVG) emit it as a `<title>`
634 /// for a native hover tooltip; others ignore it. Default: no-op.
635 fn set_tooltip(&mut self, _tooltip: Option<String>) {}
636
637 /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
638 /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
639 /// Default: no-op.
640 fn set_mark_axis(&mut self, _key: Option<String>) {}
641
642 /// Attach a series key (the colour / fill / group level) to subsequently
643 /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
644 /// filter one series without parsing the tooltip. Default: no-op.
645 fn set_mark_series(&mut self, _series: Option<String>) {}
646
647 /// Attach the raw measured value (the unformatted y) to subsequently drawn
648 /// marks — SVG emits it as `data-value`. Default: no-op.
649 fn set_mark_value(&mut self, _value: Option<String>) {}
650
651 /// Tag subsequently drawn elements as data marks of the named geom (the
652 /// layer's `Geom::name`) until cleared with `None`. SVG emits
653 /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
654 /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
655 /// marks without matching axes, grid lines or legends. Default: no-op.
656 fn set_mark_geom(&mut self, _geom: Option<&str>) {}
657
658 /// Report a non-fatal problem found while drawing (e.g. labels a repel
659 /// layout had to drop). Backends that collect warnings (the native SVG
660 /// backend) append it to the render warnings. Default: ignored.
661 fn warn(&mut self, _message: String) {}
662
663 /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
664 /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
665 /// path/line). While set, the SVG backend writes `data-part="<part>"
666 /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
667 /// decorative; the parent mark already carries the semantic info).
668 /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
669 /// still written. Default: no-op.
670 fn set_mark_part(&mut self, _part: Option<&str>) {}
671
672 /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
673 /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
674 /// attr with the same name as a built-in (`data-x`, `data-series`,
675 /// `data-value`) replaces it rather than duplicating it. An empty
676 /// `Vec` clears any previously set attrs. Default: no-op.
677 fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
678
679 /// Whether the caller asked for per-mark formatted values
680 /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
681 /// geom checks before doing the (otherwise pointless) work of resolving
682 /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
683 /// false, so the feature is byte-identical when never turned on.
684 fn wants_formatted_values(&self) -> bool {
685 false
686 }
687
688 /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
689 /// raw data value (y for most geoms, fill for hex/tile), formatted
690 /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
691 /// de-DE currency string. Only meaningful while
692 /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
693 /// `None` clears any previous mark's value, per row. Default: no-op.
694 fn set_mark_fvalue(&mut self, _value: Option<String>) {}
695
696 /// `data-fx` on subsequently-drawn marks: the x value formatted through
697 /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
698 /// backend omits it when it matches the already-written `data-x`, so a
699 /// plain numeric x doesn't get a redundant duplicate attribute. Default:
700 /// no-op.
701 fn set_mark_fx(&mut self, _value: Option<String>) {}
702
703 /// Extra CSS class tokens for subsequently-drawn marks (#221's
704 /// `Aes::class`), space-separated and already validated by the caller;
705 /// `None` clears them, per row. The SVG backend appends them to the
706 /// mark's own `class="gg-mark gg-<kind>"` (re-validating every token as
707 /// defence in depth). Default: no-op.
708 fn set_mark_class(&mut self, _class: Option<String>) {}
709
710 /// Put subsequently-drawn marks into the caller-named group
711 /// `(attribute name, value)` — e.g. `("data-of", "node-7")` — until
712 /// cleared with `None` (#221's `Aes::group_wrap`). The SVG backend
713 /// collects every mark with the same pair into one `<g name="value">`,
714 /// placed where the group's first mark was drawn. Default: no-op (a
715 /// raster backend has no grouping).
716 fn set_mark_group(&mut self, _group: Option<(String, String)>) {}
717
718 /// Per-layer SVG attributes for every mark the current layer draws
719 /// (#221's [`GGPlot::layer_svg_attr`](crate::plot::GGPlot::layer_svg_attr)):
720 /// `class` tokens, `aria-hidden`, `pointer-events`, `data-*`, already
721 /// validated by the caller. An empty `Vec` clears them (set before a
722 /// layer draws, cleared after). Default: no-op.
723 fn set_layer_attrs(&mut self, _attrs: Vec<(String, String)>) {}
724
725 /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
726 fn draw_shape(
727 &mut self,
728 center: (f64, f64),
729 radius: f64,
730 style: &PointStyle,
731 ) -> Result<(), RenderError> {
732 match style.shape {
733 PointShape::Circle => self.draw_circle(center, radius, style),
734 _ => {
735 // Default: fall back to circle for unsupported backends
736 self.draw_circle(center, radius, style)
737 }
738 }
739 }
740
741 /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
742 /// `ctrl`, in pixel space. The default implementation flattens the curve
743 /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
744 /// backends that support real curves (SVG) should override this.
745 /// Non-finite input draws nothing.
746 fn draw_quadratic(
747 &mut self,
748 p0: (f64, f64),
749 ctrl: (f64, f64),
750 p1: (f64, f64),
751 style: &LineStyle,
752 ) -> Result<(), RenderError> {
753 if !(p0.0.is_finite()
754 && p0.1.is_finite()
755 && ctrl.0.is_finite()
756 && ctrl.1.is_finite()
757 && p1.0.is_finite()
758 && p1.1.is_finite())
759 {
760 return Ok(());
761 }
762 const STEPS: usize = 32;
763 let mut pts = Vec::with_capacity(STEPS + 1);
764 for i in 0..=STEPS {
765 let t = i as f64 / STEPS as f64;
766 let one_minus_t = 1.0 - t;
767 let x =
768 one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
769 let y =
770 one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
771 pts.push((x, y));
772 }
773 self.draw_line(&pts, style)
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::render::{Rect, RenderError};
781 use std::cell::RefCell;
782
783 /// A minimal backend that does not override `draw_quadratic`, to exercise
784 /// the trait's default flattening implementation.
785 /// One `draw_polygon` call's recorded points and fill colour.
786 type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
787 /// One `draw_rect` call's corners and fill colour.
788 type RecordedRect = ((f64, f64), (f64, f64), Option<(u8, u8, u8)>);
789
790 #[derive(Default)]
791 struct RecordingBackend {
792 lines: RefCell<Vec<Vec<(f64, f64)>>>,
793 circles: RefCell<Vec<((f64, f64), f64)>>,
794 polygons: RefCell<Vec<RecordedPolygon>>,
795 rects: RefCell<Vec<RecordedRect>>,
796 }
797
798 impl DrawBackend for RecordingBackend {
799 fn draw_circle(
800 &mut self,
801 center: (f64, f64),
802 radius: f64,
803 _style: &PointStyle,
804 ) -> Result<(), RenderError> {
805 self.circles.borrow_mut().push((center, radius));
806 Ok(())
807 }
808 fn draw_line(
809 &mut self,
810 points: &[(f64, f64)],
811 _style: &LineStyle,
812 ) -> Result<(), RenderError> {
813 self.lines.borrow_mut().push(points.to_vec());
814 Ok(())
815 }
816 fn draw_rect(
817 &mut self,
818 top_left: (f64, f64),
819 bottom_right: (f64, f64),
820 style: &RectStyle,
821 ) -> Result<(), RenderError> {
822 self.rects
823 .borrow_mut()
824 .push((top_left, bottom_right, style.fill));
825 Ok(())
826 }
827 fn draw_text(
828 &mut self,
829 _text: &str,
830 _pos: (f64, f64),
831 _style: &TextStyle,
832 ) -> Result<(), RenderError> {
833 Ok(())
834 }
835 fn draw_polygon(
836 &mut self,
837 points: &[(f64, f64)],
838 style: &RectStyle,
839 ) -> Result<(), RenderError> {
840 self.polygons
841 .borrow_mut()
842 .push((points.to_vec(), style.fill));
843 Ok(())
844 }
845 fn plot_area(&self) -> Rect {
846 Rect {
847 x: 0.0,
848 y: 0.0,
849 width: 100.0,
850 height: 100.0,
851 }
852 }
853 fn total_area(&self) -> Rect {
854 self.plot_area()
855 }
856 }
857
858 #[test]
859 fn default_draw_quadratic_flattens_to_33_points() {
860 let mut b = RecordingBackend::default();
861 b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
862 .unwrap();
863 let lines = b.lines.borrow();
864 assert_eq!(lines.len(), 1);
865 assert_eq!(lines[0].len(), 33);
866 assert_eq!(lines[0][0], (0.0, 0.0));
867 assert_eq!(lines[0][32], (10.0, 0.0));
868 // Midpoint of a quadratic Bézier at t=0.5 is the average of the
869 // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
870 let mid = lines[0][16];
871 assert!((mid.0 - 5.0).abs() < 1e-9);
872 assert!((mid.1 - 5.0).abs() < 1e-9);
873 }
874
875 #[test]
876 fn default_draw_quadratic_drops_non_finite() {
877 let mut b = RecordingBackend::default();
878 b.draw_quadratic(
879 (f64::NAN, 0.0),
880 (5.0, 10.0),
881 (10.0, 0.0),
882 &LineStyle::default(),
883 )
884 .unwrap();
885 assert!(b.lines.borrow().is_empty());
886 }
887
888 /// The default `draw_shape` falls back to `draw_circle` for every
889 /// `PointShape` (not just `Circle`) on a backend that doesn't override
890 /// it — e.g. a minimal backend that only implements the required
891 /// methods.
892 #[test]
893 fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
894 let mut b = RecordingBackend::default();
895 for shape in PointShape::ALL {
896 b.draw_shape(
897 (1.0, 2.0),
898 3.0,
899 &PointStyle {
900 shape: *shape,
901 ..Default::default()
902 },
903 )
904 .unwrap();
905 }
906 assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
907 for (center, radius) in b.circles.borrow().iter() {
908 assert_eq!(*center, (1.0, 2.0));
909 assert_eq!(*radius, 3.0);
910 }
911 }
912
913 /// The default `draw_polygon_rings` (for a backend pre-dating #178, or
914 /// any third-party `DrawBackend` that never overrides it) falls back to
915 /// one `draw_polygon` call per ring: the exterior filled, every other
916 /// ring (a hole) with its fill cleared — the old approximation, not a
917 /// real cut-out, but it draws *something* rather than nothing.
918 #[test]
919 fn default_draw_polygon_rings_fills_only_the_exterior() {
920 let mut b = RecordingBackend::default();
921 let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
922 let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
923 b.draw_polygon_rings(
924 &[exterior.clone(), hole.clone()],
925 &RectStyle {
926 fill: Some((200, 50, 50)),
927 stroke: Some((0, 0, 0)),
928 stroke_width: 1.0,
929 alpha: 1.0,
930 clip: true,
931 corner_radius: 0.0,
932 },
933 )
934 .unwrap();
935 let polys = b.polygons.borrow();
936 assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
937 assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
938 assert_eq!(polys[1].0, hole);
939 assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
940 }
941
942 // #206: the default `draw_*_pattern` methods (a backend with no
943 // pattern-fill primitive — `plotters` is the real-world case, this
944 // mock stands in for it) flatten to the pattern's own `bg` and call
945 // the plain draw method, unchanged otherwise.
946
947 fn stripe() -> FillPattern {
948 FillPattern::stripe((10, 20, 30), (200, 210, 220))
949 }
950
951 #[test]
952 fn default_draw_rect_pattern_flattens_to_bg() {
953 let mut b = RecordingBackend::default();
954 b.draw_rect_pattern(
955 (0.0, 0.0),
956 (10.0, 10.0),
957 &stripe(),
958 &RectStyle {
959 stroke: Some((0, 0, 0)),
960 ..Default::default()
961 },
962 )
963 .unwrap();
964 let rects = b.rects.borrow();
965 assert_eq!(rects.len(), 1);
966 assert_eq!(rects[0], ((0.0, 0.0), (10.0, 10.0), Some(stripe().bg())));
967 }
968
969 #[test]
970 fn default_draw_polygon_pattern_flattens_to_bg() {
971 let mut b = RecordingBackend::default();
972 let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
973 b.draw_polygon_pattern(&pts, &stripe(), &RectStyle::default())
974 .unwrap();
975 let polys = b.polygons.borrow();
976 assert_eq!(polys.len(), 1);
977 assert_eq!(polys[0], (pts, Some(stripe().bg())));
978 }
979
980 #[test]
981 fn default_draw_polygon_rings_pattern_flattens_to_bg_per_ring() {
982 let mut b = RecordingBackend::default();
983 let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
984 let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
985 b.draw_polygon_rings_pattern(&[exterior.clone(), hole], &stripe(), &RectStyle::default())
986 .unwrap();
987 let polys = b.polygons.borrow();
988 // Same fallback shape as draw_polygon_rings's own default: the
989 // exterior filled (with the pattern's bg), the hole not.
990 assert_eq!(polys[0], (exterior, Some(stripe().bg())));
991 assert_eq!(polys[1].1, None);
992 }
993
994 #[test]
995 fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
996 let mut b = RecordingBackend::default();
997 let pa = b.plot_area();
998 let ta = b.total_area();
999 assert_eq!(
1000 (pa.x, pa.y, pa.width, pa.height),
1001 (ta.x, ta.y, ta.width, ta.height)
1002 );
1003 assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
1004 b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
1005 .unwrap();
1006 b.draw_text("label", (0.0, 0.0), &TextStyle::default())
1007 .unwrap();
1008 b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
1009 .unwrap();
1010 }
1011}