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