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.dash_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 angle_i = style.angle.round() as i32;
456
457        // #229: a text halo — this backend has no native stroke-text, so
458        // approximate the SVG `stroke`/`paint-order="stroke"` outline by
459        // drawing the same text at 8 offsets around the radius, in
460        // `bg_colour`, before the normal pass. `bg_colour: None` (the
461        // default) skips this entirely, so existing output is unchanged.
462        if let Some(bg) = style.bg_colour {
463            let r = if style.bg_r.is_finite() {
464                style.bg_r
465            } else {
466                0.0
467            };
468            if r > 0.0 {
469                let halo_color = to_rgba(bg, 1.0);
470                for i in 0..8 {
471                    let theta = i as f64 * std::f64::consts::FRAC_PI_4;
472                    let offset = (pos.0 + r * theta.cos(), pos.1 + r * theta.sin());
473                    self.draw_text_pass(
474                        text,
475                        offset,
476                        style.anchor.clone(),
477                        family,
478                        style.size,
479                        font_style,
480                        angle_i,
481                        halo_color,
482                    )?;
483                }
484            }
485        }
486
487        self.draw_text_pass(
488            text,
489            pos,
490            style.anchor.clone(),
491            family,
492            style.size,
493            font_style,
494            angle_i,
495            color,
496        )?;
497        Ok(())
498    }
499
500    fn draw_polygon(
501        &mut self,
502        points: &[(f64, f64)],
503        style: &RectStyle,
504    ) -> Result<(), RenderError> {
505        if points.len() < 3 {
506            return Ok(());
507        }
508        let int_points: Vec<(i32, i32)> =
509            points.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
510
511        if let Some(fill) = style.fill {
512            let fill_color = to_rgba(fill, style.alpha);
513            self.area
514                .draw(&Polygon::new(int_points.clone(), fill_color.filled()))
515                .map_err(map_err)?;
516        }
517
518        Ok(())
519    }
520
521    /// Even-odd fill across every ring at once (`geom_sf`'s polygon holes,
522    /// #178) — plotters' own `Polygon` element has no hole support, so the
523    /// fill is a per-pixel scanline (as the `canvas` backend's own
524    /// `fill_polygon_rings` does), then each ring is stroked the same way
525    /// (plotters' [`PathElement`], one closed path per ring).
526    fn draw_polygon_rings(
527        &mut self,
528        rings: &[Vec<(f64, f64)>],
529        style: &RectStyle,
530    ) -> Result<(), RenderError> {
531        if let Some(fill) = style.fill {
532            let color = to_rgba(fill, style.alpha);
533            let (mut y0, mut y1) = (f64::INFINITY, f64::NEG_INFINITY);
534            for ring in rings {
535                if ring.len() < 3 {
536                    continue;
537                }
538                for p in ring {
539                    y0 = y0.min(p.1);
540                    y1 = y1.max(p.1);
541                }
542            }
543            if y0.is_finite() && y1.is_finite() {
544                let (y0, y1) = (y0.floor() as i32, y1.ceil() as i32);
545                for y in y0..=y1 {
546                    let yc = y as f64 + 0.5;
547                    let mut xs: Vec<f64> = Vec::new();
548                    for ring in rings {
549                        let n = ring.len();
550                        if n < 3 {
551                            continue;
552                        }
553                        for i in 0..n {
554                            let (ax, ay) = ring[i];
555                            let (bx, by) = ring[(i + 1) % n];
556                            if (ay <= yc && by > yc) || (by <= yc && ay > yc) {
557                                xs.push(ax + (yc - ay) / (by - ay) * (bx - ax));
558                            }
559                        }
560                    }
561                    xs.sort_by(|p, q| p.total_cmp(q));
562                    for span in xs.chunks(2) {
563                        if let [xa, xb] = span {
564                            for x in xa.round() as i32..=xb.round() as i32 {
565                                self.area.draw_pixel((x, y), &color).map_err(map_err)?;
566                            }
567                        }
568                    }
569                }
570            }
571        }
572        if let Some(stroke) = style.stroke {
573            let stroke_color = to_rgba(stroke, style.alpha);
574            let width = if style.stroke_width > 0.0 {
575                (style.stroke_width as u32).max(1)
576            } else {
577                0
578            };
579            if width > 0 {
580                for ring in rings {
581                    if ring.len() < 3 {
582                        continue;
583                    }
584                    let mut pts: Vec<(i32, i32)> =
585                        ring.iter().map(|(x, y)| (*x as i32, *y as i32)).collect();
586                    pts.push(pts[0]);
587                    self.area
588                        .draw(&PathElement::new(pts, stroke_color.stroke_width(width)))
589                        .map_err(map_err)?;
590                }
591            }
592        }
593        Ok(())
594    }
595
596    fn draw_shape(
597        &mut self,
598        center: (f64, f64),
599        radius: f64,
600        style: &PointStyle,
601    ) -> Result<(), RenderError> {
602        // Clip: skip shapes entirely outside the plot area
603        if !point_in_rect(center.0, center.1, &self.plot_area) {
604            return Ok(());
605        }
606        let color = to_rgba(style.color, style.alpha);
607        let (cx, cy) = (center.0 as i32, center.1 as i32);
608        let r = radius as i32;
609
610        match style.shape {
611            PointShape::Circle => self.draw_circle(center, radius, style),
612            PointShape::Square => {
613                let tl = (cx - r, cy - r);
614                let br = (cx + r, cy + r);
615                if style.filled {
616                    self.area
617                        .draw(&plotters::prelude::Rectangle::new([tl, br], color.filled()))
618                        .map_err(map_err)?;
619                } else {
620                    self.area
621                        .draw(&plotters::prelude::Rectangle::new(
622                            [tl, br],
623                            color.stroke_width(1),
624                        ))
625                        .map_err(map_err)?;
626                }
627                Ok(())
628            }
629            PointShape::Triangle => {
630                let pts = vec![(cx, cy - r), (cx - r, cy + r), (cx + r, cy + r)];
631                if style.filled {
632                    self.area
633                        .draw(&Polygon::new(pts, color.filled()))
634                        .map_err(map_err)?;
635                } else {
636                    let outline = vec![
637                        (cx, cy - r),
638                        (cx - r, cy + r),
639                        (cx + r, cy + r),
640                        (cx, cy - r),
641                    ];
642                    self.area
643                        .draw(&PathElement::new(outline, color.stroke_width(1)))
644                        .map_err(map_err)?;
645                }
646                Ok(())
647            }
648            PointShape::Diamond => {
649                let pts = vec![(cx, cy - r), (cx + r, cy), (cx, cy + r), (cx - r, cy)];
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),
658                        (cx, cy + r),
659                        (cx - r, cy),
660                        (cx, cy - r),
661                    ];
662                    self.area
663                        .draw(&PathElement::new(outline, color.stroke_width(1)))
664                        .map_err(map_err)?;
665                }
666                Ok(())
667            }
668            PointShape::Cross => {
669                // X shape
670                self.area
671                    .draw(&PathElement::new(
672                        vec![(cx - r, cy - r), (cx + r, cy + r)],
673                        color.stroke_width(1),
674                    ))
675                    .map_err(map_err)?;
676                self.area
677                    .draw(&PathElement::new(
678                        vec![(cx - r, cy + r), (cx + r, cy - r)],
679                        color.stroke_width(1),
680                    ))
681                    .map_err(map_err)?;
682                Ok(())
683            }
684            PointShape::Plus => {
685                // + shape
686                self.area
687                    .draw(&PathElement::new(
688                        vec![(cx - r, cy), (cx + r, cy)],
689                        color.stroke_width(1),
690                    ))
691                    .map_err(map_err)?;
692                self.area
693                    .draw(&PathElement::new(
694                        vec![(cx, cy - r), (cx, cy + r)],
695                        color.stroke_width(1),
696                    ))
697                    .map_err(map_err)?;
698                Ok(())
699            }
700        }
701    }
702
703    fn plot_area(&self) -> Rect {
704        self.plot_area.clone()
705    }
706
707    fn total_area(&self) -> Rect {
708        self.total_area.clone()
709    }
710}
711
712impl<'a, DB: DrawingBackend> PlottersAdapter<'a, DB> {
713    /// One pass of `text` in `color` at `pos` — factored out of
714    /// `draw_text` so #229's halo can run it several times (once per
715    /// offset, in `bg_colour`) before the normal pass.
716    ///
717    /// (Kept in its own inherent `impl` block: a trait `impl` can't be
718    /// split across two blocks, so this can't live inside
719    /// `impl DrawBackend for PlottersAdapter` alongside the trait's own
720    /// methods.)
721    #[allow(clippy::too_many_arguments)]
722    fn draw_text_pass(
723        &mut self,
724        text: &str,
725        pos: (f64, f64),
726        anchor: TextAnchor,
727        family: &str,
728        size: f64,
729        font_style: FontStyle,
730        angle_i: i32,
731        color: RGBAColor,
732    ) -> Result<(), RenderError> {
733        let font = (family, size).into_font().style(font_style);
734
735        let pos_adj = match anchor {
736            TextAnchor::Start => plotters::style::text_anchor::Pos::new(
737                plotters::style::text_anchor::HPos::Left,
738                plotters::style::text_anchor::VPos::Center,
739            ),
740            TextAnchor::Middle => plotters::style::text_anchor::Pos::new(
741                plotters::style::text_anchor::HPos::Center,
742                plotters::style::text_anchor::VPos::Center,
743            ),
744            TextAnchor::End => plotters::style::text_anchor::Pos::new(
745                plotters::style::text_anchor::HPos::Right,
746                plotters::style::text_anchor::VPos::Center,
747            ),
748        };
749
750        if angle_i != 0 {
751            let (font_transform, rotated_pos) = match angle_i.rem_euclid(360) {
752                80..=100 => (
753                    FontTransform::Rotate90,
754                    plotters::style::text_anchor::Pos::new(
755                        plotters::style::text_anchor::HPos::Center,
756                        plotters::style::text_anchor::VPos::Center,
757                    ),
758                ),
759                170..=190 => (
760                    FontTransform::Rotate180,
761                    plotters::style::text_anchor::Pos::new(
762                        plotters::style::text_anchor::HPos::Center,
763                        plotters::style::text_anchor::VPos::Center,
764                    ),
765                ),
766                // 270° or -90° or 45° approximation (common ggplot2 x-axis rotation)
767                _ => (
768                    FontTransform::Rotate270,
769                    plotters::style::text_anchor::Pos::new(
770                        plotters::style::text_anchor::HPos::Right,
771                        plotters::style::text_anchor::VPos::Center,
772                    ),
773                ),
774            };
775
776            let text_style = plotters::prelude::TextStyle::from((family, size).into_font())
777                .color(&color)
778                .transform(font_transform)
779                .pos(rotated_pos);
780
781            self.area
782                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
783                .map_err(map_err)?;
784        } else {
785            let text_style = plotters::prelude::TextStyle::from(font)
786                .color(&color)
787                .pos(pos_adj);
788            self.area
789                .draw_text(text, &text_style, (pos.0 as i32, pos.1 as i32))
790                .map_err(map_err)?;
791        }
792
793        Ok(())
794    }
795}