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