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}
125
126impl Default for RectStyle {
127    fn default() -> Self {
128        RectStyle {
129            fill: Some((128, 128, 128)),
130            stroke: None,
131            stroke_width: 1.0,
132            alpha: 1.0,
133            clip: true,
134        }
135    }
136}
137
138/// Style for drawing text.
139#[derive(Clone, Debug)]
140pub struct TextStyle {
141    pub color: (u8, u8, u8),
142    pub size: f64,
143    pub anchor: TextAnchor,
144    pub angle: f64,
145    /// Font family (e.g., "serif", "monospace"). None defaults to "sans-serif".
146    pub family: Option<String>,
147    /// Font face (R's `element_text(face = ...)`).
148    pub face: FontFace,
149}
150
151/// Font face / weight (R's `element_text(face = ...)`).
152#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
153pub enum FontFace {
154    #[default]
155    Plain,
156    Bold,
157    Italic,
158}
159
160impl Default for TextStyle {
161    fn default() -> Self {
162        TextStyle {
163            color: (50, 50, 50),
164            size: 12.0,
165            anchor: TextAnchor::Middle,
166            angle: 0.0,
167            family: None,
168            face: crate::render::backend::FontFace::Plain,
169        }
170    }
171}
172
173#[derive(Clone, Debug)]
174pub enum TextAnchor {
175    Start,
176    Middle,
177    End,
178}
179
180/// Our rendering abstraction, independent of plotters details.
181pub trait DrawBackend {
182    fn draw_circle(
183        &mut self,
184        center: (f64, f64),
185        radius: f64,
186        style: &PointStyle,
187    ) -> Result<(), RenderError>;
188    fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError>;
189    fn draw_rect(
190        &mut self,
191        top_left: (f64, f64),
192        bottom_right: (f64, f64),
193        style: &RectStyle,
194    ) -> Result<(), RenderError>;
195    fn draw_text(
196        &mut self,
197        text: &str,
198        pos: (f64, f64),
199        style: &TextStyle,
200    ) -> Result<(), RenderError>;
201    fn draw_polygon(&mut self, points: &[(f64, f64)], style: &RectStyle)
202        -> Result<(), RenderError>;
203    fn plot_area(&self) -> Rect;
204    fn total_area(&self) -> Rect;
205
206    /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
207    /// Backends that produce interactive output (SVG) emit it as a `<title>`
208    /// for a native hover tooltip; others ignore it. Default: no-op.
209    fn set_tooltip(&mut self, _tooltip: Option<String>) {}
210
211    /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
212    /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
213    /// Default: no-op.
214    fn set_mark_axis(&mut self, _key: Option<String>) {}
215
216    /// Attach a series key (the colour / fill / group level) to subsequently
217    /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
218    /// filter one series without parsing the tooltip. Default: no-op.
219    fn set_mark_series(&mut self, _series: Option<String>) {}
220
221    /// Attach the raw measured value (the unformatted y) to subsequently drawn
222    /// marks — SVG emits it as `data-value`. Default: no-op.
223    fn set_mark_value(&mut self, _value: Option<String>) {}
224
225    /// Tag subsequently drawn elements as data marks of the named geom (the
226    /// layer's `Geom::name`) until cleared with `None`. SVG emits
227    /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
228    /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
229    /// marks without matching axes, grid lines or legends. Default: no-op.
230    fn set_mark_geom(&mut self, _geom: Option<&str>) {}
231
232    /// Report a non-fatal problem found while drawing (e.g. labels a repel
233    /// layout had to drop). Backends that collect warnings (the native SVG
234    /// backend) append it to the render warnings. Default: ignored.
235    fn warn(&mut self, _message: String) {}
236
237    /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
238    /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
239    /// path/line). While set, the SVG backend writes `data-part="<part>"
240    /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
241    /// decorative; the parent mark already carries the semantic info).
242    /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
243    /// still written. Default: no-op.
244    fn set_mark_part(&mut self, _part: Option<&str>) {}
245
246    /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
247    /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
248    /// attr with the same name as a built-in (`data-x`, `data-series`,
249    /// `data-value`) replaces it rather than duplicating it. An empty
250    /// `Vec` clears any previously set attrs. Default: no-op.
251    fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
252
253    /// Whether the caller asked for per-mark formatted values
254    /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
255    /// geom checks before doing the (otherwise pointless) work of resolving
256    /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
257    /// false, so the feature is byte-identical when never turned on.
258    fn wants_formatted_values(&self) -> bool {
259        false
260    }
261
262    /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
263    /// raw data value (y for most geoms, fill for hex/tile), formatted
264    /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
265    /// de-DE currency string. Only meaningful while
266    /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
267    /// `None` clears any previous mark's value, per row. Default: no-op.
268    fn set_mark_fvalue(&mut self, _value: Option<String>) {}
269
270    /// `data-fx` on subsequently-drawn marks: the x value formatted through
271    /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
272    /// backend omits it when it matches the already-written `data-x`, so a
273    /// plain numeric x doesn't get a redundant duplicate attribute. Default:
274    /// no-op.
275    fn set_mark_fx(&mut self, _value: Option<String>) {}
276
277    /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
278    fn draw_shape(
279        &mut self,
280        center: (f64, f64),
281        radius: f64,
282        style: &PointStyle,
283    ) -> Result<(), RenderError> {
284        match style.shape {
285            PointShape::Circle => self.draw_circle(center, radius, style),
286            _ => {
287                // Default: fall back to circle for unsupported backends
288                self.draw_circle(center, radius, style)
289            }
290        }
291    }
292
293    /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
294    /// `ctrl`, in pixel space. The default implementation flattens the curve
295    /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
296    /// backends that support real curves (SVG) should override this.
297    /// Non-finite input draws nothing.
298    fn draw_quadratic(
299        &mut self,
300        p0: (f64, f64),
301        ctrl: (f64, f64),
302        p1: (f64, f64),
303        style: &LineStyle,
304    ) -> Result<(), RenderError> {
305        if !(p0.0.is_finite()
306            && p0.1.is_finite()
307            && ctrl.0.is_finite()
308            && ctrl.1.is_finite()
309            && p1.0.is_finite()
310            && p1.1.is_finite())
311        {
312            return Ok(());
313        }
314        const STEPS: usize = 32;
315        let mut pts = Vec::with_capacity(STEPS + 1);
316        for i in 0..=STEPS {
317            let t = i as f64 / STEPS as f64;
318            let one_minus_t = 1.0 - t;
319            let x =
320                one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
321            let y =
322                one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
323            pts.push((x, y));
324        }
325        self.draw_line(&pts, style)
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::render::{Rect, RenderError};
333    use std::cell::RefCell;
334
335    /// A minimal backend that does not override `draw_quadratic`, to exercise
336    /// the trait's default flattening implementation.
337    #[derive(Default)]
338    struct RecordingBackend {
339        lines: RefCell<Vec<Vec<(f64, f64)>>>,
340        circles: RefCell<Vec<((f64, f64), f64)>>,
341    }
342
343    impl DrawBackend for RecordingBackend {
344        fn draw_circle(
345            &mut self,
346            center: (f64, f64),
347            radius: f64,
348            _style: &PointStyle,
349        ) -> Result<(), RenderError> {
350            self.circles.borrow_mut().push((center, radius));
351            Ok(())
352        }
353        fn draw_line(
354            &mut self,
355            points: &[(f64, f64)],
356            _style: &LineStyle,
357        ) -> Result<(), RenderError> {
358            self.lines.borrow_mut().push(points.to_vec());
359            Ok(())
360        }
361        fn draw_rect(
362            &mut self,
363            _top_left: (f64, f64),
364            _bottom_right: (f64, f64),
365            _style: &RectStyle,
366        ) -> Result<(), RenderError> {
367            Ok(())
368        }
369        fn draw_text(
370            &mut self,
371            _text: &str,
372            _pos: (f64, f64),
373            _style: &TextStyle,
374        ) -> Result<(), RenderError> {
375            Ok(())
376        }
377        fn draw_polygon(
378            &mut self,
379            _points: &[(f64, f64)],
380            _style: &RectStyle,
381        ) -> Result<(), RenderError> {
382            Ok(())
383        }
384        fn plot_area(&self) -> Rect {
385            Rect {
386                x: 0.0,
387                y: 0.0,
388                width: 100.0,
389                height: 100.0,
390            }
391        }
392        fn total_area(&self) -> Rect {
393            self.plot_area()
394        }
395    }
396
397    #[test]
398    fn default_draw_quadratic_flattens_to_33_points() {
399        let mut b = RecordingBackend::default();
400        b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
401            .unwrap();
402        let lines = b.lines.borrow();
403        assert_eq!(lines.len(), 1);
404        assert_eq!(lines[0].len(), 33);
405        assert_eq!(lines[0][0], (0.0, 0.0));
406        assert_eq!(lines[0][32], (10.0, 0.0));
407        // Midpoint of a quadratic Bézier at t=0.5 is the average of the
408        // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
409        let mid = lines[0][16];
410        assert!((mid.0 - 5.0).abs() < 1e-9);
411        assert!((mid.1 - 5.0).abs() < 1e-9);
412    }
413
414    #[test]
415    fn default_draw_quadratic_drops_non_finite() {
416        let mut b = RecordingBackend::default();
417        b.draw_quadratic(
418            (f64::NAN, 0.0),
419            (5.0, 10.0),
420            (10.0, 0.0),
421            &LineStyle::default(),
422        )
423        .unwrap();
424        assert!(b.lines.borrow().is_empty());
425    }
426
427    /// The default `draw_shape` falls back to `draw_circle` for every
428    /// `PointShape` (not just `Circle`) on a backend that doesn't override
429    /// it — e.g. a minimal backend that only implements the required
430    /// methods.
431    #[test]
432    fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
433        let mut b = RecordingBackend::default();
434        for shape in PointShape::ALL {
435            b.draw_shape(
436                (1.0, 2.0),
437                3.0,
438                &PointStyle {
439                    shape: *shape,
440                    ..Default::default()
441                },
442            )
443            .unwrap();
444        }
445        assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
446        for (center, radius) in b.circles.borrow().iter() {
447            assert_eq!(*center, (1.0, 2.0));
448            assert_eq!(*radius, 3.0);
449        }
450    }
451
452    #[test]
453    fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
454        let mut b = RecordingBackend::default();
455        let pa = b.plot_area();
456        let ta = b.total_area();
457        assert_eq!(
458            (pa.x, pa.y, pa.width, pa.height),
459            (ta.x, ta.y, ta.width, ta.height)
460        );
461        assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
462        b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
463            .unwrap();
464        b.draw_text("label", (0.0, 0.0), &TextStyle::default())
465            .unwrap();
466        b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
467            .unwrap();
468    }
469}