Skip to main content

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}