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