Skip to main content

ggplot_rs/render/
plotters_backend.rs

1use std::collections::HashSet;
2use std::sync::{Mutex, OnceLock};
3
4use plotters::prelude::*;
5use plotters::style::FontStyle;
6
7use super::backend::{
8    DrawBackend, LineStyle, PointShape, PointStyle, RectStyle, TextAnchor, TextStyle,
9};
10use super::{Rect, RenderError};
11
12/// A font bundled into the binary so text renders in a headless container with
13/// no system fonts / fontconfig. DejaVu Sans (Bitstream Vera–derived license,
14/// freely redistributable) — broad glyph coverage keeps labels tofu-free.
15const SANS: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans.ttf");
16const SANS_BOLD: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans-Bold.ttf");
17const SANS_OBLIQUE: &[u8] = include_bytes!("../../assets/fonts/DejaVuSans-Oblique.ttf");
18const SERIF: &[u8] = include_bytes!("../../assets/fonts/DejaVuSerif.ttf");
19const SERIF_BOLD: &[u8] = include_bytes!("../../assets/fonts/DejaVuSerif-Bold.ttf");
20const SERIF_ITALIC: &[u8] = include_bytes!("../../assets/fonts/DejaVuSerif-Italic.ttf");
21const MONO: &[u8] = include_bytes!("../../assets/fonts/DejaVuSansMono.ttf");
22const MONO_BOLD: &[u8] = include_bytes!("../../assets/fonts/DejaVuSansMono-Bold.ttf");
23
24/// Pick the DejaVu face set (per style) that matches a requested family name, so
25/// `family = "serif"` / `"monospace"` render with real serif/mono glyphs — not
26/// just the SVG attribute — headlessly.
27fn faces_for(family: &str) -> [(FontStyle, &'static [u8]); 4] {
28    let f = family.to_lowercase();
29    if f.contains("mono") || f.contains("courier") {
30        [
31            (FontStyle::Normal, MONO),
32            (FontStyle::Bold, MONO_BOLD),
33            (FontStyle::Italic, MONO),
34            (FontStyle::Oblique, MONO),
35        ]
36    } else if f == "serif" || f.contains("times") || (f.contains("serif") && !f.contains("sans")) {
37        [
38            (FontStyle::Normal, SERIF),
39            (FontStyle::Bold, SERIF_BOLD),
40            (FontStyle::Italic, SERIF_ITALIC),
41            (FontStyle::Oblique, SERIF_ITALIC),
42        ]
43    } else {
44        [
45            (FontStyle::Normal, SANS),
46            (FontStyle::Bold, SANS_BOLD),
47            (FontStyle::Italic, SANS_OBLIQUE),
48            (FontStyle::Oblique, SANS_OBLIQUE),
49        ]
50    }
51}
52
53fn registered_families() -> &'static Mutex<HashSet<String>> {
54    static REGISTERED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
55    REGISTERED.get_or_init(|| Mutex::new(HashSet::new()))
56}
57
58/// Ensure the bundled font is registered under `family` (idempotent).
59///
60/// With plotters' `ab_glyph` backend there is no system-font lookup: every
61/// family used for layout must be registered first. We map *all* requested
62/// families to the one bundled font, so any family name lays out correctly
63/// headlessly; SVG output still carries the requested family name for browsers.
64fn ensure_font(family: &str) {
65    let mut set = registered_families().lock().unwrap();
66    if set.insert(family.to_string()) {
67        // Register the matching DejaVu family + face per style so serif/mono and
68        // bold/italic actually render (not just carry the SVG attribute).
69        for (style, bytes) in faces_for(family) {
70            // Ignore the result: a malformed font would surface as a draw error.
71            let _ = plotters::style::register_font(family, style, bytes);
72        }
73    }
74}
75
76/// Adapter from plotters' DrawingArea to our DrawBackend trait.
77pub struct PlottersAdapter<'a, DB: DrawingBackend> {
78    area: &'a DrawingArea<DB, plotters::coord::Shift>,
79    plot_area: Rect,
80    total_area: Rect,
81}
82
83impl<'a, DB: DrawingBackend> PlottersAdapter<'a, DB> {
84    pub fn new(area: &'a DrawingArea<DB, plotters::coord::Shift>, plot_area: Rect) -> Self {
85        let (w, h) = area.dim_in_pixel();
86        PlottersAdapter {
87            area,
88            plot_area,
89            total_area: Rect {
90                x: 0.0,
91                y: 0.0,
92                width: w as f64,
93                height: h as f64,
94            },
95        }
96    }
97}
98
99fn to_rgba(color: (u8, u8, u8), alpha: f64) -> RGBAColor {
100    RGBAColor(color.0, color.1, color.2, alpha)
101}
102
103/// Clip a point to the given rectangle bounds. Returns None if fully outside.
104fn clip_point(x: f64, y: f64, rect: &Rect) -> (f64, f64) {
105    (
106        x.clamp(rect.x, rect.x + rect.width),
107        y.clamp(rect.y, rect.y + rect.height),
108    )
109}
110
111/// Check if a point is inside the rectangle (with small margin).
112fn point_in_rect(x: f64, y: f64, rect: &Rect) -> bool {
113    let margin = 2.0;
114    x >= rect.x - margin
115        && x <= rect.x + rect.width + margin
116        && y >= rect.y - margin
117        && y <= rect.y + rect.height + margin
118}
119
120/// Cohen-Sutherland line clipping. Returns clipped line segment or None if fully outside.
121fn clip_line_segment(
122    mut x0: f64,
123    mut y0: f64,
124    mut x1: f64,
125    mut y1: f64,
126    rect: &Rect,
127) -> Option<((f64, f64), (f64, f64))> {
128    let xmin = rect.x;
129    let xmax = rect.x + rect.width;
130    let ymin = rect.y;
131    let ymax = rect.y + rect.height;
132
133    const INSIDE: u8 = 0;
134    const LEFT: u8 = 1;
135    const RIGHT: u8 = 2;
136    const BOTTOM: u8 = 4;
137    const TOP: u8 = 8;
138
139    let outcode = |x: f64, y: f64| -> u8 {
140        let mut code = INSIDE;
141        if x < xmin {
142            code |= LEFT;
143        } else if x > xmax {
144            code |= RIGHT;
145        }
146        if y < ymin {
147            code |= TOP;
148        } else if y > ymax {
149            code |= BOTTOM;
150        }
151        code
152    };
153
154    let mut code0 = outcode(x0, y0);
155    let mut code1 = outcode(x1, y1);
156
157    for _ in 0..20 {
158        if (code0 | code1) == 0 {
159            return Some(((x0, y0), (x1, y1)));
160        }
161        if (code0 & code1) != 0 {
162            return None;
163        }
164
165        let code_out = if code0 != 0 { code0 } else { code1 };
166        let (x, y);
167
168        if code_out & TOP != 0 {
169            x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
170            y = ymin;
171        } else if code_out & BOTTOM != 0 {
172            x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
173            y = ymax;
174        } else if code_out & RIGHT != 0 {
175            y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
176            x = xmax;
177        } else {
178            y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
179            x = xmin;
180        }
181
182        if code_out == code0 {
183            x0 = x;
184            y0 = y;
185            code0 = outcode(x0, y0);
186        } else {
187            x1 = x;
188            y1 = y;
189            code1 = outcode(x1, y1);
190        }
191    }
192
193    None
194}
195
196fn map_err<E: std::fmt::Debug>(e: E) -> RenderError {
197    RenderError::BackendError(format!("{:?}", e))
198}
199
200/// Segment a polyline according to a dash pattern, returning visible sub-paths.
201fn segment_dashed(points: &[(f64, f64)], pattern: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
202    if pattern.is_empty() || points.len() < 2 {
203        return vec![points.to_vec()];
204    }
205
206    let mut segments: Vec<Vec<(f64, f64)>> = Vec::new();
207    let mut current_seg: Vec<(f64, f64)> = Vec::new();
208    let mut drawing = true;
209    let mut pat_idx = 0;
210    let mut remaining_in_pat = pattern[0].0; // start with draw phase
211
212    for window in points.windows(2) {
213        let (x0, y0) = window[0];
214        let (x1, y1) = window[1];
215        let dx = x1 - x0;
216        let dy = y1 - y0;
217        let seg_len = (dx * dx + dy * dy).sqrt();
218        if seg_len < 0.001 {
219            continue;
220        }
221        let ux = dx / seg_len;
222        let uy = dy / seg_len;
223        let mut consumed = 0.0;
224
225        while consumed < seg_len - 0.001 {
226            let available = seg_len - consumed;
227            let step = remaining_in_pat.min(available);
228            let px = x0 + ux * (consumed + step);
229            let py = y0 + uy * (consumed + step);
230
231            if drawing {
232                if current_seg.is_empty() {
233                    current_seg.push((x0 + ux * consumed, y0 + uy * consumed));
234                }
235                current_seg.push((px, py));
236            }
237
238            consumed += step;
239            remaining_in_pat -= step;
240
241            if remaining_in_pat < 0.001 {
242                if drawing {
243                    if current_seg.len() >= 2 {
244                        segments.push(std::mem::take(&mut current_seg));
245                    } else {
246                        current_seg.clear();
247                    }
248                    drawing = false;
249                    remaining_in_pat = pattern[pat_idx].1; // gap
250                } else {
251                    drawing = true;
252                    pat_idx = (pat_idx + 1) % pattern.len();
253                    remaining_in_pat = pattern[pat_idx].0; // draw
254                }
255            }
256        }
257    }
258
259    if drawing && current_seg.len() >= 2 {
260        segments.push(current_seg);
261    }
262
263    segments
264}
265
266impl<'a, DB: DrawingBackend> DrawBackend for PlottersAdapter<'a, DB> {
267    fn draw_circle(
268        &mut self,
269        center: (f64, f64),
270        radius: f64,
271        style: &PointStyle,
272    ) -> Result<(), RenderError> {
273        // Clip: skip points entirely outside the plot area
274        if !point_in_rect(center.0, center.1, &self.plot_area) {
275            return Ok(());
276        }
277        let color = to_rgba(style.color, style.alpha);
278        if style.filled {
279            self.area
280                .draw(&Circle::new(
281                    (center.0 as i32, center.1 as i32),
282                    radius as i32,
283                    color.filled(),
284                ))
285                .map_err(map_err)?;
286        } else {
287            self.area
288                .draw(&Circle::new(
289                    (center.0 as i32, center.1 as i32),
290                    radius as i32,
291                    color.stroke_width(1),
292                ))
293                .map_err(map_err)?;
294        }
295        Ok(())
296    }
297
298    fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
299        if points.len() < 2 {
300            return Ok(());
301        }
302        // Simulate sub-pixel line widths: render as 1px with reduced opacity
303        let (pixel_width, alpha) = if style.width >= 1.0 {
304            (style.width as u32, style.alpha)
305        } else if style.width > 0.0 {
306            (1, style.alpha * style.width)
307        } else {
308            (0, style.alpha)
309        };
310        let color = to_rgba(style.color, alpha);
311        let stroke = color.stroke_width(pixel_width);
312
313        let pattern = style.linetype.pattern();
314        let sub_paths = segment_dashed(points, pattern);
315
316        for path in &sub_paths {
317            for window in path.windows(2) {
318                // Clip each line segment to plot area
319                if let Some((p1, p2)) = clip_line_segment(
320                    window[0].0,
321                    window[0].1,
322                    window[1].0,
323                    window[1].1,
324                    &self.plot_area,
325                ) {
326                    self.area
327                        .draw(&PathElement::new(
328                            vec![(p1.0 as i32, p1.1 as i32), (p2.0 as i32, p2.1 as i32)],
329                            stroke,
330                        ))
331                        .map_err(map_err)?;
332                }
333            }
334        }
335        Ok(())
336    }
337
338    fn draw_rect(
339        &mut self,
340        top_left: (f64, f64),
341        bottom_right: (f64, f64),
342        style: &RectStyle,
343    ) -> Result<(), RenderError> {
344        let (tl, br) = if style.clip {
345            // Clamp rect to plot area (data elements only)
346            let clamped_tl = clip_point(top_left.0, top_left.1, &self.plot_area);
347            let clamped_br = clip_point(bottom_right.0, bottom_right.1, &self.plot_area);
348
349            // Skip if fully collapsed after clamping
350            if (clamped_tl.0 - clamped_br.0).abs() < 0.5
351                && (clamped_tl.1 - clamped_br.1).abs() < 0.5
352            {
353                // But don't skip if original rect was already small (it's a real data rect)
354                if (top_left.0 - bottom_right.0).abs() > 1.0
355                    || (top_left.1 - bottom_right.1).abs() > 1.0
356                {
357                    return Ok(());
358                }
359            }
360
361            (
362                (clamped_tl.0 as i32, clamped_tl.1 as i32),
363                (clamped_br.0 as i32, clamped_br.1 as i32),
364            )
365        } else {
366            // No clipping — backgrounds, strips, legends
367            (
368                (top_left.0 as i32, top_left.1 as i32),
369                (bottom_right.0 as i32, bottom_right.1 as i32),
370            )
371        };
372
373        if style.corner_radius > 0.0 {
374            // plotters has no native rounded-rect primitive: approximate with
375            // the same polygon the pixel backend uses. Note this clips the
376            // bounding box above, not each rounded corner individually — a
377            // rounded rect that straddles the clip edge gets a straight cut.
378            let pts = super::backend::rounded_rect_points(
379                tl.0 as f64,
380                tl.1 as f64,
381                br.0 as f64,
382                br.1 as f64,
383                style.corner_radius,
384            );
385            let int_pts: Vec<(i32, i32)> =
386                pts.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
387            if let Some(fill) = style.fill {
388                let fill_color = to_rgba(fill, style.alpha);
389                self.area
390                    .draw(&Polygon::new(int_pts.clone(), fill_color.filled()))
391                    .map_err(map_err)?;
392            }
393            if let Some(stroke) = style.stroke {
394                let stroke_color = to_rgba(stroke, style.alpha);
395                let width = if style.stroke_width > 0.0 {
396                    (style.stroke_width as u32).max(1)
397                } else {
398                    0
399                };
400                let mut loop_pts = int_pts;
401                if let Some(first) = loop_pts.first().copied() {
402                    loop_pts.push(first);
403                }
404                self.area
405                    .draw(&PathElement::new(
406                        loop_pts,
407                        stroke_color.stroke_width(width),
408                    ))
409                    .map_err(map_err)?;
410            }
411            return Ok(());
412        }
413
414        if let Some(fill) = style.fill {
415            let fill_color = to_rgba(fill, style.alpha);
416            self.area
417                .draw(&plotters::prelude::Rectangle::new(
418                    [tl, br],
419                    fill_color.filled(),
420                ))
421                .map_err(map_err)?;
422        }
423
424        if let Some(stroke) = style.stroke {
425            let stroke_color = to_rgba(stroke, style.alpha);
426            self.area
427                .draw(&plotters::prelude::Rectangle::new(
428                    [tl, br],
429                    stroke_color.stroke_width(if style.stroke_width > 0.0 {
430                        (style.stroke_width as u32).max(1)
431                    } else {
432                        0
433                    }),
434                ))
435                .map_err(map_err)?;
436        }
437
438        Ok(())
439    }
440
441    fn draw_text(
442        &mut self,
443        text: &str,
444        pos: (f64, f64),
445        style: &TextStyle,
446    ) -> Result<(), RenderError> {
447        let color = to_rgba(style.color, 1.0);
448        let family = style.family.as_deref().unwrap_or("sans-serif");
449        ensure_font(family);
450        let font_style = match style.face {
451            crate::render::backend::FontFace::Plain => FontStyle::Normal,
452            crate::render::backend::FontFace::Bold => FontStyle::Bold,
453            crate::render::backend::FontFace::Italic => FontStyle::Italic,
454        };
455        let font = (family, style.size).into_font().style(font_style);
456
457        let pos_adj = match style.anchor {
458            TextAnchor::Start => plotters::style::text_anchor::Pos::new(
459                plotters::style::text_anchor::HPos::Left,
460                plotters::style::text_anchor::VPos::Center,
461            ),
462            TextAnchor::Middle => plotters::style::text_anchor::Pos::new(
463                plotters::style::text_anchor::HPos::Center,
464                plotters::style::text_anchor::VPos::Center,
465            ),
466            TextAnchor::End => plotters::style::text_anchor::Pos::new(
467                plotters::style::text_anchor::HPos::Right,
468                plotters::style::text_anchor::VPos::Center,
469            ),
470        };
471
472        let angle_i = style.angle.round() as i32;
473        if angle_i != 0 {
474            let (font_transform, rotated_pos) = match angle_i.rem_euclid(360) {
475                80..=100 => (
476                    FontTransform::Rotate90,
477                    plotters::style::text_anchor::Pos::new(
478                        plotters::style::text_anchor::HPos::Center,
479                        plotters::style::text_anchor::VPos::Center,
480                    ),
481                ),
482                170..=190 => (
483                    FontTransform::Rotate180,
484                    plotters::style::text_anchor::Pos::new(
485                        plotters::style::text_anchor::HPos::Center,
486                        plotters::style::text_anchor::VPos::Center,
487                    ),
488                ),
489                // 270° or -90° or 45° approximation (common ggplot2 x-axis rotation)
490                _ => (
491                    FontTransform::Rotate270,
492                    plotters::style::text_anchor::Pos::new(
493                        plotters::style::text_anchor::HPos::Right,
494                        plotters::style::text_anchor::VPos::Center,
495                    ),
496                ),
497            };
498
499            let text_style = plotters::prelude::TextStyle::from((family, style.size).into_font())
500                .color(&color)
501                .transform(font_transform)
502                .pos(rotated_pos);
503
504            self.area
505                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
506                .map_err(map_err)?;
507        } else {
508            let text_style = plotters::prelude::TextStyle::from(font)
509                .color(&color)
510                .pos(pos_adj);
511            self.area
512                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
513                .map_err(map_err)?;
514        }
515
516        Ok(())
517    }
518
519    fn draw_polygon(
520        &mut self,
521        points: &[(f64, f64)],
522        style: &RectStyle,
523    ) -> Result<(), RenderError> {
524        if points.len() < 3 {
525            return Ok(());
526        }
527        let int_points: Vec<(i32, i32)> =
528            points.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
529
530        if let Some(fill) = style.fill {
531            let fill_color = to_rgba(fill, style.alpha);
532            self.area
533                .draw(&Polygon::new(int_points.clone(), fill_color.filled()))
534                .map_err(map_err)?;
535        }
536
537        Ok(())
538    }
539
540    /// Even-odd fill across every ring at once (`geom_sf`'s polygon holes,
541    /// #178) — plotters' own `Polygon` element has no hole support, so the
542    /// fill is a per-pixel scanline (as the `canvas` backend's own
543    /// `fill_polygon_rings` does), then each ring is stroked the same way
544    /// (plotters' [`PathElement`], one closed path per ring).
545    fn draw_polygon_rings(
546        &mut self,
547        rings: &[Vec<(f64, f64)>],
548        style: &RectStyle,
549    ) -> Result<(), RenderError> {
550        if let Some(fill) = style.fill {
551            let color = to_rgba(fill, style.alpha);
552            let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
553            for ring in rings {
554                if ring.len() < 3 {
555                    continue;
556                }
557                for p in ring {
558                    y0 = y0.min(p.1);
559                    y1 = y1.max(p.1);
560                }
561            }
562            if y0.is_finite() && y1.is_finite() {
563                let (y0, y1) = (y0.floor() as i32, y1.ceil() as i32);
564                for y in y0..=y1 {
565                    let yc = y as f64 + 0.5;
566                    let mut xs: Vec<f64> = Vec::new();
567                    for ring in rings {
568                        let n = ring.len();
569                        if n < 3 {
570                            continue;
571                        }
572                        for i in 0..n {
573                            let (ax, ay) = ring[i];
574                            let (bx, by) = ring[(i + 1) % n];
575                            if (ay <= yc && by > yc) || (by <= yc && ay > yc) {
576                                xs.push(ax + (yc - ay) / (by - ay) * (bx - ax));
577                            }
578                        }
579                    }
580                    xs.sort_by(|p, q| p.total_cmp(q));
581                    for span in xs.chunks(2) {
582                        if let [xa, xb] = span {
583                            for x in xa.round() as i32..=xb.round() as i32 {
584                                self.area.draw_pixel((x, y), &color).map_err(map_err)?;
585                            }
586                        }
587                    }
588                }
589            }
590        }
591        if let Some(stroke) = style.stroke {
592            let stroke_color = to_rgba(stroke, style.alpha);
593            let width = if style.stroke_width > 0.0 {
594                (style.stroke_width as u32).max(1)
595            } else {
596                0
597            };
598            if width > 0 {
599                for ring in rings {
600                    if ring.len() < 3 {
601                        continue;
602                    }
603                    let mut pts: Vec<(i32, i32)> =
604                        ring.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
605                    pts.push(pts[0]);
606                    self.area
607                        .draw(&PathElement::new(pts, stroke_color.stroke_width(width)))
608                        .map_err(map_err)?;
609                }
610            }
611        }
612        Ok(())
613    }
614
615    fn draw_shape(
616        &mut self,
617        center: (f64, f64),
618        radius: f64,
619        style: &PointStyle,
620    ) -> Result<(), RenderError> {
621        // Clip: skip shapes entirely outside the plot area
622        if !point_in_rect(center.0, center.1, &self.plot_area) {
623            return Ok(());
624        }
625        let color = to_rgba(style.color, style.alpha);
626        let (cx, cy) = (center.0 as i32, center.1 as i32);
627        let r = radius as i32;
628
629        match style.shape {
630            PointShape::Circle => self.draw_circle(center, radius, style),
631            PointShape::Square => {
632                let tl = (cx - r, cy - r);
633                let br = (cx + r, cy + r);
634                if style.filled {
635                    self.area
636                        .draw(&plotters::prelude::Rectangle::new([tl, br], color.filled()))
637                        .map_err(map_err)?;
638                } else {
639                    self.area
640                        .draw(&plotters::prelude::Rectangle::new(
641                            [tl, br],
642                            color.stroke_width(1),
643                        ))
644                        .map_err(map_err)?;
645                }
646                Ok(())
647            }
648            PointShape::Triangle => {
649                let pts = vec![(cx, cy - r), (cx - r, cy + r), (cx + r, cy + r)];
650                if style.filled {
651                    self.area
652                        .draw(&Polygon::new(pts, color.filled()))
653                        .map_err(map_err)?;
654                } else {
655                    let outline = vec![
656                        (cx, cy - r),
657                        (cx - r, cy + r),
658                        (cx + r, cy + r),
659                        (cx, cy - r),
660                    ];
661                    self.area
662                        .draw(&PathElement::new(outline, color.stroke_width(1)))
663                        .map_err(map_err)?;
664                }
665                Ok(())
666            }
667            PointShape::Diamond => {
668                let pts = vec![(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)];
669                if style.filled {
670                    self.area
671                        .draw(&Polygon::new(pts, color.filled()))
672                        .map_err(map_err)?;
673                } else {
674                    let outline = vec![
675                        (cx, cy - r),
676                        (cx + r, cy),
677                        (cx, cy + r),
678                        (cx - r, cy),
679                        (cx, cy - r),
680                    ];
681                    self.area
682                        .draw(&PathElement::new(outline, color.stroke_width(1)))
683                        .map_err(map_err)?;
684                }
685                Ok(())
686            }
687            PointShape::Cross => {
688                // X shape
689                self.area
690                    .draw(&PathElement::new(
691                        vec![(cx - r, cy - r), (cx + r, cy + r)],
692                        color.stroke_width(1),
693                    ))
694                    .map_err(map_err)?;
695                self.area
696                    .draw(&PathElement::new(
697                        vec![(cx - r, cy + r), (cx + r, cy - r)],
698                        color.stroke_width(1),
699                    ))
700                    .map_err(map_err)?;
701                Ok(())
702            }
703            PointShape::Plus => {
704                // + shape
705                self.area
706                    .draw(&PathElement::new(
707                        vec![(cx - r, cy), (cx + r, cy)],
708                        color.stroke_width(1),
709                    ))
710                    .map_err(map_err)?;
711                self.area
712                    .draw(&PathElement::new(
713                        vec![(cx, cy - r), (cx, cy + r)],
714                        color.stroke_width(1),
715                    ))
716                    .map_err(map_err)?;
717                Ok(())
718            }
719        }
720    }
721
722    fn plot_area(&self) -> Rect {
723        self.plot_area.clone()
724    }
725
726    fn total_area(&self) -> Rect {
727        self.total_area.clone()
728    }
729}