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