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    /// Report a non-fatal problem found while drawing (e.g. labels a repel
226    /// layout had to drop). Backends that collect warnings (the native SVG
227    /// backend) append it to the render warnings. Default: ignored.
228    fn warn(&mut self, _message: String) {}
229
230    /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
231    /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
232    /// path/line). While set, the SVG backend writes `data-part="<part>"
233    /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
234    /// decorative; the parent mark already carries the semantic info).
235    /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
236    /// still written. Default: no-op.
237    fn set_mark_part(&mut self, _part: Option<&str>) {}
238
239    /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
240    /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
241    /// attr with the same name as a built-in (`data-x`, `data-series`,
242    /// `data-value`) replaces it rather than duplicating it. An empty
243    /// `Vec` clears any previously set attrs. Default: no-op.
244    fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
245
246    /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
247    fn draw_shape(
248        &mut self,
249        center: (f64, f64),
250        radius: f64,
251        style: &PointStyle,
252    ) -> Result<(), RenderError> {
253        match style.shape {
254            PointShape::Circle => self.draw_circle(center, radius, style),
255            _ => {
256                // Default: fall back to circle for unsupported backends
257                self.draw_circle(center, radius, style)
258            }
259        }
260    }
261
262    /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
263    /// `ctrl`, in pixel space. The default implementation flattens the curve
264    /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
265    /// backends that support real curves (SVG) should override this.
266    /// Non-finite input draws nothing.
267    fn draw_quadratic(
268        &mut self,
269        p0: (f64, f64),
270        ctrl: (f64, f64),
271        p1: (f64, f64),
272        style: &LineStyle,
273    ) -> Result<(), RenderError> {
274        if !(p0.0.is_finite()
275            && p0.1.is_finite()
276            && ctrl.0.is_finite()
277            && ctrl.1.is_finite()
278            && p1.0.is_finite()
279            && p1.1.is_finite())
280        {
281            return Ok(());
282        }
283        const STEPS: usize = 32;
284        let mut pts = Vec::with_capacity(STEPS + 1);
285        for i in 0..=STEPS {
286            let t = i as f64 / STEPS as f64;
287            let one_minus_t = 1.0 - t;
288            let x =
289                one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
290            let y =
291                one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
292            pts.push((x, y));
293        }
294        self.draw_line(&pts, style)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::render::{Rect, RenderError};
302    use std::cell::RefCell;
303
304    /// A minimal backend that does not override `draw_quadratic`, to exercise
305    /// the trait's default flattening implementation.
306    #[derive(Default)]
307    struct RecordingBackend {
308        lines: RefCell<Vec<Vec<(f64, f64)>>>,
309        circles: RefCell<Vec<((f64, f64), f64)>>,
310    }
311
312    impl DrawBackend for RecordingBackend {
313        fn draw_circle(
314            &mut self,
315            center: (f64, f64),
316            radius: f64,
317            _style: &PointStyle,
318        ) -> Result<(), RenderError> {
319            self.circles.borrow_mut().push((center, radius));
320            Ok(())
321        }
322        fn draw_line(
323            &mut self,
324            points: &[(f64, f64)],
325            _style: &LineStyle,
326        ) -> Result<(), RenderError> {
327            self.lines.borrow_mut().push(points.to_vec());
328            Ok(())
329        }
330        fn draw_rect(
331            &mut self,
332            _top_left: (f64, f64),
333            _bottom_right: (f64, f64),
334            _style: &RectStyle,
335        ) -> Result<(), RenderError> {
336            Ok(())
337        }
338        fn draw_text(
339            &mut self,
340            _text: &str,
341            _pos: (f64, f64),
342            _style: &TextStyle,
343        ) -> Result<(), RenderError> {
344            Ok(())
345        }
346        fn draw_polygon(
347            &mut self,
348            _points: &[(f64, f64)],
349            _style: &RectStyle,
350        ) -> Result<(), RenderError> {
351            Ok(())
352        }
353        fn plot_area(&self) -> Rect {
354            Rect {
355                x: 0.0,
356                y: 0.0,
357                width: 100.0,
358                height: 100.0,
359            }
360        }
361        fn total_area(&self) -> Rect {
362            self.plot_area()
363        }
364    }
365
366    #[test]
367    fn default_draw_quadratic_flattens_to_33_points() {
368        let mut b = RecordingBackend::default();
369        b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
370            .unwrap();
371        let lines = b.lines.borrow();
372        assert_eq!(lines.len(), 1);
373        assert_eq!(lines[0].len(), 33);
374        assert_eq!(lines[0][0], (0.0, 0.0));
375        assert_eq!(lines[0][32], (10.0, 0.0));
376        // Midpoint of a quadratic Bézier at t=0.5 is the average of the
377        // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
378        let mid = lines[0][16];
379        assert!((mid.0 - 5.0).abs() < 1e-9);
380        assert!((mid.1 - 5.0).abs() < 1e-9);
381    }
382
383    #[test]
384    fn default_draw_quadratic_drops_non_finite() {
385        let mut b = RecordingBackend::default();
386        b.draw_quadratic(
387            (f64::NAN, 0.0),
388            (5.0, 10.0),
389            (10.0, 0.0),
390            &LineStyle::default(),
391        )
392        .unwrap();
393        assert!(b.lines.borrow().is_empty());
394    }
395
396    /// The default `draw_shape` falls back to `draw_circle` for every
397    /// `PointShape` (not just `Circle`) on a backend that doesn't override
398    /// it — e.g. a minimal backend that only implements the required
399    /// methods.
400    #[test]
401    fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
402        let mut b = RecordingBackend::default();
403        for shape in PointShape::ALL {
404            b.draw_shape(
405                (1.0, 2.0),
406                3.0,
407                &PointStyle {
408                    shape: *shape,
409                    ..Default::default()
410                },
411            )
412            .unwrap();
413        }
414        assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
415        for (center, radius) in b.circles.borrow().iter() {
416            assert_eq!(*center, (1.0, 2.0));
417            assert_eq!(*radius, 3.0);
418        }
419    }
420
421    #[test]
422    fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
423        let mut b = RecordingBackend::default();
424        let pa = b.plot_area();
425        let ta = b.total_area();
426        assert_eq!(
427            (pa.x, pa.y, pa.width, pa.height),
428            (ta.x, ta.y, ta.width, ta.height)
429        );
430        assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
431        b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
432            .unwrap();
433        b.draw_text("label", (0.0, 0.0), &TextStyle::default())
434            .unwrap();
435        b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
436            .unwrap();
437    }
438}