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    /// #273: panel-corner boxes claimed so far this render pass — see
82    /// [`DrawBackend::corner_claims`]/[`DrawBackend::claim_corner_box`].
83    corner_claims: Vec<Rect>,
84    /// Clip lines / points / clip-flagged rects to the panel (data marks);
85    /// off while legends draw (#410), whose keys sit outside it.
86    clip: bool,
87}
88
89impl<'a, DB: DrawingBackend> PlottersAdapter<'a, DB> {
90    pub fn new(area: &'a DrawingArea<DB, plotters::coord::Shift>, plot_area: Rect) -> Self {
91        let (w, h) = area.dim_in_pixel();
92        PlottersAdapter {
93            area,
94            plot_area,
95            total_area: Rect {
96                x: 0.0,
97                y: 0.0,
98                width: w as f64,
99                height: h as f64,
100            },
101            corner_claims: Vec::new(),
102            clip: true,
103        }
104    }
105}
106
107fn to_rgba(color: (u8, u8, u8), alpha: f64) -> RGBAColor {
108    RGBAColor(color.0, color.1, color.2, alpha)
109}
110
111/// Clip a point to the given rectangle bounds. Returns None if fully outside.
112fn clip_point(x: f64, y: f64, rect: &Rect) -> (f64, f64) {
113    (
114        x.clamp(rect.x, rect.x + rect.width),
115        y.clamp(rect.y, rect.y + rect.height),
116    )
117}
118
119/// Check if a point is inside the rectangle (with small margin).
120fn point_in_rect(x: f64, y: f64, rect: &Rect) -> bool {
121    let margin = 2.0;
122    x >= rect.x - margin
123        && x <= rect.x + rect.width + margin
124        && y >= rect.y - margin
125        && y <= rect.y + rect.height + margin
126}
127
128/// Cohen-Sutherland line clipping. Returns clipped line segment or None if fully outside.
129fn clip_line_segment(
130    mut x0: f64,
131    mut y0: f64,
132    mut x1: f64,
133    mut y1: f64,
134    rect: &Rect,
135) -> Option<((f64, f64), (f64, f64))> {
136    let xmin = rect.x;
137    let xmax = rect.x + rect.width;
138    let ymin = rect.y;
139    let ymax = rect.y + rect.height;
140
141    const INSIDE: u8 = 0;
142    const LEFT: u8 = 1;
143    const RIGHT: u8 = 2;
144    const BOTTOM: u8 = 4;
145    const TOP: u8 = 8;
146
147    let outcode = |x: f64, y: f64| -> u8 {
148        let mut code = INSIDE;
149        if x < xmin {
150            code |= LEFT;
151        } else if x > xmax {
152            code |= RIGHT;
153        }
154        if y < ymin {
155            code |= TOP;
156        } else if y > ymax {
157            code |= BOTTOM;
158        }
159        code
160    };
161
162    let mut code0 = outcode(x0, y0);
163    let mut code1 = outcode(x1, y1);
164
165    for _ in 0..20 {
166        if (code0 | code1) == 0 {
167            return Some(((x0, y0), (x1, y1)));
168        }
169        if (code0 & code1) != 0 {
170            return None;
171        }
172
173        let code_out = if code0 != 0 { code0 } else { code1 };
174        let (x, y);
175
176        if code_out & TOP != 0 {
177            x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
178            y = ymin;
179        } else if code_out & BOTTOM != 0 {
180            x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
181            y = ymax;
182        } else if code_out & RIGHT != 0 {
183            y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
184            x = xmax;
185        } else {
186            y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
187            x = xmin;
188        }
189
190        if code_out == code0 {
191            x0 = x;
192            y0 = y;
193            code0 = outcode(x0, y0);
194        } else {
195            x1 = x;
196            y1 = y;
197            code1 = outcode(x1, y1);
198        }
199    }
200
201    None
202}
203
204fn map_err<E: std::fmt::Debug>(e: E) -> RenderError {
205    RenderError::BackendError(format!("{:?}", e))
206}
207
208/// Segment a polyline according to a dash pattern, returning visible sub-paths.
209fn segment_dashed(points: &[(f64, f64)], pattern: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
210    if pattern.is_empty() || points.len() < 2 {
211        return vec![points.to_vec()];
212    }
213
214    let mut segments: Vec<Vec<(f64, f64)>> = Vec::new();
215    let mut current_seg: Vec<(f64, f64)> = Vec::new();
216    let mut drawing = true;
217    let mut pat_idx = 0;
218    let mut remaining_in_pat = pattern[0].0; // start with draw phase
219
220    for window in points.windows(2) {
221        let (x0, y0) = window[0];
222        let (x1, y1) = window[1];
223        let dx = x1 - x0;
224        let dy = y1 - y0;
225        let seg_len = (dx * dx + dy * dy).sqrt();
226        if seg_len < 0.001 {
227            continue;
228        }
229        let ux = dx / seg_len;
230        let uy = dy / seg_len;
231        let mut consumed = 0.0;
232
233        while consumed < seg_len - 0.001 {
234            let available = seg_len - consumed;
235            let step = remaining_in_pat.min(available);
236            let px = x0 + ux * (consumed + step);
237            let py = y0 + uy * (consumed + step);
238
239            if drawing {
240                if current_seg.is_empty() {
241                    current_seg.push((x0 + ux * consumed, y0 + uy * consumed));
242                }
243                current_seg.push((px, py));
244            }
245
246            consumed += step;
247            remaining_in_pat -= step;
248
249            if remaining_in_pat < 0.001 {
250                if drawing {
251                    if current_seg.len() >= 2 {
252                        segments.push(std::mem::take(&mut current_seg));
253                    } else {
254                        current_seg.clear();
255                    }
256                    drawing = false;
257                    remaining_in_pat = pattern[pat_idx].1; // gap
258                } else {
259                    drawing = true;
260                    pat_idx = (pat_idx + 1) % pattern.len();
261                    remaining_in_pat = pattern[pat_idx].0; // draw
262                }
263            }
264        }
265    }
266
267    if drawing && current_seg.len() >= 2 {
268        segments.push(current_seg);
269    }
270
271    segments
272}
273
274impl<DB: DrawingBackend> PlottersAdapter<'_, DB> {
275    /// The rect marks are clipped to: the panel, or the canvas when off.
276    fn clip_area(&self) -> Rect {
277        if self.clip {
278            self.plot_area.clone()
279        } else {
280            self.total_area.clone()
281        }
282    }
283}
284
285impl<'a, DB: DrawingBackend> DrawBackend for PlottersAdapter<'a, DB> {
286    fn set_clipping(&mut self, on: bool) {
287        self.clip = on;
288    }
289
290    fn draw_circle(
291        &mut self,
292        center: (f64, f64),
293        radius: f64,
294        style: &PointStyle,
295    ) -> Result<(), RenderError> {
296        // Clip: skip points entirely outside the plot area
297        if !point_in_rect(center.0, center.1, &self.clip_area()) {
298            return Ok(());
299        }
300        let color = to_rgba(style.color, style.alpha);
301        if style.filled {
302            self.area
303                .draw(&Circle::new(
304                    (center.0 as i32, center.1 as i32),
305                    radius as i32,
306                    color.filled(),
307                ))
308                .map_err(map_err)?;
309        } else {
310            self.area
311                .draw(&Circle::new(
312                    (center.0 as i32, center.1 as i32),
313                    radius as i32,
314                    color.stroke_width(1),
315                ))
316                .map_err(map_err)?;
317        }
318        Ok(())
319    }
320
321    fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
322        if points.len() < 2 {
323            return Ok(());
324        }
325        // Simulate sub-pixel line widths: render as 1px with reduced opacity
326        let (pixel_width, alpha) = if style.width >= 1.0 {
327            (style.width as u32, style.alpha)
328        } else if style.width > 0.0 {
329            (1, style.alpha * style.width)
330        } else {
331            (0, style.alpha)
332        };
333        let color = to_rgba(style.color, alpha);
334        let stroke = color.stroke_width(pixel_width);
335
336        let pattern = style.linetype.dash_pattern();
337        let sub_paths = segment_dashed(points, &pattern);
338
339        for path in &sub_paths {
340            for window in path.windows(2) {
341                // Clip each line segment to plot area
342                if let Some((p1, p2)) = clip_line_segment(
343                    window[0].0,
344                    window[0].1,
345                    window[1].0,
346                    window[1].1,
347                    &self.clip_area(),
348                ) {
349                    self.area
350                        .draw(&PathElement::new(
351                            vec![(p1.0 as i32, p1.1 as i32), (p2.0 as i32, p2.1 as i32)],
352                            stroke,
353                        ))
354                        .map_err(map_err)?;
355                }
356            }
357        }
358        Ok(())
359    }
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        let (tl, br) = if style.clip {
368            // Clamp rect to plot area (data elements only)
369            let area = self.clip_area();
370            let clamped_tl = clip_point(top_left.0, top_left.1, &area);
371            let clamped_br = clip_point(bottom_right.0, bottom_right.1, &area);
372
373            // Skip if fully collapsed after clamping
374            if (clamped_tl.0 - clamped_br.0).abs() < 0.5
375                && (clamped_tl.1 - clamped_br.1).abs() < 0.5
376            {
377                // But don't skip if original rect was already small (it's a real data rect)
378                if (top_left.0 - bottom_right.0).abs() > 1.0
379                    || (top_left.1 - bottom_right.1).abs() > 1.0
380                {
381                    return Ok(());
382                }
383            }
384
385            (
386                (clamped_tl.0 as i32, clamped_tl.1 as i32),
387                (clamped_br.0 as i32, clamped_br.1 as i32),
388            )
389        } else {
390            // No clipping — backgrounds, strips, legends
391            (
392                (top_left.0 as i32, top_left.1 as i32),
393                (bottom_right.0 as i32, bottom_right.1 as i32),
394            )
395        };
396
397        if style.corner_radius > 0.0 {
398            // plotters has no native rounded-rect primitive: approximate with
399            // the same polygon the pixel backend uses. Note this clips the
400            // bounding box above, not each rounded corner individually — a
401            // rounded rect that straddles the clip edge gets a straight cut.
402            let pts = super::backend::rounded_rect_points(
403                tl.0 as f64,
404                tl.1 as f64,
405                br.0 as f64,
406                br.1 as f64,
407                style.corner_radius,
408            );
409            let int_pts: Vec<(i32, i32)> =
410                pts.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
411            if let Some(fill) = style.fill {
412                let fill_color = to_rgba(fill, style.alpha);
413                self.area
414                    .draw(&Polygon::new(int_pts.clone(), fill_color.filled()))
415                    .map_err(map_err)?;
416            }
417            if let Some(stroke) = style.stroke {
418                let stroke_color = to_rgba(stroke, style.alpha);
419                let width = if style.stroke_width > 0.0 {
420                    (style.stroke_width as u32).max(1)
421                } else {
422                    0
423                };
424                let mut loop_pts = int_pts;
425                if let Some(first) = loop_pts.first().copied() {
426                    loop_pts.push(first);
427                }
428                self.area
429                    .draw(&PathElement::new(
430                        loop_pts,
431                        stroke_color.stroke_width(width),
432                    ))
433                    .map_err(map_err)?;
434            }
435            return Ok(());
436        }
437
438        if let Some(fill) = style.fill {
439            let fill_color = to_rgba(fill, style.alpha);
440            self.area
441                .draw(&plotters::prelude::Rectangle::new(
442                    [tl, br],
443                    fill_color.filled(),
444                ))
445                .map_err(map_err)?;
446        }
447
448        if let Some(stroke) = style.stroke {
449            let stroke_color = to_rgba(stroke, style.alpha);
450            self.area
451                .draw(&plotters::prelude::Rectangle::new(
452                    [tl, br],
453                    stroke_color.stroke_width(if style.stroke_width > 0.0 {
454                        (style.stroke_width as u32).max(1)
455                    } else {
456                        0
457                    }),
458                ))
459                .map_err(map_err)?;
460        }
461
462        Ok(())
463    }
464
465    fn draw_text(
466        &mut self,
467        text: &str,
468        pos: (f64, f64),
469        style: &TextStyle,
470    ) -> Result<(), RenderError> {
471        let color = to_rgba(style.color, 1.0);
472        let family = style.family.as_deref().unwrap_or("sans-serif");
473        ensure_font(family);
474        let font_style = match style.face {
475            crate::render::backend::FontFace::Plain => FontStyle::Normal,
476            crate::render::backend::FontFace::Bold => FontStyle::Bold,
477            crate::render::backend::FontFace::Italic => FontStyle::Italic,
478        };
479        let angle_i = style.angle.round() as i32;
480
481        // #229: a text halo — this backend has no native stroke-text, so
482        // approximate the SVG `stroke`/`paint-order="stroke"` outline by
483        // drawing the same text at 8 offsets around the radius, in
484        // `bg_colour`, before the normal pass. `bg_colour: None` (the
485        // default) skips this entirely, so existing output is unchanged.
486        if let Some(bg) = style.bg_colour {
487            let r = if style.bg_r.is_finite() {
488                style.bg_r
489            } else {
490                0.0
491            };
492            if r > 0.0 {
493                let halo_color = to_rgba(bg, 1.0);
494                for i in 0..8 {
495                    let theta = i as f64 * std::f64::consts::FRAC_PI_4;
496                    let offset = (pos.0 + r * theta.cos(), pos.1 + r * theta.sin());
497                    self.draw_text_pass(
498                        text,
499                        offset,
500                        style.anchor.clone(),
501                        family,
502                        style.size,
503                        font_style,
504                        angle_i,
505                        halo_color,
506                    )?;
507                }
508            }
509        }
510
511        self.draw_text_pass(
512            text,
513            pos,
514            style.anchor.clone(),
515            family,
516            style.size,
517            font_style,
518            angle_i,
519            color,
520        )?;
521        Ok(())
522    }
523
524    fn draw_polygon(
525        &mut self,
526        points: &[(f64, f64)],
527        style: &RectStyle,
528    ) -> Result<(), RenderError> {
529        if points.len() < 3 {
530            return Ok(());
531        }
532        let int_points: Vec<(i32, i32)> =
533            points.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
534
535        if let Some(fill) = style.fill {
536            let fill_color = to_rgba(fill, style.alpha);
537            self.area
538                .draw(&Polygon::new(int_points.clone(), fill_color.filled()))
539                .map_err(map_err)?;
540        }
541        // Outline, as the native SVG backend draws it (it was never stroked
542        // here, so violins / polygons / notched boxes lost their border in
543        // PNG output).
544        if let (Some(stroke), true) = (style.stroke, style.stroke_width > 0.0) {
545            let mut pts = int_points;
546            pts.push(pts[0]);
547            let width = (style.stroke_width as u32).max(1);
548            self.area
549                .draw(&PathElement::new(
550                    pts,
551                    to_rgba(stroke, style.alpha).stroke_width(width),
552                ))
553                .map_err(map_err)?;
554        }
555
556        Ok(())
557    }
558
559    /// Even-odd fill across every ring at once (`geom_sf`'s polygon holes,
560    /// #178) — plotters' own `Polygon` element has no hole support, so the
561    /// fill is a per-pixel scanline (as the `canvas` backend's own
562    /// `fill_polygon_rings` does), then each ring is stroked the same way
563    /// (plotters' [`PathElement`], one closed path per ring).
564    fn draw_polygon_rings(
565        &mut self,
566        rings: &[Vec<(f64, f64)>],
567        style: &RectStyle,
568    ) -> Result<(), RenderError> {
569        if let Some(fill) = style.fill {
570            let color = to_rgba(fill, style.alpha);
571            let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
572            for ring in rings {
573                if ring.len() < 3 {
574                    continue;
575                }
576                for p in ring {
577                    y0 = y0.min(p.1);
578                    y1 = y1.max(p.1);
579                }
580            }
581            if y0.is_finite() && y1.is_finite() {
582                let (y0, y1) = (y0.floor() as i32, y1.ceil() as i32);
583                for y in y0..=y1 {
584                    let yc = y as f64 + 0.5;
585                    let mut xs: Vec<f64> = Vec::new();
586                    for ring in rings {
587                        let n = ring.len();
588                        if n < 3 {
589                            continue;
590                        }
591                        for i in 0..n {
592                            let (ax, ay) = ring[i];
593                            let (bx, by) = ring[(i + 1) % n];
594                            if (ay <= yc && by > yc) || (by <= yc && ay > yc) {
595                                xs.push(ax + (yc - ay) / (by - ay) * (bx - ax));
596                            }
597                        }
598                    }
599                    xs.sort_by(|p, q| p.total_cmp(q));
600                    for span in xs.chunks(2) {
601                        if let [xa, xb] = span {
602                            for x in xa.round() as i32..=xb.round() as i32 {
603                                self.area.draw_pixel((x, y), &color).map_err(map_err)?;
604                            }
605                        }
606                    }
607                }
608            }
609        }
610        if let Some(stroke) = style.stroke {
611            let stroke_color = to_rgba(stroke, style.alpha);
612            let width = if style.stroke_width > 0.0 {
613                (style.stroke_width as u32).max(1)
614            } else {
615                0
616            };
617            if width > 0 {
618                for ring in rings {
619                    if ring.len() < 3 {
620                        continue;
621                    }
622                    let mut pts: Vec<(i32, i32)> =
623                        ring.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
624                    pts.push(pts[0]);
625                    self.area
626                        .draw(&PathElement::new(pts, stroke_color.stroke_width(width)))
627                        .map_err(map_err)?;
628                }
629            }
630        }
631        Ok(())
632    }
633
634    fn draw_shape(
635        &mut self,
636        center: (f64, f64),
637        radius: f64,
638        style: &PointStyle,
639    ) -> Result<(), RenderError> {
640        // Clip: skip shapes entirely outside the plot area
641        if !point_in_rect(center.0, center.1, &self.clip_area()) {
642            return Ok(());
643        }
644        let color = to_rgba(style.color, style.alpha);
645        let (cx, cy) = (center.0 as i32, center.1 as i32);
646        let r = radius as i32;
647
648        match style.shape {
649            PointShape::Circle => self.draw_circle(center, radius, style),
650            PointShape::Square => {
651                let tl = (cx - r, cy - r);
652                let br = (cx + r, cy + r);
653                if style.filled {
654                    self.area
655                        .draw(&plotters::prelude::Rectangle::new([tl, br], color.filled()))
656                        .map_err(map_err)?;
657                } else {
658                    self.area
659                        .draw(&plotters::prelude::Rectangle::new(
660                            [tl, br],
661                            color.stroke_width(1),
662                        ))
663                        .map_err(map_err)?;
664                }
665                Ok(())
666            }
667            PointShape::Triangle => {
668                let pts = vec![(cx, cy - r), (cx - r, cy + r), (cx + r, cy + r)];
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 + r),
677                        (cx + r, cy + r),
678                        (cx, cy - r),
679                    ];
680                    self.area
681                        .draw(&PathElement::new(outline, color.stroke_width(1)))
682                        .map_err(map_err)?;
683                }
684                Ok(())
685            }
686            PointShape::Diamond => {
687                let pts = vec![(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)];
688                if style.filled {
689                    self.area
690                        .draw(&Polygon::new(pts, color.filled()))
691                        .map_err(map_err)?;
692                } else {
693                    let outline = vec![
694                        (cx, cy - r),
695                        (cx + r, cy),
696                        (cx, cy + r),
697                        (cx - r, cy),
698                        (cx, cy - r),
699                    ];
700                    self.area
701                        .draw(&PathElement::new(outline, color.stroke_width(1)))
702                        .map_err(map_err)?;
703                }
704                Ok(())
705            }
706            PointShape::Cross => {
707                // X shape
708                self.area
709                    .draw(&PathElement::new(
710                        vec![(cx - r, cy - r), (cx + r, cy + r)],
711                        color.stroke_width(1),
712                    ))
713                    .map_err(map_err)?;
714                self.area
715                    .draw(&PathElement::new(
716                        vec![(cx - r, cy + r), (cx + r, cy - r)],
717                        color.stroke_width(1),
718                    ))
719                    .map_err(map_err)?;
720                Ok(())
721            }
722            PointShape::Plus => {
723                // + shape
724                self.area
725                    .draw(&PathElement::new(
726                        vec![(cx - r, cy), (cx + r, cy)],
727                        color.stroke_width(1),
728                    ))
729                    .map_err(map_err)?;
730                self.area
731                    .draw(&PathElement::new(
732                        vec![(cx, cy - r), (cx, cy + r)],
733                        color.stroke_width(1),
734                    ))
735                    .map_err(map_err)?;
736                Ok(())
737            }
738        }
739    }
740
741    /// #325: the visible part, scaled, as one bitmap element (an `<image>`
742    /// in plotters' SVG output). Opaque: plotters bitmaps carry no alpha.
743    fn draw_image(
744        &mut self,
745        bytes: &[u8],
746        rect: &Rect,
747        clip: &Rect,
748        _alpha: f64,
749    ) -> Result<(), RenderError> {
750        let (w, h) = self.area.dim_in_pixel();
751        let Some((pos, img)) = crate::render::raster::decode_visible(bytes, rect, clip, w, h)
752        else {
753            return Ok(());
754        };
755        let rgb = image::DynamicImage::ImageRgba8(img).to_rgb8();
756        let element: plotters::element::BitMapElement<(i32, i32)> =
757            (pos, image::DynamicImage::ImageRgb8(rgb)).into();
758        self.area.draw(&element).map_err(map_err)?;
759        Ok(())
760    }
761
762    fn plot_area(&self) -> Rect {
763        self.plot_area.clone()
764    }
765
766    fn total_area(&self) -> Rect {
767        self.total_area.clone()
768    }
769
770    fn corner_claims(&self) -> Vec<Rect> {
771        self.corner_claims.clone()
772    }
773    fn claim_corner_box(&mut self, rect: Rect) {
774        self.corner_claims.push(rect);
775    }
776    fn clear_corner_claims(&mut self) {
777        self.corner_claims.clear();
778    }
779}
780
781impl<'a, DB: DrawingBackend> PlottersAdapter<'a, DB> {
782    /// One pass of `text` in `color` at `pos` — factored out of
783    /// `draw_text` so #229's halo can run it several times (once per
784    /// offset, in `bg_colour`) before the normal pass.
785    ///
786    /// (Kept in its own inherent `impl` block: a trait `impl` can't be
787    /// split across two blocks, so this can't live inside
788    /// `impl DrawBackend for PlottersAdapter` alongside the trait's own
789    /// methods.)
790    #[allow(clippy::too_many_arguments)]
791    fn draw_text_pass(
792        &mut self,
793        text: &str,
794        pos: (f64, f64),
795        anchor: TextAnchor,
796        family: &str,
797        size: f64,
798        font_style: FontStyle,
799        angle_i: i32,
800        color: RGBAColor,
801    ) -> Result<(), RenderError> {
802        let font = (family, size).into_font().style(font_style);
803
804        let pos_adj = match anchor {
805            TextAnchor::Start => plotters::style::text_anchor::Pos::new(
806                plotters::style::text_anchor::HPos::Left,
807                plotters::style::text_anchor::VPos::Center,
808            ),
809            TextAnchor::Middle => plotters::style::text_anchor::Pos::new(
810                plotters::style::text_anchor::HPos::Center,
811                plotters::style::text_anchor::VPos::Center,
812            ),
813            TextAnchor::End => plotters::style::text_anchor::Pos::new(
814                plotters::style::text_anchor::HPos::Right,
815                plotters::style::text_anchor::VPos::Center,
816            ),
817        };
818
819        if angle_i != 0 {
820            let (font_transform, rotated_pos) = match angle_i.rem_euclid(360) {
821                80..=100 => (
822                    FontTransform::Rotate90,
823                    plotters::style::text_anchor::Pos::new(
824                        plotters::style::text_anchor::HPos::Center,
825                        plotters::style::text_anchor::VPos::Center,
826                    ),
827                ),
828                170..=190 => (
829                    FontTransform::Rotate180,
830                    plotters::style::text_anchor::Pos::new(
831                        plotters::style::text_anchor::HPos::Center,
832                        plotters::style::text_anchor::VPos::Center,
833                    ),
834                ),
835                // 270° or -90° or 45° approximation (common ggplot2 x-axis rotation)
836                _ => (
837                    FontTransform::Rotate270,
838                    plotters::style::text_anchor::Pos::new(
839                        plotters::style::text_anchor::HPos::Right,
840                        plotters::style::text_anchor::VPos::Center,
841                    ),
842                ),
843            };
844
845            let text_style = plotters::prelude::TextStyle::from((family, size).into_font())
846                .color(&color)
847                .transform(font_transform)
848                .pos(rotated_pos);
849
850            self.area
851                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
852                .map_err(map_err)?;
853        } else {
854            let text_style = plotters::prelude::TextStyle::from(font)
855                .color(&color)
856                .pos(pos_adj);
857            self.area
858                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
859                .map_err(map_err)?;
860        }
861
862        Ok(())
863    }
864}