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