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