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