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        // #273: fresh panel-corner claim registry for this panel — see
26        // `render::corner_layout`. An inside legend draws *after* every
27        // layer (step 9 below), so its box is pre-claimed here, before
28        // any corner-furniture geom (annotation_scale/attribution/
29        // bivariate_legend) in the layer loop gets a chance to draw.
30        backend.clear_corner_claims();
31        if let Some(r) = built.coord.data_rect(&plot_area) {
32            backend.set_viewport(&r);
33        }
34        if let Some(rect) = legend::inside_legend_box(
35            &built.scales,
36            theme,
37            &plot_area,
38            &total_area,
39            &built.guide_legend,
40            &built.suppressed_aes,
41            &built.legend_plans,
42        ) {
43            backend.claim_corner_box(rect);
44        }
45
46        // 1. Draw plot background
47        if theme.plot_background.visible {
48            if let Some(fill) = theme.plot_background.fill {
49                backend.draw_rect(
50                    (total_area.x, total_area.y),
51                    (
52                        total_area.x + total_area.width,
53                        total_area.y + total_area.height,
54                    ),
55                    &RectStyle {
56                        fill: Some(fill),
57                        stroke: None,
58                        stroke_width: 0.0,
59                        alpha: 1.0,
60                        clip: false,
61                        corner_radius: 0.0,
62                    },
63                )?;
64            }
65        }
66
67        // 2. Draw panel background
68        if theme.panel_background.visible {
69            if let Some(fill) = theme.panel_background.fill {
70                backend.draw_rect(
71                    (plot_area.x, plot_area.y),
72                    (
73                        plot_area.x + plot_area.width,
74                        plot_area.y + plot_area.height,
75                    ),
76                    &RectStyle {
77                        fill: Some(fill),
78                        stroke: theme.panel_background.color,
79                        stroke_width: theme.panel_background.width,
80                        alpha: 1.0,
81                        clip: false,
82                        corner_radius: 0.0,
83                    },
84                )?;
85            }
86        }
87
88        // 2b. Draw panel border
89        if theme.panel_border.visible {
90            let style = LineStyle {
91                color: theme.panel_border.color,
92                width: theme.panel_border.width,
93                alpha: 1.0,
94                linetype: Linetype::Solid,
95                ..Default::default()
96            };
97            let x0 = plot_area.x;
98            let y0 = plot_area.y;
99            let x1 = plot_area.x + plot_area.width;
100            let y1 = plot_area.y + plot_area.height;
101            backend.draw_line(&[(x0, y0), (x1, y0)], &style)?;
102            backend.draw_line(&[(x1, y0), (x1, y1)], &style)?;
103            backend.draw_line(&[(x1, y1), (x0, y1)], &style)?;
104            backend.draw_line(&[(x0, y1), (x0, y0)], &style)?;
105        }
106
107        // 3. Draw gridlines
108        let x_scale = built.scales.get(&Aesthetic::X);
109        let y_scale = built.scales.get(&Aesthetic::Y);
110
111        let (h_scale, v_scale) = if built.coord.is_flipped() {
112            (y_scale, x_scale)
113        } else {
114            (x_scale, y_scale)
115        };
116
117        if let (true, Some(xs), Some(ys)) = (built.coord.is_radar(), x_scale, y_scale) {
118            // Radar: rings + spokes + labels replace cartesian grid/axes.
119            crate::guide::polar::draw_radar_guides(
120                xs,
121                ys,
122                built.coord.as_ref(),
123                theme,
124                &plot_area,
125                backend,
126            )?;
127        } else if let (Some(hs), Some(vs)) = (h_scale, v_scale) {
128            // Gridlines under the data, unless panel.ontop draws them over it.
129            if built.coord.gridlines() && !theme.panel_ontop {
130                axis::draw_gridlines(hs, vs, built.coord.as_ref(), theme, &plot_area, backend)?;
131            }
132
133            // 4. Draw axes
134            axis::draw_x_axis(
135                hs,
136                built.coord.as_ref(),
137                theme,
138                &plot_area,
139                &total_area,
140                backend,
141            )?;
142            axis::draw_y_axis(vs, built.coord.as_ref(), theme, &plot_area, backend)?;
143
144            // 4b. Draw secondary y axis if present
145            if let Some(sec) = built.scales.sec_axis(&Aesthetic::Y) {
146                axis::draw_sec_y_axis(vs, sec, built.coord.as_ref(), theme, &plot_area, backend)?;
147            }
148        }
149
150        // 5. Draw each layer's geometry
151        // A layer whose data is empty (no input rows, or a stat that produced
152        // nothing) simply draws nothing.
153        for layer in built.layers.iter().filter(|l| l.data.nrows() > 0) {
154            backend.set_mark_geom(Some(layer.geom.name()));
155            let drawn = layer.geom.draw(
156                &layer.data,
157                built.coord.as_ref(),
158                &built.scales,
159                theme,
160                backend,
161            );
162            backend.set_mark_geom(None);
163            drawn?;
164        }
165
166        // 5b. Gridlines on top of the data (R's panel.ontop).
167        if theme.panel_ontop && built.coord.gridlines() {
168            if let (Some(hs), Some(vs)) = (h_scale, v_scale) {
169                axis::draw_gridlines(hs, vs, built.coord.as_ref(), theme, &plot_area, backend)?;
170            }
171        }
172
173        // 6. Draw annotations
174        Self::draw_annotations(
175            &built.annotations,
176            &built.scales,
177            built.coord.as_ref(),
178            &plot_area,
179            theme,
180            backend,
181        )?;
182
183        // 7. Draw title
184        let title_ref = Self::title_ref_rect(theme, &plot_area, &total_area);
185        // #233: a Top legend reserves its own band just above the panel,
186        // same idea as a top x-axis's x_axis_lift below — lift title and
187        // subtitle above it too, or they'd land inside the legend's band
188        // instead of their own (zero when there's no Top legend to draw).
189        let legend_lift = legend::top_legend_lift(
190            theme,
191            &built.scales,
192            &built.suppressed_aes,
193            &built.legend_plans,
194            &built.guide_legend,
195        );
196        if let Some(ref title) = built.labels.title {
197            let (tx, anchor) = Self::hjust_pos(&theme.title, &title_ref);
198            // A top x-axis occupies the space just above the panel — lift the
199            // title above it so they don't overlap.
200            let x_axis_top = built
201                .scales
202                .get(&Aesthetic::X)
203                .map(|s| s.axis_position_opposite())
204                .unwrap_or(false);
205            let x_axis_lift = if x_axis_top {
206                theme.axis_ticks_length + theme.axis_text_x.size + theme.legend_spacing
207            } else {
208                0.0
209            };
210            let (title_y, _) = Self::title_subtitle_y(
211                theme,
212                plot_area.y,
213                x_axis_lift + legend_lift,
214                built.labels.subtitle.is_some(),
215            );
216            let family = if theme.title.family.is_empty() {
217                None
218            } else {
219                Some(theme.title.family.clone())
220            };
221            backend.draw_text(
222                title,
223                (tx, title_y),
224                &TextStyle {
225                    color: theme.title.color,
226                    size: theme.title.size,
227                    anchor,
228                    angle: 0.0,
229                    family,
230                    face: theme.title.face,
231                    bg_colour: None,
232                    bg_r: 0.0,
233                },
234            )?;
235        }
236
237        // 8. Draw subtitle
238        if let Some(ref subtitle) = built.labels.subtitle {
239            let (sx, anchor) = Self::hjust_pos(&theme.subtitle, &title_ref);
240            // #281: lifted above a Top legend's band; a top x-axis too.
241            let x_axis_top = built
242                .scales
243                .get(&Aesthetic::X)
244                .map(|s| s.axis_position_opposite())
245                .unwrap_or(false);
246            let x_axis_lift = if x_axis_top {
247                theme.axis_ticks_length + theme.axis_text_x.size + theme.legend_spacing
248            } else {
249                0.0
250            };
251            let (_, subtitle_y) =
252                Self::title_subtitle_y(theme, plot_area.y, x_axis_lift + legend_lift, true);
253            let family = if theme.subtitle.family.is_empty() {
254                None
255            } else {
256                Some(theme.subtitle.family.clone())
257            };
258            backend.draw_text(
259                subtitle,
260                (sx, subtitle_y),
261                &TextStyle {
262                    color: theme.subtitle.color,
263                    size: theme.subtitle.size,
264                    anchor,
265                    angle: 0.0,
266                    family,
267                    face: theme.subtitle.face,
268                    bg_colour: None,
269                    bg_r: 0.0,
270                },
271            )?;
272        }
273
274        // 9. Draw legend
275        legend::draw_legend_planned(
276            &built.scales,
277            theme,
278            &plot_area,
279            plot_area.width,
280            backend,
281            &built.guide_legend,
282            &built.suppressed_aes,
283            &built.legend_plans,
284        )?;
285
286        // 10. Draw caption
287        if let Some(ref caption) = built.labels.caption {
288            let (cx, anchor) = Self::hjust_pos(&theme.caption, &title_ref);
289            // Inside the layout's `caption_area`: the bottom of the
290            // canvas less `plot_margin.bottom` (#286).
291            let caption_y = total_area.y + total_area.height
292                - theme.plot_margin.bottom.max(0.0)
293                - theme.caption.size * 0.5;
294            let family = if theme.caption.family.is_empty() {
295                None
296            } else {
297                Some(theme.caption.family.clone())
298            };
299            backend.draw_text(
300                caption,
301                (cx, caption_y),
302                &TextStyle {
303                    color: theme.caption.color,
304                    size: theme.caption.size,
305                    anchor,
306                    angle: 0.0,
307                    family,
308                    face: theme.caption.face,
309                    bg_colour: None,
310                    bg_r: 0.0,
311                },
312            )?;
313        }
314
315        // 11. Draw corner tag (labs(tag = ...))
316        Self::draw_tag(&built.labels, theme, &total_area, backend)?;
317
318        Ok(())
319    }
320
321    /// Horizontal position + anchor for a theme text element, from its `hjust`
322    /// (0 = left, 0.5 = center, 1 = right), relative to `area`.
323    fn hjust_pos(
324        el: &crate::theme::elements::ElementText,
325        area: &crate::render::Rect,
326    ) -> (f64, TextAnchor) {
327        let x = area.x + el.hjust.clamp(0.0, 1.0) * area.width;
328        let anchor = if el.hjust <= 0.02 {
329            TextAnchor::Start
330        } else if el.hjust >= 0.98 {
331            TextAnchor::End
332        } else {
333            TextAnchor::Middle
334        };
335        (x, anchor)
336    }
337
338    /// Vertical centres `(title_y, subtitle_y)` of the title and subtitle
339    /// (#384). Text is drawn `dominant-baseline="middle"`, so these are the
340    /// line centres, stacked bottom-up from the panel top inside the bands
341    /// `PlotLayout` reserves: the subtitle in its `1.5 × size` band directly
342    /// above the panel (less `lift` — a top x-axis and/or a Top legend,
343    /// #233/#281), the title above that. Without a subtitle the title keeps
344    /// its old place, `0.9 × size` above the panel. The title is floored at
345    /// `title.size` so it never leaves the canvas.
346    fn title_subtitle_y(
347        theme: &crate::theme::Theme,
348        panel_top: f64,
349        lift: f64,
350        has_subtitle: bool,
351    ) -> (f64, f64) {
352        let base = panel_top - lift;
353        let subtitle_band = if has_subtitle {
354            theme.subtitle.size * 1.5
355        } else {
356            0.0
357        };
358        let subtitle_y = base - subtitle_band / 2.0;
359        let title_y = (base - subtitle_band - theme.title.size * 0.9).max(theme.title.size);
360        (title_y, subtitle_y)
361    }
362
363    /// Horizontal reference rect for titles: the panel or the whole plot width
364    /// (R's `plot.title.position`).
365    fn title_ref_rect(
366        theme: &crate::theme::Theme,
367        plot_area: &crate::render::Rect,
368        total_area: &crate::render::Rect,
369    ) -> crate::render::Rect {
370        match theme.title_position {
371            crate::theme::TitlePosition::Panel => plot_area.clone(),
372            crate::theme::TitlePosition::Plot => crate::render::Rect {
373                x: total_area.x + theme.plot_margin.left,
374                y: plot_area.y,
375                width: (total_area.width - theme.plot_margin.left - theme.plot_margin.right)
376                    .max(1.0),
377                height: plot_area.height,
378            },
379        }
380    }
381
382    /// Draw the corner tag label (position from `plot.tag.position`).
383    /// [`draw_tag`](Self::draw_tag) for a non-plot grid cell (#378).
384    pub(crate) fn draw_cell_tag(
385        labels: &crate::plot::Labels,
386        theme: &crate::theme::Theme,
387        area: &crate::render::Rect,
388        backend: &mut dyn DrawBackend,
389    ) -> Result<(), RenderError> {
390        Self::draw_tag(labels, theme, area, backend)
391    }
392
393    fn draw_tag(
394        labels: &crate::plot::Labels,
395        theme: &crate::theme::Theme,
396        total_area: &crate::render::Rect,
397        backend: &mut dyn DrawBackend,
398    ) -> Result<(), RenderError> {
399        if let Some(ref tag) = labels.tag {
400            let family = if theme.title.family.is_empty() {
401                None
402            } else {
403                Some(theme.title.family.clone())
404            };
405            use crate::theme::TagPosition::*;
406            let pad = theme.title.size;
407            let (left, right) = (total_area.x + pad, total_area.x + total_area.width - pad);
408            let (top, bottom) = (total_area.y + pad, total_area.y + total_area.height - pad);
409            let (x, y, anchor) = match theme.tag_position {
410                TopLeft => (left, top, TextAnchor::Start),
411                TopRight => (right, top, TextAnchor::End),
412                BottomLeft => (left, bottom, TextAnchor::Start),
413                BottomRight => (right, bottom, TextAnchor::End),
414            };
415            backend.draw_text(
416                tag,
417                (x, y),
418                &TextStyle {
419                    color: theme.title.color,
420                    size: theme.title.size,
421                    anchor,
422                    angle: 0.0,
423                    family,
424                    face: theme.title.face,
425                    bg_colour: None,
426                    bg_r: 0.0,
427                },
428            )?;
429        }
430        Ok(())
431    }
432
433    fn render_faceted(built: &BuiltPlot, backend: &mut dyn DrawBackend) -> Result<(), RenderError> {
434        let theme = &built.theme;
435        let plot_area = backend.plot_area();
436        let total_area = backend.total_area();
437
438        // Draw plot background
439        if theme.plot_background.visible {
440            if let Some(fill) = theme.plot_background.fill {
441                backend.draw_rect(
442                    (total_area.x, total_area.y),
443                    (
444                        total_area.x + total_area.width,
445                        total_area.y + total_area.height,
446                    ),
447                    &RectStyle {
448                        fill: Some(fill),
449                        stroke: None,
450                        stroke_width: 0.0,
451                        alpha: 1.0,
452                        clip: false,
453                        corner_radius: 0.0,
454                    },
455                )?;
456            }
457        }
458
459        // Compute panel grid dimensions
460        let ncol = match &built.facet {
461            Facet::Wrap { ncol, .. } => {
462                ncol.unwrap_or_else(|| (built.panels.len() as f64).sqrt().ceil() as usize)
463            }
464            Facet::Grid { .. } => built.panels.iter().map(|p| p.col).max().unwrap_or(0) + 1,
465            Facet::None => 1,
466        };
467        let mut nrow = built.panels.len().div_ceil(ncol);
468
469        let strip_height = theme.strip_text.size + 8.0;
470        let gap_x = theme.get_panel_spacing_x();
471        let gap_y = theme.get_panel_spacing_y();
472
473        // Figure-level layers (`GeomAttribution::figure`): one band under
474        // the whole grid, which shrinks by its height.
475        let is_figure = |li: usize| {
476            built.layers[li]
477                .geom
478                .figure_band_height(plot_area.width, theme)
479                .is_some()
480        };
481        let band_h: f64 = built
482            .layers
483            .iter()
484            .filter_map(|l| l.geom.figure_band_height(plot_area.width, theme))
485            .sum::<f64>()
486            .min(plot_area.height * 0.5);
487        let band_rect = crate::render::Rect {
488            x: plot_area.x,
489            y: plot_area.y + plot_area.height - band_h,
490            width: plot_area.width,
491            height: band_h,
492        };
493        let plot_area = crate::render::Rect {
494            height: plot_area.height - band_h,
495            ..plot_area
496        };
497
498        // `facet_wrap` under a fixed-aspect coord (`coord_sf` maps): equal
499        // panels shaped like the data, in the column count that makes them
500        // largest (or the given `ncol`), centred in the plot area — instead
501        // of `sqrt(n)` arbitrary cells each letterboxing its map.
502        let fixed_grid = match (&built.facet, built.coord.fixed_aspect()) {
503            (Facet::Wrap { ncol: want, .. }, Some(aspect)) => Some(crate::facet::wrap::fit_grid(
504                built.panels.len(),
505                *want,
506                aspect,
507                plot_area.width,
508                plot_area.height,
509                strip_height,
510                gap_x,
511                gap_y,
512            )),
513            _ => None,
514        };
515        let ncol = if let Some(g) = fixed_grid {
516            nrow = g.nrow;
517            g.ncol
518        } else {
519            ncol
520        };
521        // Each panel's (row, col): from its index under the fixed grid (the
522        // build-time row/col assumed the `sqrt(n)` default), else as built.
523        let cell: Vec<(usize, usize)> = built
524            .panels
525            .iter()
526            .enumerate()
527            .map(|(i, p)| {
528                if fixed_grid.is_some() {
529                    (i / ncol, i % ncol)
530                } else {
531                    (p.row, p.col)
532                }
533            })
534            .collect();
535
536        // Proportional panel sizing (R's `space =`): free_x sizes columns to
537        // their data x-range, free_y sizes rows to their y-range. Fixed = equal.
538        let space = match &built.facet {
539            Facet::Grid { space, .. } => space.clone(),
540            _ => crate::facet::FacetSpace::Fixed,
541        };
542        let extent = |pi: usize, col: &str| -> Option<f64> {
543            let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
544            for df in &built.panels_data[pi] {
545                if let Some(c) = df.column(col) {
546                    for v in c {
547                        if let Some(f) = v.as_f64() {
548                            lo = lo.min(f);
549                            hi = hi.max(f);
550                        }
551                    }
552                }
553            }
554            (lo <= hi).then_some(hi - lo)
555        };
556        let avail_w = plot_area.width - gap_x * (ncol as f64 - 1.0);
557        let col_widths: Vec<f64> = if let Some(g) = fixed_grid {
558            vec![g.panel_w; ncol]
559        } else if space.free_x() && ncol > 0 {
560            let mut ranges = vec![0.0f64; ncol];
561            for (pi, panel) in built.panels.iter().enumerate() {
562                if let Some(r) = extent(pi, "x") {
563                    ranges[panel.col] = ranges[panel.col].max(r);
564                }
565            }
566            let total: f64 = ranges.iter().sum();
567            if total > 0.0 {
568                ranges.iter().map(|r| avail_w * (r / total)).collect()
569            } else {
570                vec![avail_w / ncol as f64; ncol]
571            }
572        } else {
573            vec![avail_w / ncol.max(1) as f64; ncol.max(1)]
574        };
575        let mut col_x = vec![plot_area.x; ncol.max(1)];
576        {
577            let used_w: f64 = col_widths.iter().sum::<f64>() + gap_x * (ncol as f64 - 1.0);
578            let mut acc = if fixed_grid.is_some() {
579                plot_area.x + ((plot_area.width - used_w) / 2.0).max(0.0)
580            } else {
581                plot_area.x
582            };
583            for c in 0..ncol {
584                col_x[c] = acc;
585                acc += col_widths[c] + gap_x;
586            }
587        }
588
589        let avail_h = plot_area.height - gap_y * (nrow as f64 - 1.0) - strip_height * nrow as f64;
590        let row_heights: Vec<f64> = if let Some(g) = fixed_grid {
591            vec![g.panel_h; nrow]
592        } else if space.free_y() && nrow > 0 {
593            let mut ranges = vec![0.0f64; nrow];
594            for (pi, panel) in built.panels.iter().enumerate() {
595                if let Some(r) = extent(pi, "y") {
596                    ranges[panel.row] = ranges[panel.row].max(r);
597                }
598            }
599            let total: f64 = ranges.iter().sum();
600            if total > 0.0 {
601                ranges.iter().map(|r| avail_h * (r / total)).collect()
602            } else {
603                vec![avail_h / nrow as f64; nrow]
604            }
605        } else {
606            vec![avail_h / nrow.max(1) as f64; nrow.max(1)]
607        };
608        let mut row_y = vec![plot_area.y; nrow.max(1)];
609        {
610            let used_h: f64 =
611                row_heights.iter().sum::<f64>() + (strip_height + gap_y) * nrow as f64 - gap_y;
612            let mut acc = if fixed_grid.is_some() {
613                plot_area.y + ((plot_area.height - used_h) / 2.0).max(0.0)
614            } else {
615                plot_area.y
616            };
617            for r in 0..nrow {
618                row_y[r] = acc;
619                acc += row_heights[r] + strip_height + gap_y;
620            }
621        }
622
623        // #348: one x / y axis title for the whole grid (ggplot2), drawn
624        // after the panels over the union of the panels carrying that axis.
625        // The y title also needs the widest of those panels' tick labels.
626        let mut x_title_area: Option<super::Rect> = None;
627        let mut y_title_area: Option<(super::Rect, f64)> = None;
628        let union = |a: &Option<super::Rect>, b: &super::Rect| match a {
629            None => b.clone(),
630            Some(a) => {
631                let (x0, y0) = (a.x.min(b.x), a.y.min(b.y));
632                let x1 = (a.x + a.width).max(b.x + b.width);
633                let y1 = (a.y + a.height).max(b.y + b.height);
634                super::Rect {
635                    x: x0,
636                    y: y0,
637                    width: x1 - x0,
638                    height: y1 - y0,
639                }
640            }
641        };
642
643        for (pi, panel) in built.panels.iter().enumerate() {
644            let (prow, pcol) = cell[pi];
645            let panel_width = col_widths[pcol];
646            let panel_height = row_heights[prow];
647            let px = col_x[pcol];
648            let py = row_y[prow];
649
650            let panel_rect = crate::render::Rect {
651                x: px,
652                y: py + strip_height,
653                width: panel_width,
654                height: panel_height,
655            };
656            // #273: fresh panel-corner claim registry per facet panel —
657            // see `render_single`'s identical reset. A faceted plot's
658            // inside legend (if any) is drawn once globally, not per
659            // panel, so it isn't pre-claimed here the way `render_single`
660            // pre-claims it — map furniture geoms under faceting still
661            // stack clear of each other within one panel, just not of a
662            // faceted inside legend.
663            backend.clear_corner_claims();
664
665            // Strip label background
666            if theme.strip_background.visible {
667                backend.draw_rect(
668                    (px, py),
669                    (px + panel_width, py + strip_height),
670                    &RectStyle {
671                        fill: theme.strip_background.fill,
672                        stroke: theme.strip_background.color,
673                        stroke_width: theme.strip_background.width,
674                        alpha: 1.0,
675                        clip: false,
676                        corner_radius: 0.0,
677                    },
678                )?;
679            }
680
681            // Strip label text
682            if theme.strip_text.visible {
683                let label = panel.col_label.as_deref().unwrap_or(&panel.label);
684                let family = if theme.strip_text.family.is_empty() {
685                    None
686                } else {
687                    Some(theme.strip_text.family.clone())
688                };
689                backend.draw_text(
690                    label,
691                    (px + panel_width / 2.0, py + strip_height / 2.0),
692                    &TextStyle {
693                        color: theme.strip_text.color,
694                        size: theme.strip_text.size,
695                        anchor: TextAnchor::Middle,
696                        angle: 0.0,
697                        family,
698                        face: theme.strip_text.face,
699                        bg_colour: None,
700                        bg_r: 0.0,
701                    },
702                )?;
703            }
704
705            // Panel background
706            if theme.panel_background.visible {
707                if let Some(fill) = theme.panel_background.fill {
708                    backend.draw_rect(
709                        (panel_rect.x, panel_rect.y),
710                        (
711                            panel_rect.x + panel_rect.width,
712                            panel_rect.y + panel_rect.height,
713                        ),
714                        &RectStyle {
715                            fill: Some(fill),
716                            stroke: theme.panel_background.color,
717                            stroke_width: theme.panel_background.width,
718                            alpha: 1.0,
719                            clip: false,
720                            corner_radius: 0.0,
721                        },
722                    )?;
723                }
724            }
725
726            // Panel border
727            if theme.panel_border.visible {
728                let style = LineStyle {
729                    color: theme.panel_border.color,
730                    width: theme.panel_border.width,
731                    alpha: 1.0,
732                    linetype: Linetype::Solid,
733                    ..Default::default()
734                };
735                let x0 = panel_rect.x;
736                let y0 = panel_rect.y;
737                let x1 = panel_rect.x + panel_rect.width;
738                let y1 = panel_rect.y + panel_rect.height;
739                backend.draw_line(&[(x0, y0), (x1, y0)], &style)?;
740                backend.draw_line(&[(x1, y0), (x1, y1)], &style)?;
741                backend.draw_line(&[(x1, y1), (x0, y1)], &style)?;
742                backend.draw_line(&[(x0, y1), (x0, y0)], &style)?;
743            }
744
745            // Use per-panel scales if free facets, otherwise global scales
746            let panel_scale_set = if pi < built.panel_scales.len() {
747                &built.panel_scales[pi]
748            } else {
749                &built.scales
750            };
751
752            // Gridlines + axes for edge panels
753            let x_scale = panel_scale_set.get(&Aesthetic::X);
754            let y_scale = panel_scale_set.get(&Aesthetic::Y);
755
756            if let (Some(xs), Some(ys)) = (x_scale, y_scale) {
757                if built.coord.gridlines() {
758                    axis::draw_gridlines(
759                        xs,
760                        ys,
761                        built.coord.as_ref(),
762                        theme,
763                        &panel_rect,
764                        backend,
765                    )?;
766                }
767
768                // Bottom row gets x axis
769                if prow == nrow - 1 || pi + ncol >= built.panels.len() {
770                    axis::draw_x_axis_with(
771                        xs,
772                        built.coord.as_ref(),
773                        theme,
774                        &panel_rect,
775                        &total_area,
776                        false,
777                        backend,
778                    )?;
779                    x_title_area = Some(union(&x_title_area, &panel_rect));
780                }
781
782                // Left column gets y axis
783                if pcol == 0 {
784                    axis::draw_y_axis_with(
785                        ys,
786                        built.coord.as_ref(),
787                        theme,
788                        &panel_rect,
789                        false,
790                        backend,
791                    )?;
792                    let reserve = axis::y_label_reserve(theme, &ys.breaks());
793                    let prev = y_title_area.as_ref().map(|(r, _)| r.clone());
794                    let widest = y_title_area.as_ref().map_or(0.0, |(_, w)| *w);
795                    y_title_area = Some((union(&prev, &panel_rect), widest.max(reserve)));
796                }
797            }
798
799            // Draw layers for this panel — under a fixed-aspect wrap (maps)
800            // in their own clipped group, so one panel's features never
801            // spill into the next.
802            if fixed_grid.is_some() {
803                let data = built
804                    .coord
805                    .data_rect(&panel_rect)
806                    .unwrap_or_else(|| panel_rect.clone());
807                backend.begin_panel(pi, &panel.label, &panel_rect, &data);
808            }
809            if pi < built.panels_data.len() {
810                for (li, layer_data) in built.panels_data[pi].iter().enumerate() {
811                    if li < built.layers.len() && layer_data.nrows() > 0 && !is_figure(li) {
812                        let mut panel_backend = PanelBackendAdapter {
813                            inner: backend,
814                            panel_rect: panel_rect.clone(),
815                        };
816                        let geom = &built.layers[li].geom;
817                        panel_backend.set_mark_geom(Some(geom.name()));
818                        let drawn = geom.draw(
819                            layer_data,
820                            built.coord.as_ref(),
821                            panel_scale_set,
822                            theme,
823                            &mut panel_backend,
824                        );
825                        panel_backend.set_mark_geom(None);
826                        drawn?;
827                    }
828                }
829            }
830            if fixed_grid.is_some() {
831                backend.end_panel();
832            }
833        }
834
835        // The grid's single axis titles (#348), against the first panel's
836        // scales (the titles are the same on every panel).
837        let title_scales = built.panel_scales.first().unwrap_or(&built.scales);
838        if let (Some(area), Some(xs)) = (&x_title_area, title_scales.get(&Aesthetic::X)) {
839            axis::draw_x_axis_title(xs, theme, area, backend)?;
840        }
841        if let (Some((area, reserve)), Some(ys)) = (&y_title_area, title_scales.get(&Aesthetic::Y))
842        {
843            axis::draw_y_axis_title(ys, theme, area, *reserve, backend)?;
844        }
845
846        // Figure-level layers, once, in the band under the grid.
847        if band_h > 0.0 {
848            backend.clear_corner_claims();
849            for li in (0..built.layers.len()).filter(|&li| is_figure(li)) {
850                let empty = crate::data::DataFrame::new();
851                let data = built
852                    .panels_data
853                    .iter()
854                    .filter_map(|p| p.get(li))
855                    .find(|d| d.nrows() > 0)
856                    .unwrap_or(&empty);
857                let mut band_backend = PanelBackendAdapter {
858                    inner: backend,
859                    panel_rect: band_rect.clone(),
860                };
861                let geom = &built.layers[li].geom;
862                band_backend.set_mark_geom(Some(geom.name()));
863                let drawn = geom.draw(
864                    data,
865                    built.coord.as_ref(),
866                    &built.scales,
867                    theme,
868                    &mut band_backend,
869                );
870                band_backend.set_mark_geom(None);
871                drawn?;
872            }
873        }
874
875        // Draw title
876        let title_ref = Self::title_ref_rect(theme, &plot_area, &total_area);
877        // #233: see render_single's identical lift — a Top legend must not
878        // overlap the title/subtitle (zero when there's no Top legend).
879        let legend_lift = legend::top_legend_lift(
880            theme,
881            &built.scales,
882            &built.suppressed_aes,
883            &built.legend_plans,
884            &built.guide_legend,
885        );
886        if let Some(ref title) = built.labels.title {
887            let (tx, anchor) = Self::hjust_pos(&theme.title, &title_ref);
888            let (title_y, _) = Self::title_subtitle_y(
889                theme,
890                plot_area.y,
891                legend_lift,
892                built.labels.subtitle.is_some(),
893            );
894            let family = if theme.title.family.is_empty() {
895                None
896            } else {
897                Some(theme.title.family.clone())
898            };
899            backend.draw_text(
900                title,
901                (tx, title_y),
902                &TextStyle {
903                    color: theme.title.color,
904                    size: theme.title.size,
905                    anchor,
906                    angle: 0.0,
907                    family,
908                    face: theme.title.face,
909                    bg_colour: None,
910                    bg_r: 0.0,
911                },
912            )?;
913        }
914
915        // Draw subtitle
916        if let Some(ref subtitle) = built.labels.subtitle {
917            let (sx, anchor) = Self::hjust_pos(&theme.subtitle, &title_ref);
918            let (_, subtitle_y) = Self::title_subtitle_y(theme, plot_area.y, legend_lift, true);
919            let family = if theme.subtitle.family.is_empty() {
920                None
921            } else {
922                Some(theme.subtitle.family.clone())
923            };
924            backend.draw_text(
925                subtitle,
926                (sx, subtitle_y),
927                &TextStyle {
928                    color: theme.subtitle.color,
929                    size: theme.subtitle.size,
930                    anchor,
931                    angle: 0.0,
932                    family,
933                    face: theme.subtitle.face,
934                    bg_colour: None,
935                    bg_r: 0.0,
936                },
937            )?;
938        }
939
940        // Draw caption
941        if let Some(ref caption) = built.labels.caption {
942            let (cx, anchor) = Self::hjust_pos(&theme.caption, &title_ref);
943            // Inside the layout's `caption_area`: the bottom of the
944            // canvas less `plot_margin.bottom` (#286).
945            let caption_y = total_area.y + total_area.height
946                - theme.plot_margin.bottom.max(0.0)
947                - theme.caption.size * 0.5;
948            let family = if theme.caption.family.is_empty() {
949                None
950            } else {
951                Some(theme.caption.family.clone())
952            };
953            backend.draw_text(
954                caption,
955                (cx, caption_y),
956                &TextStyle {
957                    color: theme.caption.color,
958                    size: theme.caption.size,
959                    anchor,
960                    angle: 0.0,
961                    family,
962                    face: theme.caption.face,
963                    bg_colour: None,
964                    bg_r: 0.0,
965                },
966            )?;
967        }
968
969        // Draw annotations
970        Self::draw_annotations(
971            &built.annotations,
972            &built.scales,
973            built.coord.as_ref(),
974            &plot_area,
975            theme,
976            backend,
977        )?;
978
979        // Draw legend
980        legend::draw_legend_planned(
981            &built.scales,
982            theme,
983            &plot_area,
984            plot_area.width,
985            backend,
986            &built.guide_legend,
987            &built.suppressed_aes,
988            &built.legend_plans,
989        )?;
990
991        // Draw corner tag (labs(tag = ...))
992        Self::draw_tag(&built.labels, theme, &total_area, backend)?;
993
994        Ok(())
995    }
996
997    /// `annotate_text`/`annotate_segment` under theme ink/paper (#157,
998    /// follow-up to #142): `Annotation`'s fields are all `pub` with no
999    /// `#[non_exhaustive]`, so it can't gain an "explicitly styled" flag
1000    /// without a semver break — the same `value == hard-coded default`
1001    /// gate `apply_element_geom`'s other callers already use (e.g.
1002    /// `geom/refline.rs`'s `(50, 50, 50)` → `ink`) is applied here instead.
1003    /// Default theme (`geom.ink` unset) is byte-identical.
1004    ///
1005    /// `annotate_rect`'s grey fill is deliberately excluded: checked
1006    /// against ggplot2 4.0.3 (`theme(geom = element_geom(ink = "white",
1007    /// paper = "black"))`), `annotate("text")`/`annotate("segment")`
1008    /// recolour to `ink` but `annotate("rect")`'s fill stays the same
1009    /// hard-coded grey (`#A6A6A6`) regardless — ggplot2 itself doesn't
1010    /// route a rect annotation's fill through ink/paper.
1011    fn draw_annotations(
1012        annotations: &[Annotation],
1013        scales: &crate::scale::ScaleSet,
1014        coord: &dyn crate::coord::Coord,
1015        plot_area: &crate::render::Rect,
1016        theme: &crate::theme::Theme,
1017        backend: &mut dyn DrawBackend,
1018    ) -> Result<(), RenderError> {
1019        use crate::data::Value;
1020
1021        let x_scale = scales.get(&Aesthetic::X);
1022        let y_scale = scales.get(&Aesthetic::Y);
1023
1024        for ann in annotations {
1025            match ann {
1026                Annotation::Text {
1027                    label,
1028                    x,
1029                    y,
1030                    size,
1031                    color,
1032                } => {
1033                    let nx = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
1034                    let ny = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
1035                    let pos = coord.transform((nx, ny), plot_area);
1036                    backend.draw_text(
1037                        label,
1038                        pos,
1039                        &TextStyle {
1040                            color: annotation_ink(*color, theme),
1041                            size: *size,
1042                            anchor: TextAnchor::Middle,
1043                            angle: 0.0,
1044                            family: crate::geom::resolve_font_family("", theme),
1045                            face: crate::render::backend::FontFace::Plain,
1046                            bg_colour: None,
1047                            bg_r: 0.0,
1048                        },
1049                    )?;
1050                }
1051                Annotation::Rect {
1052                    xmin,
1053                    xmax,
1054                    ymin,
1055                    ymax,
1056                    fill,
1057                    alpha,
1058                } => {
1059                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*xmin))).unwrap_or(0.0);
1060                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xmax))).unwrap_or(1.0);
1061                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*ymin))).unwrap_or(0.0);
1062                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*ymax))).unwrap_or(1.0);
1063                    let tl = coord.transform((nx0, ny1), plot_area);
1064                    let br = coord.transform((nx1, ny0), plot_area);
1065                    backend.draw_rect(
1066                        tl,
1067                        br,
1068                        &RectStyle {
1069                            fill: Some(*fill),
1070                            stroke: None,
1071                            stroke_width: 0.0,
1072                            alpha: *alpha,
1073                            clip: false,
1074                            corner_radius: 0.0,
1075                        },
1076                    )?;
1077                }
1078                Annotation::Segment {
1079                    x,
1080                    y,
1081                    xend,
1082                    yend,
1083                    color,
1084                    width,
1085                } => {
1086                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
1087                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
1088                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xend))).unwrap_or(1.0);
1089                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*yend))).unwrap_or(1.0);
1090                    let p0 = coord.transform((nx0, ny0), plot_area);
1091                    let p1 = coord.transform((nx1, ny1), plot_area);
1092                    backend.draw_line(
1093                        &[p0, p1],
1094                        &LineStyle {
1095                            color: annotation_ink(*color, theme),
1096                            alpha: 1.0,
1097                            width: *width,
1098                            linetype: Linetype::Solid,
1099                            ..Default::default()
1100                        },
1101                    )?;
1102                }
1103            }
1104        }
1105        Ok(())
1106    }
1107}
1108
1109/// `color`, recoloured from `theme.geom.ink` when it's still at
1110/// `annotate_text`/`annotate_segment`'s hard-coded default (`(50, 50,
1111/// 50)`) and the theme actually sets `ink` — see the doc comment on
1112/// [`PlotRenderer::draw_annotations`]. `None`/an explicitly-styled colour
1113/// (anything else) passes through unchanged.
1114fn annotation_ink(color: (u8, u8, u8), theme: &crate::theme::Theme) -> (u8, u8, u8) {
1115    match (color == (50, 50, 50), theme.geom.ink) {
1116        (true, Some(ink)) => ink,
1117        _ => color,
1118    }
1119}
1120
1121/// Wrapper that overrides plot_area() to return the panel rect.
1122struct PanelBackendAdapter<'a> {
1123    inner: &'a mut dyn DrawBackend,
1124    panel_rect: crate::render::Rect,
1125}
1126
1127impl<'a> DrawBackend for PanelBackendAdapter<'a> {
1128    fn draw_image(
1129        &mut self,
1130        bytes: &[u8],
1131        rect: &crate::render::Rect,
1132        clip: &crate::render::Rect,
1133        alpha: f64,
1134    ) -> Result<(), RenderError> {
1135        self.inner.draw_image(bytes, rect, clip, alpha)
1136    }
1137    fn draw_circle(
1138        &mut self,
1139        center: (f64, f64),
1140        radius: f64,
1141        style: &crate::render::backend::PointStyle,
1142    ) -> Result<(), RenderError> {
1143        self.inner.draw_circle(center, radius, style)
1144    }
1145    fn draw_line(
1146        &mut self,
1147        points: &[(f64, f64)],
1148        style: &crate::render::backend::LineStyle,
1149    ) -> Result<(), RenderError> {
1150        self.inner.draw_line(points, style)
1151    }
1152    fn draw_quadratic(
1153        &mut self,
1154        p0: (f64, f64),
1155        ctrl: (f64, f64),
1156        p1: (f64, f64),
1157        style: &crate::render::backend::LineStyle,
1158    ) -> Result<(), RenderError> {
1159        self.inner.draw_quadratic(p0, ctrl, p1, style)
1160    }
1161    fn draw_rect(
1162        &mut self,
1163        top_left: (f64, f64),
1164        bottom_right: (f64, f64),
1165        style: &RectStyle,
1166    ) -> Result<(), RenderError> {
1167        self.inner.draw_rect(top_left, bottom_right, style)
1168    }
1169    fn draw_text(
1170        &mut self,
1171        text: &str,
1172        pos: (f64, f64),
1173        style: &TextStyle,
1174    ) -> Result<(), RenderError> {
1175        self.inner.draw_text(text, pos, style)
1176    }
1177    fn draw_polygon(
1178        &mut self,
1179        points: &[(f64, f64)],
1180        style: &RectStyle,
1181    ) -> Result<(), RenderError> {
1182        self.inner.draw_polygon(points, style)
1183    }
1184    fn draw_polygon_rings(
1185        &mut self,
1186        rings: &[Vec<(f64, f64)>],
1187        style: &RectStyle,
1188    ) -> Result<(), RenderError> {
1189        self.inner.draw_polygon_rings(rings, style)
1190    }
1191    fn draw_shape(
1192        &mut self,
1193        center: (f64, f64),
1194        radius: f64,
1195        style: &crate::render::backend::PointStyle,
1196    ) -> Result<(), RenderError> {
1197        self.inner.draw_shape(center, radius, style)
1198    }
1199    fn plot_area(&self) -> crate::render::Rect {
1200        self.panel_rect.clone()
1201    }
1202    fn set_tooltip(&mut self, tooltip: Option<String>) {
1203        self.inner.set_tooltip(tooltip)
1204    }
1205    fn set_mark_axis(&mut self, key: Option<String>) {
1206        self.inner.set_mark_axis(key)
1207    }
1208    fn set_mark_series(&mut self, series: Option<String>) {
1209        self.inner.set_mark_series(series)
1210    }
1211    fn set_mark_value(&mut self, value: Option<String>) {
1212        self.inner.set_mark_value(value)
1213    }
1214    fn set_mark_geom(&mut self, geom: Option<&str>) {
1215        self.inner.set_mark_geom(geom)
1216    }
1217    fn warn(&mut self, message: String) {
1218        self.inner.warn(message)
1219    }
1220    fn set_mark_part(&mut self, part: Option<&str>) {
1221        self.inner.set_mark_part(part)
1222    }
1223    fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1224        self.inner.set_mark_attrs(attrs)
1225    }
1226
1227    fn set_mark_class(&mut self, class: Option<String>) {
1228        self.inner.set_mark_class(class)
1229    }
1230
1231    fn set_mark_group(&mut self, group: Option<(String, String)>) {
1232        self.inner.set_mark_group(group)
1233    }
1234
1235    fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1236        self.inner.set_layer_attrs(attrs)
1237    }
1238    fn wants_formatted_values(&self) -> bool {
1239        self.inner.wants_formatted_values()
1240    }
1241    fn set_mark_fvalue(&mut self, value: Option<String>) {
1242        self.inner.set_mark_fvalue(value)
1243    }
1244    fn set_mark_fx(&mut self, value: Option<String>) {
1245        self.inner.set_mark_fx(value)
1246    }
1247    fn total_area(&self) -> crate::render::Rect {
1248        self.inner.total_area()
1249    }
1250    fn corner_claims(&self) -> Vec<crate::render::Rect> {
1251        self.inner.corner_claims()
1252    }
1253    fn claim_corner_box(&mut self, rect: crate::render::Rect) {
1254        self.inner.claim_corner_box(rect)
1255    }
1256    fn clear_corner_claims(&mut self) {
1257        self.inner.clear_corner_claims()
1258    }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::*;
1264    use crate::render::backend::LineStyle;
1265    use crate::render::svg_backend::SvgBackend;
1266
1267    /// `PanelBackendAdapter` (every geom draws through one — see the panel
1268    /// loop in `PlotRenderer::render`) forwards `draw_quadratic`,
1269    /// `set_mark_part` and `set_mark_attrs` to its inner backend, and
1270    /// reports the *panel* rect (not the inner backend's full area) as its
1271    /// `plot_area`.
1272    #[test]
1273    fn panel_backend_adapter_forwards_quadratic_and_mark_metadata() {
1274        let total = crate::render::Rect {
1275            x: 0.0,
1276            y: 0.0,
1277            width: 200.0,
1278            height: 200.0,
1279        };
1280        let panel_rect = crate::render::Rect {
1281            x: 10.0,
1282            y: 10.0,
1283            width: 80.0,
1284            height: 80.0,
1285        };
1286        let mut backend = SvgBackend::new(200, 200, total);
1287        let mut adapter = PanelBackendAdapter {
1288            inner: &mut backend,
1289            panel_rect: panel_rect.clone(),
1290        };
1291
1292        assert_eq!(adapter.plot_area().x, panel_rect.x);
1293        assert_eq!(adapter.plot_area().width, panel_rect.width);
1294
1295        adapter.set_mark_part(Some("arrow"));
1296        adapter.set_mark_attrs(vec![("data-x".to_string(), "v".to_string())]);
1297        adapter
1298            .draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
1299            .unwrap();
1300        adapter.set_mark_part(None);
1301        adapter.set_mark_attrs(Vec::new());
1302
1303        let svg = backend.finish();
1304        assert!(svg.contains("<path"));
1305        assert!(svg.contains("data-part=\"arrow\""));
1306        assert!(svg.contains("data-x=\"v\""));
1307    }
1308}