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            if built.coord.gridlines() {
96                axis::draw_gridlines(hs, vs, built.coord.as_ref(), theme, &plot_area, backend)?;
97            }
98
99            // 4. Draw axes
100            axis::draw_x_axis(hs, built.coord.as_ref(), theme, &plot_area, backend)?;
101            axis::draw_y_axis(vs, built.coord.as_ref(), theme, &plot_area, backend)?;
102
103            // 4b. Draw secondary y axis if present
104            if let Some(sec) = built.scales.sec_axis(&Aesthetic::Y) {
105                axis::draw_sec_y_axis(vs, sec, built.coord.as_ref(), theme, &plot_area, backend)?;
106            }
107        }
108
109        // 5. Draw each layer's geometry
110        for layer in &built.layers {
111            layer.geom.draw(
112                &layer.data,
113                built.coord.as_ref(),
114                &built.scales,
115                theme,
116                backend,
117            )?;
118        }
119
120        // 6. Draw annotations
121        Self::draw_annotations(
122            &built.annotations,
123            &built.scales,
124            built.coord.as_ref(),
125            &plot_area,
126            backend,
127        )?;
128
129        // 7. Draw title
130        if let Some(ref title) = built.labels.title {
131            let center_x = plot_area.x + plot_area.width / 2.0;
132            let title_y = plot_area.y - theme.title.size * 0.9;
133            let family = if theme.title.family.is_empty() {
134                None
135            } else {
136                Some(theme.title.family.clone())
137            };
138            backend.draw_text(
139                title,
140                (center_x, title_y.max(theme.title.size)),
141                &TextStyle {
142                    color: theme.title.color,
143                    size: theme.title.size,
144                    anchor: TextAnchor::Middle,
145                    angle: 0.0,
146                    family,
147                },
148            )?;
149        }
150
151        // 8. Draw subtitle
152        if let Some(ref subtitle) = built.labels.subtitle {
153            let center_x = plot_area.x + plot_area.width / 2.0;
154            let subtitle_y = plot_area.y - 2.0;
155            let family = if theme.subtitle.family.is_empty() {
156                None
157            } else {
158                Some(theme.subtitle.family.clone())
159            };
160            backend.draw_text(
161                subtitle,
162                (
163                    center_x,
164                    subtitle_y.max(theme.title.size + theme.subtitle.size),
165                ),
166                &TextStyle {
167                    color: theme.subtitle.color,
168                    size: theme.subtitle.size,
169                    anchor: TextAnchor::Middle,
170                    angle: 0.0,
171                    family,
172                },
173            )?;
174        }
175
176        // 9. Draw legend
177        legend::draw_legend(
178            &built.scales,
179            theme,
180            &plot_area,
181            backend,
182            &built.guide_legend,
183            &built.suppressed_aes,
184        )?;
185
186        // 10. Draw caption
187        if let Some(ref caption) = built.labels.caption {
188            let right_x = plot_area.x + plot_area.width;
189            let caption_y = total_area.y + total_area.height - theme.caption.size * 0.5;
190            let family = if theme.caption.family.is_empty() {
191                None
192            } else {
193                Some(theme.caption.family.clone())
194            };
195            backend.draw_text(
196                caption,
197                (right_x, caption_y),
198                &TextStyle {
199                    color: theme.caption.color,
200                    size: theme.caption.size,
201                    anchor: TextAnchor::End,
202                    angle: 0.0,
203                    family,
204                },
205            )?;
206        }
207
208        Ok(())
209    }
210
211    fn render_faceted(built: &BuiltPlot, backend: &mut dyn DrawBackend) -> Result<(), RenderError> {
212        let theme = &built.theme;
213        let plot_area = backend.plot_area();
214        let total_area = backend.total_area();
215
216        // Draw plot background
217        if theme.plot_background.visible {
218            if let Some(fill) = theme.plot_background.fill {
219                backend.draw_rect(
220                    (total_area.x, total_area.y),
221                    (
222                        total_area.x + total_area.width,
223                        total_area.y + total_area.height,
224                    ),
225                    &RectStyle {
226                        fill: Some(fill),
227                        stroke: None,
228                        stroke_width: 0.0,
229                        alpha: 1.0,
230                        clip: false,
231                    },
232                )?;
233            }
234        }
235
236        // Compute panel grid dimensions
237        let ncol = match &built.facet {
238            Facet::Wrap { ncol, .. } => {
239                ncol.unwrap_or_else(|| (built.panels.len() as f64).sqrt().ceil() as usize)
240            }
241            Facet::Grid { .. } => built.panels.iter().map(|p| p.col).max().unwrap_or(0) + 1,
242            Facet::None => 1,
243        };
244        let nrow = built.panels.len().div_ceil(ncol);
245
246        let strip_height = theme.strip_text.size + 8.0;
247        let gap_x = theme.get_panel_spacing_x();
248        let gap_y = theme.get_panel_spacing_y();
249        let panel_width = (plot_area.width - gap_x * (ncol as f64 - 1.0)) / ncol as f64;
250        let panel_height =
251            (plot_area.height - gap_y * (nrow as f64 - 1.0) - strip_height * nrow as f64)
252                / nrow as f64;
253
254        for (pi, panel) in built.panels.iter().enumerate() {
255            let px = plot_area.x + panel.col as f64 * (panel_width + gap_x);
256            let py = plot_area.y + panel.row as f64 * (panel_height + strip_height + gap_y);
257
258            let panel_rect = crate::render::Rect {
259                x: px,
260                y: py + strip_height,
261                width: panel_width,
262                height: panel_height,
263            };
264
265            // Strip label background
266            if theme.strip_background.visible {
267                backend.draw_rect(
268                    (px, py),
269                    (px + panel_width, py + strip_height),
270                    &RectStyle {
271                        fill: theme.strip_background.fill,
272                        stroke: theme.strip_background.color,
273                        stroke_width: theme.strip_background.width,
274                        alpha: 1.0,
275                        clip: false,
276                    },
277                )?;
278            }
279
280            // Strip label text
281            if theme.strip_text.visible {
282                let label = panel.col_label.as_deref().unwrap_or(&panel.label);
283                let family = if theme.strip_text.family.is_empty() {
284                    None
285                } else {
286                    Some(theme.strip_text.family.clone())
287                };
288                backend.draw_text(
289                    label,
290                    (px + panel_width / 2.0, py + strip_height / 2.0),
291                    &TextStyle {
292                        color: theme.strip_text.color,
293                        size: theme.strip_text.size,
294                        anchor: TextAnchor::Middle,
295                        angle: 0.0,
296                        family,
297                    },
298                )?;
299            }
300
301            // Panel background
302            if theme.panel_background.visible {
303                if let Some(fill) = theme.panel_background.fill {
304                    backend.draw_rect(
305                        (panel_rect.x, panel_rect.y),
306                        (
307                            panel_rect.x + panel_rect.width,
308                            panel_rect.y + panel_rect.height,
309                        ),
310                        &RectStyle {
311                            fill: Some(fill),
312                            stroke: theme.panel_background.color,
313                            stroke_width: theme.panel_background.width,
314                            alpha: 1.0,
315                            clip: false,
316                        },
317                    )?;
318                }
319            }
320
321            // Panel border
322            if theme.panel_border.visible {
323                let style = LineStyle {
324                    color: theme.panel_border.color,
325                    width: theme.panel_border.width,
326                    alpha: 1.0,
327                    linetype: Linetype::Solid,
328                };
329                let x0 = panel_rect.x;
330                let y0 = panel_rect.y;
331                let x1 = panel_rect.x + panel_rect.width;
332                let y1 = panel_rect.y + panel_rect.height;
333                backend.draw_line(&[(x0, y0), (x1, y0)], &style)?;
334                backend.draw_line(&[(x1, y0), (x1, y1)], &style)?;
335                backend.draw_line(&[(x1, y1), (x0, y1)], &style)?;
336                backend.draw_line(&[(x0, y1), (x0, y0)], &style)?;
337            }
338
339            // Use per-panel scales if free facets, otherwise global scales
340            let panel_scale_set = if pi < built.panel_scales.len() {
341                &built.panel_scales[pi]
342            } else {
343                &built.scales
344            };
345
346            // Gridlines + axes for edge panels
347            let x_scale = panel_scale_set.get(&Aesthetic::X);
348            let y_scale = panel_scale_set.get(&Aesthetic::Y);
349
350            if let (Some(xs), Some(ys)) = (x_scale, y_scale) {
351                if built.coord.gridlines() {
352                    axis::draw_gridlines(
353                        xs,
354                        ys,
355                        built.coord.as_ref(),
356                        theme,
357                        &panel_rect,
358                        backend,
359                    )?;
360                }
361
362                // Bottom row gets x axis
363                if panel.row == nrow - 1 || pi + ncol >= built.panels.len() {
364                    axis::draw_x_axis(xs, built.coord.as_ref(), theme, &panel_rect, backend)?;
365                }
366
367                // Left column gets y axis
368                if panel.col == 0 {
369                    axis::draw_y_axis(ys, built.coord.as_ref(), theme, &panel_rect, backend)?;
370                }
371            }
372
373            // Draw layers for this panel
374            if pi < built.panels_data.len() {
375                for (li, layer_data) in built.panels_data[pi].iter().enumerate() {
376                    if li < built.layers.len() && layer_data.nrows() > 0 {
377                        let mut panel_backend = PanelBackendAdapter {
378                            inner: backend,
379                            panel_rect: panel_rect.clone(),
380                        };
381                        built.layers[li].geom.draw(
382                            layer_data,
383                            built.coord.as_ref(),
384                            panel_scale_set,
385                            theme,
386                            &mut panel_backend,
387                        )?;
388                    }
389                }
390            }
391        }
392
393        // Draw title
394        if let Some(ref title) = built.labels.title {
395            let center_x = plot_area.x + plot_area.width / 2.0;
396            let title_y = plot_area.y - theme.title.size * 0.9;
397            let family = if theme.title.family.is_empty() {
398                None
399            } else {
400                Some(theme.title.family.clone())
401            };
402            backend.draw_text(
403                title,
404                (center_x, title_y.max(theme.title.size)),
405                &TextStyle {
406                    color: theme.title.color,
407                    size: theme.title.size,
408                    anchor: TextAnchor::Middle,
409                    angle: 0.0,
410                    family,
411                },
412            )?;
413        }
414
415        // Draw subtitle
416        if let Some(ref subtitle) = built.labels.subtitle {
417            let center_x = plot_area.x + plot_area.width / 2.0;
418            let subtitle_y = plot_area.y - 2.0;
419            let family = if theme.subtitle.family.is_empty() {
420                None
421            } else {
422                Some(theme.subtitle.family.clone())
423            };
424            backend.draw_text(
425                subtitle,
426                (
427                    center_x,
428                    subtitle_y.max(theme.title.size + theme.subtitle.size),
429                ),
430                &TextStyle {
431                    color: theme.subtitle.color,
432                    size: theme.subtitle.size,
433                    anchor: TextAnchor::Middle,
434                    angle: 0.0,
435                    family,
436                },
437            )?;
438        }
439
440        // Draw caption
441        if let Some(ref caption) = built.labels.caption {
442            let right_x = plot_area.x + plot_area.width;
443            let caption_y = total_area.y + total_area.height - theme.caption.size * 0.5;
444            let family = if theme.caption.family.is_empty() {
445                None
446            } else {
447                Some(theme.caption.family.clone())
448            };
449            backend.draw_text(
450                caption,
451                (right_x, caption_y),
452                &TextStyle {
453                    color: theme.caption.color,
454                    size: theme.caption.size,
455                    anchor: TextAnchor::End,
456                    angle: 0.0,
457                    family,
458                },
459            )?;
460        }
461
462        // Draw annotations
463        Self::draw_annotations(
464            &built.annotations,
465            &built.scales,
466            built.coord.as_ref(),
467            &plot_area,
468            backend,
469        )?;
470
471        // Draw legend
472        legend::draw_legend(
473            &built.scales,
474            theme,
475            &plot_area,
476            backend,
477            &built.guide_legend,
478            &built.suppressed_aes,
479        )?;
480
481        Ok(())
482    }
483
484    fn draw_annotations(
485        annotations: &[Annotation],
486        scales: &crate::scale::ScaleSet,
487        coord: &dyn crate::coord::Coord,
488        plot_area: &crate::render::Rect,
489        backend: &mut dyn DrawBackend,
490    ) -> Result<(), RenderError> {
491        use crate::data::Value;
492
493        let x_scale = scales.get(&Aesthetic::X);
494        let y_scale = scales.get(&Aesthetic::Y);
495
496        for ann in annotations {
497            match ann {
498                Annotation::Text {
499                    label,
500                    x,
501                    y,
502                    size,
503                    color,
504                } => {
505                    let nx = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
506                    let ny = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
507                    let pos = coord.transform((nx, ny), plot_area);
508                    backend.draw_text(
509                        label,
510                        pos,
511                        &TextStyle {
512                            color: *color,
513                            size: *size,
514                            anchor: TextAnchor::Middle,
515                            angle: 0.0,
516                            family: None,
517                        },
518                    )?;
519                }
520                Annotation::Rect {
521                    xmin,
522                    xmax,
523                    ymin,
524                    ymax,
525                    fill,
526                    alpha,
527                } => {
528                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*xmin))).unwrap_or(0.0);
529                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xmax))).unwrap_or(1.0);
530                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*ymin))).unwrap_or(0.0);
531                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*ymax))).unwrap_or(1.0);
532                    let tl = coord.transform((nx0, ny1), plot_area);
533                    let br = coord.transform((nx1, ny0), plot_area);
534                    backend.draw_rect(
535                        tl,
536                        br,
537                        &RectStyle {
538                            fill: Some(*fill),
539                            stroke: None,
540                            stroke_width: 0.0,
541                            alpha: *alpha,
542                            clip: false,
543                        },
544                    )?;
545                }
546                Annotation::Segment {
547                    x,
548                    y,
549                    xend,
550                    yend,
551                    color,
552                    width,
553                } => {
554                    let nx0 = x_scale.map(|s| s.map(&Value::Float(*x))).unwrap_or(0.0);
555                    let ny0 = y_scale.map(|s| s.map(&Value::Float(*y))).unwrap_or(0.0);
556                    let nx1 = x_scale.map(|s| s.map(&Value::Float(*xend))).unwrap_or(1.0);
557                    let ny1 = y_scale.map(|s| s.map(&Value::Float(*yend))).unwrap_or(1.0);
558                    let p0 = coord.transform((nx0, ny0), plot_area);
559                    let p1 = coord.transform((nx1, ny1), plot_area);
560                    backend.draw_line(
561                        &[p0, p1],
562                        &LineStyle {
563                            color: *color,
564                            alpha: 1.0,
565                            width: *width,
566                            linetype: Linetype::Solid,
567                        },
568                    )?;
569                }
570            }
571        }
572        Ok(())
573    }
574}
575
576/// Wrapper that overrides plot_area() to return the panel rect.
577struct PanelBackendAdapter<'a> {
578    inner: &'a mut dyn DrawBackend,
579    panel_rect: crate::render::Rect,
580}
581
582impl<'a> DrawBackend for PanelBackendAdapter<'a> {
583    fn draw_circle(
584        &mut self,
585        center: (f64, f64),
586        radius: f64,
587        style: &crate::render::backend::PointStyle,
588    ) -> Result<(), RenderError> {
589        self.inner.draw_circle(center, radius, style)
590    }
591    fn draw_line(
592        &mut self,
593        points: &[(f64, f64)],
594        style: &crate::render::backend::LineStyle,
595    ) -> Result<(), RenderError> {
596        self.inner.draw_line(points, style)
597    }
598    fn draw_rect(
599        &mut self,
600        top_left: (f64, f64),
601        bottom_right: (f64, f64),
602        style: &RectStyle,
603    ) -> Result<(), RenderError> {
604        self.inner.draw_rect(top_left, bottom_right, style)
605    }
606    fn draw_text(
607        &mut self,
608        text: &str,
609        pos: (f64, f64),
610        style: &TextStyle,
611    ) -> Result<(), RenderError> {
612        self.inner.draw_text(text, pos, style)
613    }
614    fn draw_polygon(
615        &mut self,
616        points: &[(f64, f64)],
617        style: &RectStyle,
618    ) -> Result<(), RenderError> {
619        self.inner.draw_polygon(points, style)
620    }
621    fn draw_shape(
622        &mut self,
623        center: (f64, f64),
624        radius: f64,
625        style: &crate::render::backend::PointStyle,
626    ) -> Result<(), RenderError> {
627        self.inner.draw_shape(center, radius, style)
628    }
629    fn plot_area(&self) -> crate::render::Rect {
630        self.panel_rect.clone()
631    }
632    fn total_area(&self) -> crate::render::Rect {
633        self.inner.total_area()
634    }
635}