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