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