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