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