Skip to main content

ggplot_rs/render/
plotters_backend.rs

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