Skip to main content

ggplot_rs/render/
renderer.rs

1use crate::aes::Aesthetic;
2use crate::annotate::Annotation;
3use crate::build::BuiltPlot;
4use crate::facet::Facet;
5use crate::guide::{axis, legend};
6use crate::render::backend::{DrawBackend, LineStyle, Linetype, RectStyle, TextAnchor, TextStyle};
7use crate::render::RenderError;
8
9/// Orchestrates rendering of a built plot.
10pub struct PlotRenderer;
11
12impl PlotRenderer {
13    pub fn render(built: &BuiltPlot, backend: &mut dyn DrawBackend) -> Result<(), RenderError> {
14        if !built.facet.is_none() && !built.panels.is_empty() {
15            Self::render_faceted(built, backend)
16        } else {
17            Self::render_single(built, backend)
18        }
19    }
20
21    fn render_single(built: &BuiltPlot, backend: &mut dyn DrawBackend) -> Result<(), RenderError> {
22        let theme = &built.theme;
23        let plot_area = backend.plot_area();
24        let total_area = backend.total_area();
25
26        // 1. Draw plot background
27        if theme.plot_background.visible {
28            if let Some(fill) = theme.plot_background.fill {
29                backend.draw_rect(
30                    (total_area.x, total_area.y),
31                    (
32                        total_area.x + total_area.width,
33                        total_area.y + total_area.height,
34                    ),
35                    &RectStyle {
36                        fill: Some(fill),
37                        stroke: None,
38                        stroke_width: 0.0,
39                        alpha: 1.0,
40                        clip: false,
41                    },
42                )?;
43            }
44        }
45
46        // 2. Draw panel background
47        if theme.panel_background.visible {
48            if let Some(fill) = theme.panel_background.fill {
49                backend.draw_rect(
50                    (plot_area.x, plot_area.y),
51                    (
52                        plot_area.x + plot_area.width,
53                        plot_area.y + plot_area.height,
54                    ),
55                    &RectStyle {
56                        fill: Some(fill),
57                        stroke: theme.panel_background.color,
58                        stroke_width: theme.panel_background.width,
59                        alpha: 1.0,
60                        clip: false,
61                    },
62                )?;
63            }
64        }
65
66        // 2b. Draw panel border
67        if theme.panel_border.visible {
68            let style = LineStyle {
69                color: theme.panel_border.color,
70                width: theme.panel_border.width,
71                alpha: 1.0,
72                linetype: Linetype::Solid,
73            };
74            let x0 = plot_area.x;
75            let y0 = plot_area.y;
76            let x1 = plot_area.x + plot_area.width;
77            let y1 = plot_area.y + plot_area.height;
78            backend.draw_line(&[(x0, y0), (x1, y0)], &style)?;
79            backend.draw_line(&[(x1, y0), (x1, y1)], &style)?;
80            backend.draw_line(&[(x1, y1), (x0, y1)], &style)?;
81            backend.draw_line(&[(x0, y1), (x0, y0)], &style)?;
82        }
83
84        // 3. Draw gridlines
85        let x_scale = built.scales.get(&Aesthetic::X);
86        let y_scale = built.scales.get(&Aesthetic::Y);
87
88        let (h_scale, v_scale) = if built.coord.is_flipped() {
89            (y_scale, x_scale)
90        } else {
91            (x_scale, y_scale)
92        };
93
94        if let (Some(hs), Some(vs)) = (h_scale, v_scale) {
95            if built.coord.gridlines() {
96                axis::draw_gridlines(hs, vs, built.coord.as_ref(), theme, &plot_area, backend)?;
97            }
98
99            // 4. Draw axes
100            axis::draw_x_axis(hs, built.coord.as_ref(), theme, &plot_area, backend)?;
101            axis::draw_y_axis(vs, built.coord.as_ref(), theme, &plot_area, backend)?;
102
103            // 4b. Draw secondary y axis if present
104            if let Some(sec) = built.scales.sec_axis(&Aesthetic::Y) {
105                axis::draw_sec_y_axis(vs, sec, built.coord.as_ref(), theme, &plot_area, backend)?;
106            }
107        }
108
109        // 5. Draw each layer's geometry
110        for layer in &built.layers {
111            layer.geom.draw(
112                &layer.data,
113                built.coord.as_ref(),
114                &built.scales,
115                theme,
116                backend,
117            )?;
118        }
119
120        // 6. Draw annotations
121        Self::draw_annotations(
122            &built.annotations,
123            &built.scales,
124            built.coord.as_ref(),
125            &plot_area,
126            backend,
127        )?;
128
129        // 7. Draw title
130        if let Some(ref title) = built.labels.title {
131            let center_x = plot_area.x + plot_area.width / 2.0;
132            // A top x-axis occupies the space just above the panel — lift the
133            // title above it so they don't overlap.
134            let x_axis_top = built
135                .scales
136                .get(&Aesthetic::X)
137                .map(|s| s.axis_position_opposite())
138                .unwrap_or(false);
139            let x_axis_lift = if x_axis_top {
140                theme.axis_ticks_length + theme.axis_text_x.size + theme.legend_spacing
141            } else {
142                0.0
143            };
144            let title_y = plot_area.y - theme.title.size * 0.9 - x_axis_lift;
145            let family = if theme.title.family.is_empty() {
146                None
147            } else {
148                Some(theme.title.family.clone())
149            };
150            backend.draw_text(
151                title,
152                (center_x, title_y.max(theme.title.size)),
153                &TextStyle {
154                    color: theme.title.color,
155                    size: theme.title.size,
156                    anchor: TextAnchor::Middle,
157                    angle: 0.0,
158                    family,
159                },
160            )?;
161        }
162
163        // 8. Draw subtitle
164        if let Some(ref subtitle) = built.labels.subtitle {
165            let center_x = plot_area.x + plot_area.width / 2.0;
166            let subtitle_y = plot_area.y - 2.0;
167            let family = if theme.subtitle.family.is_empty() {
168                None
169            } else {
170                Some(theme.subtitle.family.clone())
171            };
172            backend.draw_text(
173                subtitle,
174                (
175                    center_x,
176                    subtitle_y.max(theme.title.size + theme.subtitle.size),
177                ),
178                &TextStyle {
179                    color: theme.subtitle.color,
180                    size: theme.subtitle.size,
181                    anchor: TextAnchor::Middle,
182                    angle: 0.0,
183                    family,
184                },
185            )?;
186        }
187
188        // 9. Draw legend
189        legend::draw_legend(
190            &built.scales,
191            theme,
192            &plot_area,
193            backend,
194            &built.guide_legend,
195            &built.suppressed_aes,
196        )?;
197
198        // 10. Draw caption
199        if let Some(ref caption) = built.labels.caption {
200            let right_x = plot_area.x + plot_area.width;
201            let caption_y = total_area.y + total_area.height - theme.caption.size * 0.5;
202            let family = if theme.caption.family.is_empty() {
203                None
204            } else {
205                Some(theme.caption.family.clone())
206            };
207            backend.draw_text(
208                caption,
209                (right_x, caption_y),
210                &TextStyle {
211                    color: theme.caption.color,
212                    size: theme.caption.size,
213                    anchor: TextAnchor::End,
214                    angle: 0.0,
215                    family,
216                },
217            )?;
218        }
219
220        // 11. Draw corner tag (labs(tag = ...))
221        Self::draw_tag(&built.labels, theme, &total_area, backend)?;
222
223        Ok(())
224    }
225
226    /// Draw the corner tag label at the top-left of the plot area.
227    fn draw_tag(
228        labels: &crate::plot::Labels,
229        theme: &crate::theme::Theme,
230        total_area: &crate::render::Rect,
231        backend: &mut dyn DrawBackend,
232    ) -> Result<(), RenderError> {
233        if let Some(ref tag) = labels.tag {
234            let family = if theme.title.family.is_empty() {
235                None
236            } else {
237                Some(theme.title.family.clone())
238            };
239            backend.draw_text(
240                tag,
241                (
242                    total_area.x + theme.title.size,
243                    total_area.y + theme.title.size,
244                ),
245                &TextStyle {
246                    color: theme.title.color,
247                    size: theme.title.size,
248                    anchor: TextAnchor::Start,
249                    angle: 0.0,
250                    family,
251                },
252            )?;
253        }
254        Ok(())
255    }
256
257    fn render_faceted(built: &BuiltPlot, backend: &mut dyn DrawBackend) -> Result<(), RenderError> {
258        let theme = &built.theme;
259        let plot_area = backend.plot_area();
260        let total_area = backend.total_area();
261
262        // Draw plot background
263        if theme.plot_background.visible {
264            if let Some(fill) = theme.plot_background.fill {
265                backend.draw_rect(
266                    (total_area.x, total_area.y),
267                    (
268                        total_area.x + total_area.width,
269                        total_area.y + total_area.height,
270                    ),
271                    &RectStyle {
272                        fill: Some(fill),
273                        stroke: None,
274                        stroke_width: 0.0,
275                        alpha: 1.0,
276                        clip: false,
277                    },
278                )?;
279            }
280        }
281
282        // Compute panel grid dimensions
283        let ncol = match &built.facet {
284            Facet::Wrap { ncol, .. } => {
285                ncol.unwrap_or_else(|| (built.panels.len() as f64).sqrt().ceil() as usize)
286            }
287            Facet::Grid { .. } => built.panels.iter().map(|p| p.col).max().unwrap_or(0) + 1,
288            Facet::None => 1,
289        };
290        let nrow = built.panels.len().div_ceil(ncol);
291
292        let strip_height = theme.strip_text.size + 8.0;
293        let gap_x = theme.get_panel_spacing_x();
294        let gap_y = theme.get_panel_spacing_y();
295
296        // Proportional panel sizing (R's `space =`): free_x sizes columns to
297        // their data x-range, free_y sizes rows to their y-range. Fixed = equal.
298        let space = match &built.facet {
299            Facet::Grid { space, .. } => space.clone(),
300            _ => crate::facet::FacetSpace::Fixed,
301        };
302        let extent = |pi: usize, col: &str| -> Option<f64> {
303            let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
304            for df in &built.panels_data[pi] {
305                if let Some(c) = df.column(col) {
306                    for v in c {
307                        if let Some(f) = v.as_f64() {
308                            lo = lo.min(f);
309                            hi = hi.max(f);
310                        }
311                    }
312                }
313            }
314            (lo <= hi).then_some(hi - lo)
315        };
316        let avail_w = plot_area.width - gap_x * (ncol as f64 - 1.0);
317        let col_widths: Vec<f64> = if space.free_x() && ncol > 0 {
318            let mut ranges = vec![0.0f64; ncol];
319            for (pi, panel) in built.panels.iter().enumerate() {
320                if let Some(r) = extent(pi, "x") {
321                    ranges[panel.col] = ranges[panel.col].max(r);
322                }
323            }
324            let total: f64 = ranges.iter().sum();
325            if total > 0.0 {
326                ranges.iter().map(|r| avail_w * (r / total)).collect()
327            } else {
328                vec![avail_w / ncol as f64; ncol]
329            }
330        } else {
331            vec![avail_w / ncol.max(1) as f64; ncol.max(1)]
332        };
333        let mut col_x = vec![plot_area.x; ncol.max(1)];
334        {
335            let mut acc = plot_area.x;
336            for c in 0..ncol {
337                col_x[c] = acc;
338                acc += col_widths[c] + gap_x;
339            }
340        }
341
342        let avail_h = plot_area.height - gap_y * (nrow as f64 - 1.0) - strip_height * nrow as f64;
343        let row_heights: Vec<f64> = if space.free_y() && nrow > 0 {
344            let mut ranges = vec![0.0f64; nrow];
345            for (pi, panel) in built.panels.iter().enumerate() {
346                if let Some(r) = extent(pi, "y") {
347                    ranges[panel.row] = ranges[panel.row].max(r);
348                }
349            }
350            let total: f64 = ranges.iter().sum();
351            if total > 0.0 {
352                ranges.iter().map(|r| avail_h * (r / total)).collect()
353            } else {
354                vec![avail_h / nrow as f64; nrow]
355            }
356        } else {
357            vec![avail_h / nrow.max(1) as f64; nrow.max(1)]
358        };
359        let mut row_y = vec![plot_area.y; nrow.max(1)];
360        {
361            let mut acc = plot_area.y;
362            for r in 0..nrow {
363                row_y[r] = acc;
364                acc += row_heights[r] + strip_height + gap_y;
365            }
366        }
367
368        for (pi, panel) in built.panels.iter().enumerate() {
369            let panel_width = col_widths[panel.col];
370            let panel_height = row_heights[panel.row];
371            let px = col_x[panel.col];
372            let py = row_y[panel.row];
373
374            let panel_rect = crate::render::Rect {
375                x: px,
376                y: py + strip_height,
377                width: panel_width,
378                height: panel_height,
379            };
380
381            // Strip label background
382            if theme.strip_background.visible {
383                backend.draw_rect(
384                    (px, py),
385                    (px + panel_width, py + strip_height),
386                    &RectStyle {
387                        fill: theme.strip_background.fill,
388                        stroke: theme.strip_background.color,
389                        stroke_width: theme.strip_background.width,
390                        alpha: 1.0,
391                        clip: false,
392                    },
393                )?;
394            }
395
396            // Strip label text
397            if theme.strip_text.visible {
398                let label = panel.col_label.as_deref().unwrap_or(&panel.label);
399                let family = if theme.strip_text.family.is_empty() {
400                    None
401                } else {
402                    Some(theme.strip_text.family.clone())
403                };
404                backend.draw_text(
405                    label,
406                    (px + panel_width / 2.0, py + strip_height / 2.0),
407                    &TextStyle {
408                        color: theme.strip_text.color,
409                        size: theme.strip_text.size,
410                        anchor: TextAnchor::Middle,
411                        angle: 0.0,
412                        family,
413                    },
414                )?;
415            }
416
417            // Panel background
418            if theme.panel_background.visible {
419                if let Some(fill) = theme.panel_background.fill {
420                    backend.draw_rect(
421                        (panel_rect.x, panel_rect.y),
422                        (
423                            panel_rect.x + panel_rect.width,
424                            panel_rect.y + panel_rect.height,
425                        ),
426                        &RectStyle {
427                            fill: Some(fill),
428                            stroke: theme.panel_background.color,
429                            stroke_width: theme.panel_background.width,
430                            alpha: 1.0,
431                            clip: false,
432                        },
433                    )?;
434                }
435            }
436
437            // Panel border
438            if theme.panel_border.visible {
439                let style = LineStyle {
440                    color: theme.panel_border.color,
441                    width: theme.panel_border.width,
442                    alpha: 1.0,
443                    linetype: Linetype::Solid,
444                };
445                let x0 = panel_rect.x;
446                let y0 = panel_rect.y;
447                let x1 = panel_rect.x + panel_rect.width;
448                let y1 = panel_rect.y + panel_rect.height;
449                backend.draw_line(&[(x0, y0), (x1, y0)], &style)?;
450                backend.draw_line(&[(x1, y0), (x1, y1)], &style)?;
451                backend.draw_line(&[(x1, y1), (x0, y1)], &style)?;
452                backend.draw_line(&[(x0, y1), (x0, y0)], &style)?;
453            }
454
455            // Use per-panel scales if free facets, otherwise global scales
456            let panel_scale_set = if pi < built.panel_scales.len() {
457                &built.panel_scales[pi]
458            } else {
459                &built.scales
460            };
461
462            // Gridlines + axes for edge panels
463            let x_scale = panel_scale_set.get(&Aesthetic::X);
464            let y_scale = panel_scale_set.get(&Aesthetic::Y);
465
466            if let (Some(xs), Some(ys)) = (x_scale, y_scale) {
467                if built.coord.gridlines() {
468                    axis::draw_gridlines(
469                        xs,
470                        ys,
471                        built.coord.as_ref(),
472                        theme,
473                        &panel_rect,
474                        backend,
475                    )?;
476                }
477
478                // Bottom row gets x axis
479                if panel.row == nrow - 1 || pi + ncol >= built.panels.len() {
480                    axis::draw_x_axis(xs, built.coord.as_ref(), theme, &panel_rect, backend)?;
481                }
482
483                // Left column gets y axis
484                if panel.col == 0 {
485                    axis::draw_y_axis(ys, built.coord.as_ref(), theme, &panel_rect, backend)?;
486                }
487            }
488
489            // Draw layers for this panel
490            if pi < built.panels_data.len() {
491                for (li, layer_data) in built.panels_data[pi].iter().enumerate() {
492                    if li < built.layers.len() && layer_data.nrows() > 0 {
493                        let mut panel_backend = PanelBackendAdapter {
494                            inner: backend,
495                            panel_rect: panel_rect.clone(),
496                        };
497                        built.layers[li].geom.draw(
498                            layer_data,
499                            built.coord.as_ref(),
500                            panel_scale_set,
501                            theme,
502                            &mut panel_backend,
503                        )?;
504                    }
505                }
506            }
507        }
508
509        // Draw title
510        if let Some(ref title) = built.labels.title {
511            let center_x = plot_area.x + plot_area.width / 2.0;
512            let title_y = plot_area.y - theme.title.size * 0.9;
513            let family = if theme.title.family.is_empty() {
514                None
515            } else {
516                Some(theme.title.family.clone())
517            };
518            backend.draw_text(
519                title,
520                (center_x, title_y.max(theme.title.size)),
521                &TextStyle {
522                    color: theme.title.color,
523                    size: theme.title.size,
524                    anchor: TextAnchor::Middle,
525                    angle: 0.0,
526                    family,
527                },
528            )?;
529        }
530
531        // Draw subtitle
532        if let Some(ref subtitle) = built.labels.subtitle {
533            let center_x = plot_area.x + plot_area.width / 2.0;
534            let subtitle_y = plot_area.y - 2.0;
535            let family = if theme.subtitle.family.is_empty() {
536                None
537            } else {
538                Some(theme.subtitle.family.clone())
539            };
540            backend.draw_text(
541                subtitle,
542                (
543                    center_x,
544                    subtitle_y.max(theme.title.size + theme.subtitle.size),
545                ),
546                &TextStyle {
547                    color: theme.subtitle.color,
548                    size: theme.subtitle.size,
549                    anchor: TextAnchor::Middle,
550                    angle: 0.0,
551                    family,
552                },
553            )?;
554        }
555
556        // Draw caption
557        if let Some(ref caption) = built.labels.caption {
558            let right_x = plot_area.x + plot_area.width;
559            let caption_y = total_area.y + total_area.height - theme.caption.size * 0.5;
560            let family = if theme.caption.family.is_empty() {
561                None
562            } else {
563                Some(theme.caption.family.clone())
564            };
565            backend.draw_text(
566                caption,
567                (right_x, caption_y),
568                &TextStyle {
569                    color: theme.caption.color,
570                    size: theme.caption.size,
571                    anchor: TextAnchor::End,
572                    angle: 0.0,
573                    family,
574                },
575            )?;
576        }
577
578        // Draw annotations
579        Self::draw_annotations(
580            &built.annotations,
581            &built.scales,
582            built.coord.as_ref(),
583            &plot_area,
584            backend,
585        )?;
586
587        // Draw legend
588        legend::draw_legend(
589            &built.scales,
590            theme,
591            &plot_area,
592            backend,
593            &built.guide_legend,
594            &built.suppressed_aes,
595        )?;
596
597        // Draw corner tag (labs(tag = ...))
598        Self::draw_tag(&built.labels, theme, &total_area, backend)?;
599
600        Ok(())
601    }
602
603    fn draw_annotations(
604        annotations: &[Annotation],
605        scales: &crate::scale::ScaleSet,
606        coord: &dyn crate::coord::Coord,
607        plot_area: &crate::render::Rect,
608        backend: &mut dyn DrawBackend,
609    ) -> Result<(), RenderError> {
610        use crate::data::Value;
611
612        let x_scale = scales.get(&Aesthetic::X);
613        let y_scale = scales.get(&Aesthetic::Y);
614
615        for ann in annotations {
616            match ann {
617                Annotation::Text {
618                    label,
619                    x,
620                    y,
621                    size,
622                    color,
623                } => {
624                    let nx = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
625                    let ny = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
626                    let pos = coord.transform((nx, ny), plot_area);
627                    backend.draw_text(
628                        label,
629                        pos,
630                        &TextStyle {
631                            color: *color,
632                            size: *size,
633                            anchor: TextAnchor::Middle,
634                            angle: 0.0,
635                            family: None,
636                        },
637                    )?;
638                }
639                Annotation::Rect {
640                    xmin,
641                    xmax,
642                    ymin,
643                    ymax,
644                    fill,
645                    alpha,
646                } => {
647                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*xmin))).unwrap_or(0.0);
648                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xmax))).unwrap_or(1.0);
649                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*ymin))).unwrap_or(0.0);
650                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*ymax))).unwrap_or(1.0);
651                    let tl = coord.transform((nx0, ny1), plot_area);
652                    let br = coord.transform((nx1, ny0), plot_area);
653                    backend.draw_rect(
654                        tl,
655                        br,
656                        &RectStyle {
657                            fill: Some(*fill),
658                            stroke: None,
659                            stroke_width: 0.0,
660                            alpha: *alpha,
661                            clip: false,
662                        },
663                    )?;
664                }
665                Annotation::Segment {
666                    x,
667                    y,
668                    xend,
669                    yend,
670                    color,
671                    width,
672                } => {
673                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
674                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
675                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xend))).unwrap_or(1.0);
676                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*yend))).unwrap_or(1.0);
677                    let p0 = coord.transform((nx0, ny0), plot_area);
678                    let p1 = coord.transform((nx1, ny1), plot_area);
679                    backend.draw_line(
680                        &[p0, p1],
681                        &LineStyle {
682                            color: *color,
683                            alpha: 1.0,
684                            width: *width,
685                            linetype: Linetype::Solid,
686                        },
687                    )?;
688                }
689            }
690        }
691        Ok(())
692    }
693}
694
695/// Wrapper that overrides plot_area() to return the panel rect.
696struct PanelBackendAdapter<'a> {
697    inner: &'a mut dyn DrawBackend,
698    panel_rect: crate::render::Rect,
699}
700
701impl<'a> DrawBackend for PanelBackendAdapter<'a> {
702    fn draw_circle(
703        &mut self,
704        center: (f64, f64),
705        radius: f64,
706        style: &crate::render::backend::PointStyle,
707    ) -> Result<(), RenderError> {
708        self.inner.draw_circle(center, radius, style)
709    }
710    fn draw_line(
711        &mut self,
712        points: &[(f64, f64)],
713        style: &crate::render::backend::LineStyle,
714    ) -> Result<(), RenderError> {
715        self.inner.draw_line(points, style)
716    }
717    fn draw_rect(
718        &mut self,
719        top_left: (f64, f64),
720        bottom_right: (f64, f64),
721        style: &RectStyle,
722    ) -> Result<(), RenderError> {
723        self.inner.draw_rect(top_left, bottom_right, style)
724    }
725    fn draw_text(
726        &mut self,
727        text: &str,
728        pos: (f64, f64),
729        style: &TextStyle,
730    ) -> Result<(), RenderError> {
731        self.inner.draw_text(text, pos, style)
732    }
733    fn draw_polygon(
734        &mut self,
735        points: &[(f64, f64)],
736        style: &RectStyle,
737    ) -> Result<(), RenderError> {
738        self.inner.draw_polygon(points, style)
739    }
740    fn draw_shape(
741        &mut self,
742        center: (f64, f64),
743        radius: f64,
744        style: &crate::render::backend::PointStyle,
745    ) -> Result<(), RenderError> {
746        self.inner.draw_shape(center, radius, style)
747    }
748    fn plot_area(&self) -> crate::render::Rect {
749        self.panel_rect.clone()
750    }
751    fn total_area(&self) -> crate::render::Rect {
752        self.inner.total_area()
753    }
754}