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