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