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