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