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