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