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