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    /// Draw an encoded raster image (PNG or JPEG bytes) stretched over
646    /// `rect`, clipped to `clip`, at opacity `alpha` — a map tile (#325).
647    /// Default: draws nothing (a backend without raster support).
648    fn draw_image(
649        &mut self,
650        _bytes: &[u8],
651        _rect: &Rect,
652        _clip: &Rect,
653        _alpha: f64,
654    ) -> Result<(), RenderError> {
655        Ok(())
656    }
657
658    fn plot_area(&self) -> Rect;
659    fn total_area(&self) -> Rect;
660
661    /// Every box already claimed in a panel corner via
662    /// [`Self::claim_corner_box`] so far this render pass — the scale
663    /// bar, attribution text, bivariate legend, or an inside ordinary
664    /// legend already placed (`render::corner_layout`, #273). Default:
665    /// always empty — a backend that never overrides this (anything that
666    /// pre-dates #273, including a third-party `impl DrawBackend`) keeps
667    /// every corner item positioned exactly as before, byte-identical.
668    fn corner_claims(&self) -> Vec<Rect> {
669        Vec::new()
670    }
671
672    /// Register `rect` as claimed in a panel corner, for a later corner
673    /// geom in the same render pass to see via [`Self::corner_claims`]
674    /// (#273). Default: no-op, paired with that method's empty default.
675    fn claim_corner_box(&mut self, _rect: Rect) {}
676
677    /// Clear every box claimed via [`Self::claim_corner_box`] — called
678    /// once per panel (`renderer.rs`), so a facet's second panel doesn't
679    /// see the first panel's claims (#273). Default: no-op, paired with
680    /// the above.
681    fn clear_corner_claims(&mut self) {}
682
683    /// Start one panel of a fixed-aspect `facet_wrap` (e.g. `coord_sf` small
684    /// multiples): marks until [`end_panel`](Self::end_panel) belong to
685    /// panel `index` (strip label `label`), clipped to `clip`; `data` is the
686    /// rect the coord maps the data window onto. The SVG backend wraps them
687    /// in a clipped `<g data-facet-panel>` and lists every panel on the root
688    /// as `data-panels`. Default: no-op.
689    fn begin_panel(
690        &mut self,
691        _index: usize,
692        _label: &str,
693        _clip: &crate::render::Rect,
694        _data: &crate::render::Rect,
695    ) {
696    }
697
698    /// End the panel started by [`begin_panel`](Self::begin_panel).
699    /// Default: no-op.
700    fn end_panel(&mut self) {}
701
702    /// The rect a single-panel plot's coord maps the data window onto, when
703    /// it differs from the plot area (a letterboxing coord such as
704    /// `coord_sf`). The SVG backend writes it as the root `data-viewport`.
705    /// Default: no-op.
706    fn set_viewport(&mut self, _rect: &crate::render::Rect) {}
707
708    /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
709    /// Backends that produce interactive output (SVG) emit it as a `<title>`
710    /// for a native hover tooltip; others ignore it. Default: no-op.
711    fn set_tooltip(&mut self, _tooltip: Option<String>) {}
712
713    /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
714    /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
715    /// Default: no-op.
716    fn set_mark_axis(&mut self, _key: Option<String>) {}
717
718    /// Attach a series key (the colour / fill / group level) to subsequently
719    /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
720    /// filter one series without parsing the tooltip. Default: no-op.
721    fn set_mark_series(&mut self, _series: Option<String>) {}
722
723    /// Attach the raw measured value (the unformatted y) to subsequently drawn
724    /// marks — SVG emits it as `data-value`. Default: no-op.
725    fn set_mark_value(&mut self, _value: Option<String>) {}
726
727    /// Tag subsequently drawn elements as data marks of the named geom (the
728    /// layer's `Geom::name`) until cleared with `None`. SVG emits
729    /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
730    /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
731    /// marks without matching axes, grid lines or legends. Default: no-op.
732    fn set_mark_geom(&mut self, _geom: Option<&str>) {}
733
734    /// Report a non-fatal problem found while drawing (e.g. labels a repel
735    /// layout had to drop). Backends that collect warnings (the native SVG
736    /// backend) append it to the render warnings. Default: ignored.
737    fn warn(&mut self, _message: String) {}
738
739    /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
740    /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
741    /// path/line). While set, the SVG backend writes `data-part="<part>"
742    /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
743    /// decorative; the parent mark already carries the semantic info).
744    /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
745    /// still written. Default: no-op.
746    fn set_mark_part(&mut self, _part: Option<&str>) {}
747
748    /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
749    /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
750    /// attr with the same name as a built-in (`data-x`, `data-series`,
751    /// `data-value`) replaces it rather than duplicating it. An empty
752    /// `Vec` clears any previously set attrs. Default: no-op.
753    fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
754
755    /// Whether the caller asked for per-mark formatted values
756    /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
757    /// geom checks before doing the (otherwise pointless) work of resolving
758    /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
759    /// false, so the feature is byte-identical when never turned on.
760    fn wants_formatted_values(&self) -> bool {
761        false
762    }
763
764    /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
765    /// raw data value (y for most geoms, fill for hex/tile), formatted
766    /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
767    /// de-DE currency string. Only meaningful while
768    /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
769    /// `None` clears any previous mark's value, per row. Default: no-op.
770    fn set_mark_fvalue(&mut self, _value: Option<String>) {}
771
772    /// `data-fx` on subsequently-drawn marks: the x value formatted through
773    /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
774    /// backend omits it when it matches the already-written `data-x`, so a
775    /// plain numeric x doesn't get a redundant duplicate attribute. Default:
776    /// no-op.
777    fn set_mark_fx(&mut self, _value: Option<String>) {}
778
779    /// Extra CSS class tokens for subsequently-drawn marks (#221's
780    /// `Aes::class`), space-separated and already validated by the caller;
781    /// `None` clears them, per row. The SVG backend appends them to the
782    /// mark's own `class="gg-mark gg-<kind>"` (re-validating every token as
783    /// defence in depth). Default: no-op.
784    fn set_mark_class(&mut self, _class: Option<String>) {}
785
786    /// Put subsequently-drawn marks into the caller-named group
787    /// `(attribute name, value)` — e.g. `("data-of", "node-7")` — until
788    /// cleared with `None` (#221's `Aes::group_wrap`). The SVG backend
789    /// collects every mark with the same pair into one `<g name="value">`,
790    /// placed where the group's first mark was drawn. Default: no-op (a
791    /// raster backend has no grouping).
792    fn set_mark_group(&mut self, _group: Option<(String, String)>) {}
793
794    /// Per-layer SVG attributes for every mark the current layer draws
795    /// (#221's [`GGPlot::layer_svg_attr`](crate::plot::GGPlot::layer_svg_attr)):
796    /// `class` tokens, `aria-hidden`, `pointer-events`, `data-*`, already
797    /// validated by the caller. An empty `Vec` clears them (set before a
798    /// layer draws, cleared after). Default: no-op.
799    fn set_layer_attrs(&mut self, _attrs: Vec<(String, String)>) {}
800
801    /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
802    fn draw_shape(
803        &mut self,
804        center: (f64, f64),
805        radius: f64,
806        style: &PointStyle,
807    ) -> Result<(), RenderError> {
808        match style.shape {
809            PointShape::Circle => self.draw_circle(center, radius, style),
810            _ => {
811                // Default: fall back to circle for unsupported backends
812                self.draw_circle(center, radius, style)
813            }
814        }
815    }
816
817    /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
818    /// `ctrl`, in pixel space. The default implementation flattens the curve
819    /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
820    /// backends that support real curves (SVG) should override this.
821    /// Non-finite input draws nothing.
822    fn draw_quadratic(
823        &mut self,
824        p0: (f64, f64),
825        ctrl: (f64, f64),
826        p1: (f64, f64),
827        style: &LineStyle,
828    ) -> Result<(), RenderError> {
829        if !(p0.0.is_finite()
830            && p0.1.is_finite()
831            && ctrl.0.is_finite()
832            && ctrl.1.is_finite()
833            && p1.0.is_finite()
834            && p1.1.is_finite())
835        {
836            return Ok(());
837        }
838        const STEPS: usize = 32;
839        let mut pts = Vec::with_capacity(STEPS + 1);
840        for i in 0..=STEPS {
841            let t = i as f64 / STEPS as f64;
842            let one_minus_t = 1.0 - t;
843            let x =
844                one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
845            let y =
846                one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
847            pts.push((x, y));
848        }
849        self.draw_line(&pts, style)
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856    use crate::render::{Rect, RenderError};
857    use std::cell::RefCell;
858
859    /// A minimal backend that does not override `draw_quadratic`, to exercise
860    /// the trait's default flattening implementation.
861    /// One `draw_polygon` call's recorded points and fill colour.
862    type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
863    /// One `draw_rect` call's corners and fill colour.
864    type RecordedRect = ((f64, f64), (f64, f64), Option<(u8, u8, u8)>);
865
866    #[derive(Default)]
867    struct RecordingBackend {
868        lines: RefCell<Vec<Vec<(f64, f64)>>>,
869        circles: RefCell<Vec<((f64, f64), f64)>>,
870        polygons: RefCell<Vec<RecordedPolygon>>,
871        rects: RefCell<Vec<RecordedRect>>,
872    }
873
874    impl DrawBackend for RecordingBackend {
875        fn draw_circle(
876            &mut self,
877            center: (f64, f64),
878            radius: f64,
879            _style: &PointStyle,
880        ) -> Result<(), RenderError> {
881            self.circles.borrow_mut().push((center, radius));
882            Ok(())
883        }
884        fn draw_line(
885            &mut self,
886            points: &[(f64, f64)],
887            _style: &LineStyle,
888        ) -> Result<(), RenderError> {
889            self.lines.borrow_mut().push(points.to_vec());
890            Ok(())
891        }
892        fn draw_rect(
893            &mut self,
894            top_left: (f64, f64),
895            bottom_right: (f64, f64),
896            style: &RectStyle,
897        ) -> Result<(), RenderError> {
898            self.rects
899                .borrow_mut()
900                .push((top_left, bottom_right, style.fill));
901            Ok(())
902        }
903        fn draw_text(
904            &mut self,
905            _text: &str,
906            _pos: (f64, f64),
907            _style: &TextStyle,
908        ) -> Result<(), RenderError> {
909            Ok(())
910        }
911        fn draw_polygon(
912            &mut self,
913            points: &[(f64, f64)],
914            style: &RectStyle,
915        ) -> Result<(), RenderError> {
916            self.polygons
917                .borrow_mut()
918                .push((points.to_vec(), style.fill));
919            Ok(())
920        }
921        fn plot_area(&self) -> Rect {
922            Rect {
923                x: 0.0,
924                y: 0.0,
925                width: 100.0,
926                height: 100.0,
927            }
928        }
929        fn total_area(&self) -> Rect {
930            self.plot_area()
931        }
932    }
933
934    #[test]
935    fn default_draw_quadratic_flattens_to_33_points() {
936        let mut b = RecordingBackend::default();
937        b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
938            .unwrap();
939        let lines = b.lines.borrow();
940        assert_eq!(lines.len(), 1);
941        assert_eq!(lines[0].len(), 33);
942        assert_eq!(lines[0][0], (0.0, 0.0));
943        assert_eq!(lines[0][32], (10.0, 0.0));
944        // Midpoint of a quadratic Bézier at t=0.5 is the average of the
945        // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
946        let mid = lines[0][16];
947        assert!((mid.0 - 5.0).abs() < 1e-9);
948        assert!((mid.1 - 5.0).abs() < 1e-9);
949    }
950
951    #[test]
952    fn default_draw_image_draws_nothing() {
953        let mut b = RecordingBackend::default();
954        let r = b.plot_area();
955        b.draw_image(b"png", &r, &r, 1.0).unwrap();
956        assert!(b.rects.borrow().is_empty() && b.polygons.borrow().is_empty());
957    }
958
959    #[test]
960    fn default_draw_quadratic_drops_non_finite() {
961        let mut b = RecordingBackend::default();
962        b.draw_quadratic(
963            (f64::NAN, 0.0),
964            (5.0, 10.0),
965            (10.0, 0.0),
966            &LineStyle::default(),
967        )
968        .unwrap();
969        assert!(b.lines.borrow().is_empty());
970    }
971
972    /// The default `draw_shape` falls back to `draw_circle` for every
973    /// `PointShape` (not just `Circle`) on a backend that doesn't override
974    /// it — e.g. a minimal backend that only implements the required
975    /// methods.
976    #[test]
977    fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
978        let mut b = RecordingBackend::default();
979        for shape in PointShape::ALL {
980            b.draw_shape(
981                (1.0, 2.0),
982                3.0,
983                &PointStyle {
984                    shape: *shape,
985                    ..Default::default()
986                },
987            )
988            .unwrap();
989        }
990        assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
991        for (center, radius) in b.circles.borrow().iter() {
992            assert_eq!(*center, (1.0, 2.0));
993            assert_eq!(*radius, 3.0);
994        }
995    }
996
997    /// The default `draw_polygon_rings` (for a backend pre-dating #178, or
998    /// any third-party `DrawBackend` that never overrides it) falls back to
999    /// one `draw_polygon` call per ring: the exterior filled, every other
1000    /// ring (a hole) with its fill cleared — the old approximation, not a
1001    /// real cut-out, but it draws *something* rather than nothing.
1002    #[test]
1003    fn default_draw_polygon_rings_fills_only_the_exterior() {
1004        let mut b = RecordingBackend::default();
1005        let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1006        let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
1007        b.draw_polygon_rings(
1008            &[exterior.clone(), hole.clone()],
1009            &RectStyle {
1010                fill: Some((200, 50, 50)),
1011                stroke: Some((0, 0, 0)),
1012                stroke_width: 1.0,
1013                alpha: 1.0,
1014                clip: true,
1015                corner_radius: 0.0,
1016            },
1017        )
1018        .unwrap();
1019        let polys = b.polygons.borrow();
1020        assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
1021        assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
1022        assert_eq!(polys[1].0, hole);
1023        assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
1024    }
1025
1026    // #206: the default `draw_*_pattern` methods (a backend with no
1027    // pattern-fill primitive — `plotters` is the real-world case, this
1028    // mock stands in for it) flatten to the pattern's own `bg` and call
1029    // the plain draw method, unchanged otherwise.
1030
1031    fn stripe() -> FillPattern {
1032        FillPattern::stripe((10, 20, 30), (200, 210, 220))
1033    }
1034
1035    #[test]
1036    fn default_draw_rect_pattern_flattens_to_bg() {
1037        let mut b = RecordingBackend::default();
1038        b.draw_rect_pattern(
1039            (0.0, 0.0),
1040            (10.0, 10.0),
1041            &stripe(),
1042            &RectStyle {
1043                stroke: Some((0, 0, 0)),
1044                ..Default::default()
1045            },
1046        )
1047        .unwrap();
1048        let rects = b.rects.borrow();
1049        assert_eq!(rects.len(), 1);
1050        assert_eq!(rects[0], ((0.0, 0.0), (10.0, 10.0), Some(stripe().bg())));
1051    }
1052
1053    #[test]
1054    fn default_draw_polygon_pattern_flattens_to_bg() {
1055        let mut b = RecordingBackend::default();
1056        let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
1057        b.draw_polygon_pattern(&pts, &stripe(), &RectStyle::default())
1058            .unwrap();
1059        let polys = b.polygons.borrow();
1060        assert_eq!(polys.len(), 1);
1061        assert_eq!(polys[0], (pts, Some(stripe().bg())));
1062    }
1063
1064    #[test]
1065    fn default_draw_polygon_rings_pattern_flattens_to_bg_per_ring() {
1066        let mut b = RecordingBackend::default();
1067        let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1068        let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
1069        b.draw_polygon_rings_pattern(&[exterior.clone(), hole], &stripe(), &RectStyle::default())
1070            .unwrap();
1071        let polys = b.polygons.borrow();
1072        // Same fallback shape as draw_polygon_rings's own default: the
1073        // exterior filled (with the pattern's bg), the hole not.
1074        assert_eq!(polys[0], (exterior, Some(stripe().bg())));
1075        assert_eq!(polys[1].1, None);
1076    }
1077
1078    #[test]
1079    fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
1080        let mut b = RecordingBackend::default();
1081        let pa = b.plot_area();
1082        let ta = b.total_area();
1083        assert_eq!(
1084            (pa.x, pa.y, pa.width, pa.height),
1085            (ta.x, ta.y, ta.width, ta.height)
1086        );
1087        assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
1088        b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
1089            .unwrap();
1090        b.draw_text("label", (0.0, 0.0), &TextStyle::default())
1091            .unwrap();
1092        b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
1093            .unwrap();
1094    }
1095}