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