1use std::ops::{BitOr, Div};
27
28use crate::guide::legend;
29use crate::plot::{GGError, GGPlot, RenderMeta};
30use crate::render::backend::{
31 DrawBackend, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
32};
33use crate::render::renderer::PlotRenderer;
34use crate::render::svg_backend::{root_data_attrs, SvgBackend};
35use crate::render::{Rect, RenderError};
36use crate::theme::{LegendPosition, Theme};
37
38#[derive(Clone, Debug, PartialEq)]
40pub enum TagLevels {
41 Lower,
43 Upper,
45 Numeric,
47 LowerRoman,
49 UpperRoman,
51 Custom(Vec<String>),
53}
54
55impl TagLevels {
56 pub fn tag(&self, i: usize) -> String {
58 match self {
59 TagLevels::Lower => alpha(i, b'a'),
60 TagLevels::Upper => alpha(i, b'A'),
61 TagLevels::Numeric => (i + 1).to_string(),
62 TagLevels::LowerRoman => roman(i + 1).to_lowercase(),
63 TagLevels::UpperRoman => roman(i + 1),
64 TagLevels::Custom(v) => v.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()),
65 }
66 }
67}
68
69fn alpha(mut i: usize, base: u8) -> String {
70 let mut out = Vec::new();
72 loop {
73 out.push(base + (i % 26) as u8);
74 if i < 26 {
75 break;
76 }
77 i = i / 26 - 1;
78 }
79 out.reverse();
80 String::from_utf8(out).unwrap_or_default()
81}
82
83fn roman(mut n: usize) -> String {
84 if n >= 4000 {
85 return n.to_string();
86 }
87 const T: [(usize, &str); 13] = [
88 (1000, "M"),
89 (900, "CM"),
90 (500, "D"),
91 (400, "CD"),
92 (100, "C"),
93 (90, "XC"),
94 (50, "L"),
95 (40, "XL"),
96 (10, "X"),
97 (9, "IX"),
98 (5, "V"),
99 (4, "IV"),
100 (1, "I"),
101 ];
102 let mut s = String::new();
103 for (v, r) in T {
104 while n >= v {
105 s.push_str(r);
106 n -= v;
107 }
108 }
109 s
110}
111
112#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
114pub enum GridLegendPosition {
115 #[default]
117 Right,
118 Bottom,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125enum Op {
126 Row,
127 Col,
128}
129
130pub enum GridCell {
132 Plot(Box<GGPlot>),
133 Grid(Box<PlotGrid>),
134 Spacer,
136}
137
138impl From<GGPlot> for GridCell {
139 fn from(p: GGPlot) -> Self {
140 GridCell::Plot(Box::new(p))
141 }
142}
143
144impl From<PlotGrid> for GridCell {
145 fn from(g: PlotGrid) -> Self {
146 GridCell::Grid(Box::new(g))
147 }
148}
149
150pub struct PlotGrid {
169 cells: Vec<GridCell>,
170 ncol: Option<usize>,
171 nrow: Option<usize>,
172 byrow: bool,
173 widths: Vec<f64>,
174 heights: Vec<f64>,
175 title: Option<String>,
176 subtitle: Option<String>,
177 caption: Option<String>,
178 tags: Option<TagLevels>,
179 tag_prefix: String,
180 tag_suffix: String,
181 collect_legends: bool,
182 legend_position: GridLegendPosition,
183 spacing: f64,
184 theme: Theme,
185 op: Option<Op>,
186 root_attrs: Vec<(String, String)>,
189 svg_warnings_attr: bool,
194}
195
196impl Default for PlotGrid {
197 fn default() -> Self {
198 PlotGrid {
199 cells: Vec::new(),
200 ncol: None,
201 nrow: None,
202 byrow: true,
203 widths: Vec::new(),
204 heights: Vec::new(),
205 title: None,
206 subtitle: None,
207 caption: None,
208 tags: None,
209 tag_prefix: String::new(),
210 tag_suffix: String::new(),
211 collect_legends: false,
212 legend_position: GridLegendPosition::Right,
213 spacing: 0.0,
214 theme: Theme::default(),
215 op: None,
216 root_attrs: Vec::new(),
217 svg_warnings_attr: false,
218 }
219 }
220}
221
222const MAX_TRACKS: usize = 10_000;
224
225impl PlotGrid {
226 pub fn new() -> Self {
227 Self::default()
228 }
229
230 #[allow(clippy::should_implement_trait)]
232 pub fn add(mut self, cell: impl Into<GridCell>) -> Self {
233 self.cells.push(cell.into());
234 self
235 }
236
237 pub fn add_all(mut self, plots: impl IntoIterator<Item = GGPlot>) -> Self {
239 self.cells.extend(plots.into_iter().map(GridCell::from));
240 self
241 }
242
243 pub fn add_spacer(mut self) -> Self {
245 self.cells.push(GridCell::Spacer);
246 self
247 }
248
249 pub fn ncol(mut self, n: usize) -> Self {
251 self.ncol = (n > 0).then_some(n.min(MAX_TRACKS));
252 self
253 }
254
255 pub fn nrow(mut self, n: usize) -> Self {
257 self.nrow = (n > 0).then_some(n.min(MAX_TRACKS));
258 self
259 }
260
261 pub fn byrow(mut self, byrow: bool) -> Self {
263 self.byrow = byrow;
264 self
265 }
266
267 pub fn widths(mut self, widths: &[f64]) -> Self {
269 self.widths = widths.to_vec();
270 self
271 }
272
273 pub fn heights(mut self, heights: &[f64]) -> Self {
275 self.heights = heights.to_vec();
276 self
277 }
278
279 pub fn title(mut self, title: &str) -> Self {
281 self.title = Some(title.to_string());
282 self
283 }
284
285 pub fn subtitle(mut self, subtitle: &str) -> Self {
287 self.subtitle = Some(subtitle.to_string());
288 self
289 }
290
291 pub fn caption(mut self, caption: &str) -> Self {
293 self.caption = Some(caption.to_string());
294 self
295 }
296
297 pub fn tag_levels(mut self, levels: TagLevels) -> Self {
299 self.tags = Some(levels);
300 self
301 }
302
303 pub fn tag_affixes(mut self, prefix: &str, suffix: &str) -> Self {
305 self.tag_prefix = prefix.to_string();
306 self.tag_suffix = suffix.to_string();
307 self
308 }
309
310 pub fn collect_legends(mut self, collect: bool) -> Self {
313 self.collect_legends = collect;
314 self
315 }
316
317 pub fn legend_position(mut self, pos: GridLegendPosition) -> Self {
319 self.legend_position = pos;
320 self
321 }
322
323 pub fn spacing(mut self, px: f64) -> Self {
325 self.spacing = if px.is_finite() { px.max(0.0) } else { 0.0 };
326 self
327 }
328
329 pub fn theme(mut self, theme: Theme) -> Self {
332 self.theme = theme;
333 self
334 }
335
336 pub fn svg_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
343 self.root_attrs.push((name.into(), value.into()));
344 self
345 }
346
347 pub fn svg_attrs(mut self, attrs: impl IntoIterator<Item = (String, String)>) -> Self {
349 self.root_attrs.extend(attrs);
350 self
351 }
352
353 pub fn svg_warnings_attr(mut self, on: bool) -> Self {
357 self.svg_warnings_attr = on;
358 self
359 }
360
361 pub fn len(&self) -> usize {
363 self.cells.len()
364 }
365
366 pub fn is_empty(&self) -> bool {
368 self.cells.is_empty()
369 }
370
371 pub fn render_svg_native(self) -> Result<String, GGError> {
373 self.render_svg_native_with_size(800, 600)
374 }
375
376 pub fn render_svg_native_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
380 Ok(self.render_page(w, h)?.0.finish())
381 }
382
383 pub fn render_svg_native_with_warnings(
386 self,
387 w: u32,
388 h: u32,
389 ) -> Result<(String, Vec<String>), GGError> {
390 let (page, warnings) = self.render_page(w, h)?;
391 Ok((page.finish(), warnings))
392 }
393
394 pub fn render_svg_native_at(self, x: f64, y: f64, w: u32, h: u32) -> Result<String, GGError> {
397 Ok(self.render_page(w, h)?.0.finish_fragment(x, y))
398 }
399
400 fn render_page(self, w: u32, h: u32) -> Result<(SvgBackend, Vec<String>), GGError> {
401 let (w, h) = (w.max(1), h.max(1));
402 let mut theme = self.theme.clone();
403 theme.resolve_inheritance();
404 let collect = self.collect_legends;
405 let legend_pos = self.legend_position;
406 let root_attrs = self.root_attrs.clone();
407 let svg_warnings_attr = self.svg_warnings_attr;
408
409 let mut ctx = PrepCtx {
412 tags: self
413 .tags
414 .clone()
415 .map(|t| (t, self.tag_prefix.clone(), self.tag_suffix.clone())),
416 next_tag: 0,
417 next_index: 0,
418 collect: collect.then_some(legend_pos),
419 legends: Vec::new(),
420 page_w: w as f64,
421 page_h: h as f64,
422 legend_warnings: Vec::new(),
423 };
424 let prepared = prepare_grid(self, &mut ctx)?;
425 let legends = ctx.legends;
426 let legend_warnings = ctx.legend_warnings;
427
428 let full = Rect {
430 x: 0.0,
431 y: 0.0,
432 width: w as f64,
433 height: h as f64,
434 };
435 let mut page = SvgBackend::new(w, h, full.clone());
436 page.without_plot_attr();
437 let (rows, cols) = prepared.dims();
438 let data_grid_attr = ("data-grid".to_string(), format!("{rows} {cols}"));
439 page.set_root_attrs(vec![data_grid_attr.clone()]);
440 if let Some(fill) = theme
441 .plot_background
442 .fill
443 .filter(|_| theme.plot_background.visible)
444 {
445 page.draw_rect(
446 (0.0, 0.0),
447 (w as f64, h as f64),
448 &RectStyle {
449 fill: Some(fill),
450 stroke: None,
451 stroke_width: 0.0,
452 alpha: 1.0,
453 clip: false,
454 },
455 )
456 .map_err(GGError::Render)?;
457 }
458
459 let mut body = full;
460 if !legends.is_empty() {
461 let gap = theme.legend_spacing.max(0.0) * 2.0;
462 let pad = 8.0;
463 match legend_pos {
464 GridLegendPosition::Right => {
465 let strip_w = legends.iter().map(|l| l.w).fold(0.0, f64::max) + 2.0 * pad;
466 let strip_w = strip_w.min(body.width * 0.4);
467 body.width -= strip_w;
468 let total_h =
469 legends.iter().map(|l| l.h).sum::<f64>() + gap * (legends.len() - 1) as f64;
470 let mut y = (body.y + (body.height - total_h) / 2.0).max(body.y + pad);
471 for l in &legends {
472 place_legend(&mut page, l, body.x + body.width + pad, y);
473 y += l.h + gap;
474 }
475 }
476 GridLegendPosition::Bottom => {
477 let strip_h = legends.iter().map(|l| l.h).fold(0.0, f64::max) + 2.0 * pad;
478 let strip_h = strip_h.min(body.height * 0.4);
479 body.height -= strip_h;
480 let total_w =
481 legends.iter().map(|l| l.w).sum::<f64>() + gap * (legends.len() - 1) as f64;
482 let mut x = (body.x + (body.width - total_w) / 2.0).max(body.x + pad);
483 for l in &legends {
484 place_legend(&mut page, l, x, body.y + body.height + pad);
485 x += l.w + gap;
486 }
487 }
488 }
489 }
490
491 let mut warnings = legend_warnings;
493 render_prepared(prepared, &body, &mut page, &mut warnings)?;
494
495 let mut reserved = vec![data_grid_attr];
496 if svg_warnings_attr {
497 reserved.push(("data-warnings".to_string(), String::new()));
498 }
499 let extra = crate::render::svg_backend::validate_root_attrs(
500 &root_attrs,
501 &reserved,
502 "grid",
503 &mut warnings,
504 );
505 page.extend_root_attrs(extra);
506 if svg_warnings_attr {
507 page.extend_root_attrs(vec![(
508 "data-warnings".to_string(),
509 crate::render::svg_backend::warnings_json(&warnings),
510 )]);
511 }
512 Ok((page, warnings))
513 }
514}
515
516struct Legend {
518 body: String,
519 x0: f64,
520 y0: f64,
521 w: f64,
522 h: f64,
523}
524
525fn place_legend(page: &mut SvgBackend, l: &Legend, x: f64, y: f64) {
526 let (tx, ty) = (x - l.x0, y - l.y0);
527 if tx.is_finite() && ty.is_finite() {
528 page.push_raw(&format!(
529 "<g class=\"legend\" transform=\"translate({tx:.2} {ty:.2})\">{}</g>",
530 l.body
531 ));
532 }
533}
534
535struct PrepCtx {
536 tags: Option<(TagLevels, String, String)>,
537 next_tag: usize,
538 next_index: usize,
539 collect: Option<GridLegendPosition>,
540 legends: Vec<Legend>,
541 page_w: f64,
547 page_h: f64,
548 legend_warnings: Vec<String>,
554}
555
556struct PreparedPlot {
558 built: crate::build::BuiltPlot,
559 meta: RenderMeta,
560 panel: String,
561 svg_formatted_values: bool,
569}
570
571enum Prepared {
572 Plot(Box<PreparedPlot>),
573 Grid(Box<PreparedGrid>),
574 Spacer,
575}
576
577struct PreparedGrid {
578 cells: Vec<Prepared>,
579 ncol: usize,
580 nrow: usize,
581 byrow: bool,
582 widths: Vec<f64>,
583 heights: Vec<f64>,
584 title: Option<String>,
585 subtitle: Option<String>,
586 caption: Option<String>,
587 spacing: f64,
588 theme: Theme,
589}
590
591impl Prepared {
592 fn dims(&self) -> (usize, usize) {
593 match self {
594 Prepared::Grid(g) => (g.nrow, g.ncol),
595 _ => (1, 1),
596 }
597 }
598}
599
600fn prepare_grid(grid: PlotGrid, ctx: &mut PrepCtx) -> Result<Prepared, GGError> {
601 let n = grid.cells.len();
602 let (ncol, nrow) = match (grid.ncol, grid.nrow, grid.op) {
603 (Some(c), Some(r), _) => {
604 (c, r.max(n.div_ceil(c)))
606 }
607 (Some(c), None, _) => (c, n.div_ceil(c).max(1)),
608 (None, Some(r), _) => (n.div_ceil(r).max(1), r),
609 (None, None, Some(Op::Row)) => (n.max(1), 1),
610 (None, None, Some(Op::Col)) => (1, n.max(1)),
611 (None, None, None) => {
612 let c = ((n as f64).sqrt().ceil() as usize).max(1);
613 (c, n.div_ceil(c).max(1))
614 }
615 };
616 let mut theme = grid.theme;
617 theme.resolve_inheritance();
618 let mut cells = Vec::with_capacity(n);
619 for cell in grid.cells {
620 cells.push(match cell {
621 GridCell::Spacer => Prepared::Spacer,
622 GridCell::Grid(g) => prepare_grid(*g, ctx)?,
623 GridCell::Plot(p) => Prepared::Plot(Box::new(prepare_plot(*p, ctx)?)),
624 });
625 }
626 Ok(Prepared::Grid(Box::new(PreparedGrid {
627 cells,
628 ncol,
629 nrow,
630 byrow: grid.byrow,
631 widths: grid.widths,
632 heights: grid.heights,
633 title: grid.title,
634 subtitle: grid.subtitle,
635 caption: grid.caption,
636 spacing: grid.spacing,
637 theme,
638 })))
639}
640
641fn prepare_plot(mut plot: GGPlot, ctx: &mut PrepCtx) -> Result<PreparedPlot, GGError> {
642 ctx.next_index += 1;
643 if let Some((levels, pre, suf)) = &ctx.tags {
644 plot.labels.tag = Some(format!("{pre}{}{suf}", levels.tag(ctx.next_tag)));
645 ctx.next_tag += 1;
646 }
647 let panel = plot
648 .labels
649 .tag
650 .clone()
651 .unwrap_or_else(|| ctx.next_index.to_string());
652
653 let original = plot.theme.legend_position.clone();
655 let collect = ctx.collect.filter(|_| {
656 matches!(
657 original,
658 LegendPosition::Right
659 | LegendPosition::Left
660 | LegendPosition::Top
661 | LegendPosition::Bottom
662 )
663 });
664 if collect.is_some() {
665 plot.theme.legend_position = LegendPosition::None;
666 }
667 let svg_formatted_values = plot.svg_formatted_values;
668 let (built, mut meta) = plot.build_for_render()?;
669 if let Some(pos) = collect {
670 meta.has_legend = false;
671 if let Some(l) = capture_legend(
672 &built,
673 pos,
674 ctx.page_w,
675 ctx.page_h,
676 &mut ctx.legend_warnings,
677 )
678 .map_err(GGError::Render)?
679 {
680 if !ctx.legends.iter().any(|o| o.body == l.body) {
681 ctx.legends.push(l);
682 }
683 }
684 }
685 Ok(PreparedPlot {
686 built,
687 meta,
688 panel,
689 svg_formatted_values,
690 })
691}
692
693fn capture_legend(
701 built: &crate::build::BuiltPlot,
702 pos: GridLegendPosition,
703 page_w: f64,
704 page_h: f64,
705 warnings: &mut Vec<String>,
713) -> Result<Option<Legend>, RenderError> {
714 let mut theme = built.theme.clone();
715 let m = theme.legend_margin.clone();
716 let (pa, lp, is_horizontal) = match pos {
718 GridLegendPosition::Right => (
719 Rect {
720 x: -m.left,
721 y: -m.top,
722 width: 0.0,
723 height: 0.0,
724 },
725 LegendPosition::Right,
726 false,
727 ),
728 GridLegendPosition::Bottom => (
729 Rect {
730 x: -m.left,
731 y: -(m.top + 30.0),
732 width: 0.0,
733 height: 0.0,
734 },
735 LegendPosition::Bottom,
736 true,
737 ),
738 };
739 theme.legend_position = lp;
740 let mut svg = SvgBackend::new(1, 1, pa.clone());
741 let mut meas = Measure::new(&mut svg, pa.clone());
742 let avail_width = (page_w + m.left).max(0.0);
743 let avail_height = (page_h + m.top).max(0.0);
744 let legend_plans = legend::plan_all_legends(
745 &built.scales,
746 &built.guide_legend,
747 &theme,
748 &built.suppressed_aes,
749 avail_width,
750 avail_height,
751 is_horizontal,
752 warnings,
753 );
754 legend::draw_legend_planned(
755 &built.scales,
756 &theme,
757 &pa,
758 avail_width,
765 &mut meas,
766 &built.guide_legend,
767 &built.suppressed_aes,
768 &legend_plans,
769 )?;
770 let ext = meas.extent();
771 if svg.body().is_empty() {
772 return Ok(None);
773 }
774 let (x0, y0, x1, y1) = match ext {
775 Some(e) => e,
776 None => return Ok(None),
777 };
778 Ok(Some(Legend {
779 body: svg.body().to_string(),
780 x0,
781 y0,
782 w: (x1 - x0).max(0.0),
783 h: (y1 - y0).max(0.0),
784 }))
785}
786
787fn tracks(weights: &[f64], n: usize, total: f64, gap: f64) -> Vec<(f64, f64)> {
789 let ws: Vec<f64> = (0..n)
790 .map(|i| {
791 if weights.is_empty() {
792 1.0
793 } else {
794 let w = weights[i % weights.len()];
795 if w.is_finite() && w > 0.0 {
796 w
797 } else {
798 1.0
799 }
800 }
801 })
802 .collect();
803 let sum: f64 = ws.iter().sum();
804 let avail = (total - gap * n.saturating_sub(1) as f64).max(0.0);
805 let mut pos = 0.0;
806 ws.iter()
807 .map(|w| {
808 let size = avail * w / sum;
809 let t = (pos, size);
810 pos += size + gap;
811 t
812 })
813 .collect()
814}
815
816fn text_style(el: &crate::theme::elements::ElementText, anchor: TextAnchor) -> TextStyle {
817 TextStyle {
818 color: el.color,
819 size: el.size,
820 anchor,
821 angle: 0.0,
822 family: (!el.family.is_empty()).then(|| el.family.clone()),
823 face: el.face,
824 }
825}
826
827fn hjust_x(el: &crate::theme::elements::ElementText, x0: f64, x1: f64) -> (f64, TextAnchor) {
828 let hj = if el.hjust.is_finite() {
829 el.hjust.clamp(0.0, 1.0)
830 } else {
831 0.0
832 };
833 let anchor = if hj <= 0.02 {
834 TextAnchor::Start
835 } else if hj >= 0.98 {
836 TextAnchor::End
837 } else {
838 TextAnchor::Middle
839 };
840 (x0 + hj * (x1 - x0), anchor)
841}
842
843fn render_prepared(
844 prepared: Prepared,
845 area: &Rect,
846 page: &mut SvgBackend,
847 warnings: &mut Vec<String>,
848) -> Result<(), GGError> {
849 match prepared {
850 Prepared::Spacer => Ok(()),
851 Prepared::Plot(p) => {
852 let PreparedPlot {
853 mut built,
854 meta,
855 panel,
856 svg_formatted_values,
857 } = *p;
858 let (cw, ch) = (
859 area.width.floor().max(1.0) as u32,
860 area.height.floor().max(1.0) as u32,
861 );
862 let layout = GGPlot::layout_built(&mut built, &meta, cw, ch);
863 let mut backend = SvgBackend::new(cw, ch, layout.plot_area.clone());
864 backend.set_formatted_values(svg_formatted_values);
865 let mut attrs = root_data_attrs(&built);
866 attrs.push(("data-panel".into(), panel.clone()));
867 backend.set_root_attrs(attrs);
868 PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
869 warnings.extend(
870 built
871 .warnings
872 .iter()
873 .cloned()
874 .chain(backend.take_warnings())
875 .map(|w| format!("panel {panel}: {w}")),
876 );
877 page.push_raw(&backend.finish_fragment(area.x, area.y));
878 Ok(())
879 }
880 Prepared::Grid(g) => {
881 let g = *g;
882 let theme = &g.theme;
883 let m = &theme.plot_margin;
884 let mut top = area.y;
886 let (x0, x1) = (area.x + m.left, area.x + area.width - m.right);
887 if let Some(t) = &g.title {
888 top += m.top;
889 let (tx, anchor) = hjust_x(&theme.title, x0, x1);
890 page.draw_text(
891 t,
892 (tx, top + theme.title.size * 0.8),
893 &text_style(&theme.title, anchor),
894 )
895 .map_err(GGError::Render)?;
896 top += theme.title.size * 1.6;
897 }
898 if let Some(s) = &g.subtitle {
899 if g.title.is_none() {
900 top += m.top;
901 }
902 let (sx, anchor) = hjust_x(&theme.subtitle, x0, x1);
903 page.draw_text(
904 s,
905 (sx, top + theme.subtitle.size * 0.7),
906 &text_style(&theme.subtitle, anchor),
907 )
908 .map_err(GGError::Render)?;
909 top += theme.subtitle.size * 1.5;
910 }
911 let mut bottom = area.y + area.height;
912 if let Some(c) = &g.caption {
913 let (cx, anchor) = hjust_x(&theme.caption, x0, x1);
914 bottom -= theme.caption.size * 1.8;
915 page.draw_text(
916 c,
917 (cx, bottom + theme.caption.size * 0.9),
918 &text_style(&theme.caption, anchor),
919 )
920 .map_err(GGError::Render)?;
921 }
922 let body_h = (bottom - top).max(1.0);
923 let cols = tracks(&g.widths, g.ncol, area.width, g.spacing);
924 let rows = tracks(&g.heights, g.nrow, body_h, g.spacing);
925 for (k, cell) in g.cells.into_iter().enumerate() {
926 let (r, c) = if g.byrow {
927 (k / g.ncol, k % g.ncol)
928 } else {
929 (k % g.nrow, k / g.nrow)
930 };
931 if r >= g.nrow || c >= g.ncol {
932 warnings.push(format!(
934 "plot_grid: cell {} does not fit a {}×{} grid; skipped",
935 k + 1,
936 g.nrow,
937 g.ncol
938 ));
939 continue;
940 }
941 let rect = Rect {
942 x: area.x + cols[c].0,
943 y: top + rows[r].0,
944 width: cols[c].1,
945 height: rows[r].1,
946 };
947 render_prepared(cell, &rect, page, warnings)?;
948 }
949 Ok(())
950 }
951 }
952}
953
954struct Measure<'a> {
956 inner: &'a mut SvgBackend,
957 pa: Rect,
958 bbox: Option<(f64, f64, f64, f64)>,
959}
960
961impl<'a> Measure<'a> {
962 fn new(inner: &'a mut SvgBackend, pa: Rect) -> Self {
963 Measure {
964 inner,
965 pa,
966 bbox: None,
967 }
968 }
969
970 fn grow(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) {
971 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
972 return;
973 }
974 self.bbox = Some(match self.bbox {
975 None => (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)),
976 Some((a, b, c, d)) => (
977 a.min(x0.min(x1)),
978 b.min(y0.min(y1)),
979 c.max(x0.max(x1)),
980 d.max(y0.max(y1)),
981 ),
982 });
983 }
984
985 fn extent(&self) -> Option<(f64, f64, f64, f64)> {
986 self.bbox
987 }
988}
989
990impl DrawBackend for Measure<'_> {
991 fn draw_circle(
992 &mut self,
993 c: (f64, f64),
994 r: f64,
995 style: &PointStyle,
996 ) -> Result<(), RenderError> {
997 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
998 self.inner.draw_circle(c, r, style)
999 }
1000 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1001 for p in pts {
1002 self.grow(p.0, p.1, p.0, p.1);
1003 }
1004 self.inner.draw_line(pts, style)
1005 }
1006 fn draw_quadratic(
1007 &mut self,
1008 p0: (f64, f64),
1009 ctrl: (f64, f64),
1010 p1: (f64, f64),
1011 style: &LineStyle,
1012 ) -> Result<(), RenderError> {
1013 self.grow(p0.0, p0.1, p0.0, p0.1);
1014 self.grow(ctrl.0, ctrl.1, ctrl.0, ctrl.1);
1015 self.grow(p1.0, p1.1, p1.0, p1.1);
1016 self.inner.draw_quadratic(p0, ctrl, p1, style)
1017 }
1018 fn draw_rect(
1019 &mut self,
1020 a: (f64, f64),
1021 b: (f64, f64),
1022 style: &RectStyle,
1023 ) -> Result<(), RenderError> {
1024 self.grow(a.0, a.1, b.0, b.1);
1025 self.inner.draw_rect(a, b, style)
1026 }
1027 fn draw_text(
1028 &mut self,
1029 text: &str,
1030 pos: (f64, f64),
1031 style: &TextStyle,
1032 ) -> Result<(), RenderError> {
1033 self.draw_text_with_title(text, pos, style, None)
1034 }
1035 fn draw_text_with_title(
1036 &mut self,
1037 text: &str,
1038 pos: (f64, f64),
1039 style: &TextStyle,
1040 title: Option<&str>,
1041 ) -> Result<(), RenderError> {
1042 let w = text.chars().count() as f64 * style.size * 0.6;
1050 let (x0, x1) = match style.anchor {
1051 TextAnchor::Start => (pos.0, pos.0 + w),
1052 TextAnchor::Middle => (pos.0 - w / 2.0, pos.0 + w / 2.0),
1053 TextAnchor::End => (pos.0 - w, pos.0),
1054 };
1055 self.grow(x0, pos.1 - style.size / 2.0, x1, pos.1 + style.size / 2.0);
1056 self.inner.draw_text_with_title(text, pos, style, title)
1057 }
1058 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1059 for p in pts {
1060 self.grow(p.0, p.1, p.0, p.1);
1061 }
1062 self.inner.draw_polygon(pts, style)
1063 }
1064 fn draw_shape(&mut self, c: (f64, f64), r: f64, style: &PointStyle) -> Result<(), RenderError> {
1065 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
1066 self.inner.draw_shape(c, r, style)
1067 }
1068 fn plot_area(&self) -> Rect {
1069 self.pa.clone()
1070 }
1071 fn total_area(&self) -> Rect {
1072 self.inner.total_area()
1073 }
1074 fn set_tooltip(&mut self, t: Option<String>) {
1075 self.inner.set_tooltip(t)
1076 }
1077 fn set_mark_axis(&mut self, k: Option<String>) {
1078 self.inner.set_mark_axis(k)
1079 }
1080 fn set_mark_series(&mut self, s: Option<String>) {
1081 self.inner.set_mark_series(s)
1082 }
1083 fn set_mark_value(&mut self, v: Option<String>) {
1084 self.inner.set_mark_value(v)
1085 }
1086 fn warn(&mut self, m: String) {
1087 self.inner.warn(m)
1088 }
1089 fn set_mark_geom(&mut self, geom: Option<&str>) {
1090 self.inner.set_mark_geom(geom)
1091 }
1092 fn set_mark_part(&mut self, part: Option<&str>) {
1093 self.inner.set_mark_part(part)
1094 }
1095 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1096 self.inner.set_mark_attrs(attrs)
1097 }
1098 fn wants_formatted_values(&self) -> bool {
1099 self.inner.wants_formatted_values()
1100 }
1101 fn set_mark_fvalue(&mut self, value: Option<String>) {
1102 self.inner.set_mark_fvalue(value)
1103 }
1104 fn set_mark_fx(&mut self, value: Option<String>) {
1105 self.inner.set_mark_fx(value)
1106 }
1107}
1108
1109fn join(op: Op, a: GridCell, b: GridCell) -> PlotGrid {
1112 let flatten = |c: GridCell| -> Vec<GridCell> {
1113 match c {
1114 GridCell::Grid(g) if g.op == Some(op) && g.is_plain() => g.cells,
1115 other => vec![other],
1116 }
1117 };
1118 let mut cells = flatten(a);
1119 cells.extend(flatten(b));
1120 PlotGrid {
1121 cells,
1122 op: Some(op),
1123 ..PlotGrid::default()
1124 }
1125}
1126
1127impl PlotGrid {
1128 fn is_plain(&self) -> bool {
1131 self.ncol.is_none()
1132 && self.nrow.is_none()
1133 && self.widths.is_empty()
1134 && self.heights.is_empty()
1135 && self.title.is_none()
1136 && self.subtitle.is_none()
1137 && self.caption.is_none()
1138 && self.tags.is_none()
1139 && !self.collect_legends
1140 }
1141}
1142
1143macro_rules! grid_ops {
1144 ($lhs:ty, $rhs:ty) => {
1145 impl BitOr<$rhs> for $lhs {
1147 type Output = PlotGrid;
1148 fn bitor(self, rhs: $rhs) -> PlotGrid {
1149 join(Op::Row, self.into(), rhs.into())
1150 }
1151 }
1152 impl Div<$rhs> for $lhs {
1154 type Output = PlotGrid;
1155 fn div(self, rhs: $rhs) -> PlotGrid {
1156 join(Op::Col, self.into(), rhs.into())
1157 }
1158 }
1159 };
1160}
1161
1162grid_ops!(GGPlot, GGPlot);
1163grid_ops!(GGPlot, PlotGrid);
1164grid_ops!(PlotGrid, GGPlot);
1165grid_ops!(PlotGrid, PlotGrid);
1166
1167#[cfg(test)]
1168mod tests {
1169 use super::*;
1170
1171 #[test]
1172 fn tag_sequences() {
1173 let l: Vec<String> = (0..28).map(|i| TagLevels::Lower.tag(i)).collect();
1174 assert_eq!(&l[..3], &["a", "b", "c"]);
1175 assert_eq!(l[25], "z");
1176 assert_eq!(l[26], "aa");
1177 assert_eq!(l[27], "ab");
1178 assert_eq!(TagLevels::Upper.tag(1), "B");
1179 assert_eq!(TagLevels::Numeric.tag(9), "10");
1180 assert_eq!(TagLevels::LowerRoman.tag(3), "iv");
1181 assert_eq!(TagLevels::UpperRoman.tag(13), "XIV");
1182 assert_eq!(TagLevels::Custom(vec!["x".into()]).tag(1), "2");
1183 }
1184
1185 #[test]
1186 fn tracks_are_relative_and_robust() {
1187 let t = tracks(&[2.0, 1.0], 2, 300.0, 0.0);
1188 assert_eq!(t, vec![(0.0, 200.0), (200.0, 100.0)]);
1189 let t = tracks(&[f64::NAN, -1.0], 2, 100.0, 10.0);
1190 assert_eq!(t, vec![(0.0, 45.0), (55.0, 45.0)]);
1191 let t = tracks(&[], 3, 30.0, 100.0);
1192 assert!(t.iter().all(|(p, s)| p.is_finite() && *s == 0.0));
1193 }
1194
1195 #[test]
1199 fn measure_forwards_draw_quadratic_and_mark_metadata_and_grows_its_extent() {
1200 let pa = Rect {
1201 x: 0.0,
1202 y: 0.0,
1203 width: 100.0,
1204 height: 100.0,
1205 };
1206 let mut backend = SvgBackend::new(100, 100, pa.clone());
1207 backend.set_formatted_values(true);
1208 let mut meas = Measure::new(&mut backend, pa);
1209
1210 assert!(meas.extent().is_none());
1211 assert!(meas.wants_formatted_values());
1213 meas.set_mark_geom(Some("line"));
1214 meas.set_mark_part(Some("arrow"));
1215 meas.set_mark_attrs(vec![("data-x".to_string(), "v".to_string())]);
1216 meas.set_mark_fvalue(Some("1,234.5".to_string()));
1217 meas.set_mark_fx(Some("2026-10-12".to_string()));
1218 meas.draw_quadratic(
1219 (10.0, 20.0),
1220 (90.0, 5.0),
1221 (50.0, 80.0),
1222 &LineStyle::default(),
1223 )
1224 .unwrap();
1225 meas.set_mark_geom(None);
1226 meas.set_mark_part(None);
1227 meas.set_mark_attrs(Vec::new());
1228 meas.set_mark_fvalue(None);
1229 meas.set_mark_fx(None);
1230
1231 let (x0, y0, x1, y1) = meas.extent().expect("extent after drawing");
1232 assert_eq!((x0, y0, x1, y1), (10.0, 5.0, 90.0, 80.0));
1233
1234 let svg = backend.finish();
1238 assert!(svg.contains("data-part=\"arrow\""));
1239 assert!(svg.contains("data-x=\"v\""));
1240 assert!(svg.contains("gg-arrow"));
1241 assert!(svg.contains("data-fvalue=\"1,234.5\""));
1242 assert!(svg.contains("data-fx=\"2026-10-12\""));
1243 }
1244
1245 #[test]
1256 fn measure_forwards_the_title_to_its_inner_backend() {
1257 let pa = Rect {
1258 x: 0.0,
1259 y: 0.0,
1260 width: 100.0,
1261 height: 100.0,
1262 };
1263 let mut backend = SvgBackend::new(100, 100, pa.clone());
1264 let mut meas = Measure::new(&mut backend, pa);
1265 meas.draw_text_with_title(
1266 "Grp…",
1267 (10.0, 20.0),
1268 &TextStyle {
1269 size: 12.0,
1270 anchor: TextAnchor::Start,
1271 ..Default::default()
1272 },
1273 Some("Group 01"),
1274 )
1275 .unwrap();
1276 let svg = backend.finish();
1277 assert!(
1278 svg.contains("<title>Group 01</title>"),
1279 "the full label should reach the inner backend's <title>: {svg}"
1280 );
1281 }
1282}