1use std::ops::{BitOr, Div};
27
28use crate::aes::Aesthetic;
29use crate::guide::legend;
30use crate::plot::{GGError, GGPlot, RenderMeta};
31use crate::render::backend::{
32 DrawBackend, LineStyle, PointStyle, RectStyle, TextAnchor, TextStyle,
33};
34use crate::render::renderer::PlotRenderer;
35use crate::render::svg_backend::{root_data_attrs, validate_root_attrs, SvgBackend};
36use crate::render::{Rect, RenderError};
37use crate::theme::elements::{ElementLine, ElementText};
38use crate::theme::{AxisLabelFit, LegendPosition, Theme};
39
40#[derive(Clone, Debug, PartialEq)]
42pub enum TagLevels {
43 Lower,
45 Upper,
47 Numeric,
49 LowerRoman,
51 UpperRoman,
53 Custom(Vec<String>),
55}
56
57impl TagLevels {
58 pub fn tag(&self, i: usize) -> String {
60 match self {
61 TagLevels::Lower => alpha(i, b'a'),
62 TagLevels::Upper => alpha(i, b'A'),
63 TagLevels::Numeric => (i + 1).to_string(),
64 TagLevels::LowerRoman => roman(i + 1).to_lowercase(),
65 TagLevels::UpperRoman => roman(i + 1),
66 TagLevels::Custom(v) => v.get(i).cloned().unwrap_or_else(|| (i + 1).to_string()),
67 }
68 }
69}
70
71fn alpha(mut i: usize, base: u8) -> String {
72 let mut out = Vec::new();
74 loop {
75 out.push(base + (i % 26) as u8);
76 if i < 26 {
77 break;
78 }
79 i = i / 26 - 1;
80 }
81 out.reverse();
82 String::from_utf8(out).unwrap_or_default()
83}
84
85fn roman(mut n: usize) -> String {
86 if n >= 4000 {
87 return n.to_string();
88 }
89 const T: [(usize, &str); 13] = [
90 (1000, "M"),
91 (900, "CM"),
92 (500, "D"),
93 (400, "CD"),
94 (100, "C"),
95 (90, "XC"),
96 (50, "L"),
97 (40, "XL"),
98 (10, "X"),
99 (9, "IX"),
100 (5, "V"),
101 (4, "IV"),
102 (1, "I"),
103 ];
104 let mut s = String::new();
105 for (v, r) in T {
106 while n >= v {
107 s.push_str(r);
108 n -= v;
109 }
110 }
111 s
112}
113
114#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
122pub enum AxisCollect {
123 #[default]
125 Keep,
126 Align,
141 CollectX,
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum GridLegendPosition {
153 #[default]
155 Right,
156 Bottom,
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163enum Op {
164 Row,
165 Col,
166}
167
168pub enum GridCell {
170 Plot(Box<GGPlot>),
171 Grid(Box<PlotGrid>),
172 Spacer,
174}
175
176impl From<GGPlot> for GridCell {
177 fn from(p: GGPlot) -> Self {
178 GridCell::Plot(Box::new(p))
179 }
180}
181
182impl From<PlotGrid> for GridCell {
183 fn from(g: PlotGrid) -> Self {
184 GridCell::Grid(Box::new(g))
185 }
186}
187
188pub struct PlotGrid {
207 cells: Vec<GridCell>,
208 ncol: Option<usize>,
209 nrow: Option<usize>,
210 byrow: bool,
211 widths: Vec<f64>,
212 heights: Vec<f64>,
213 title: Option<String>,
214 subtitle: Option<String>,
215 caption: Option<String>,
216 tags: Option<TagLevels>,
217 tag_prefix: String,
218 tag_suffix: String,
219 collect_legends: bool,
220 legend_position: GridLegendPosition,
221 spacing: f64,
222 theme: Theme,
223 axis_collect: AxisCollect,
224 op: Option<Op>,
225 root_attrs: Vec<(String, String)>,
228 svg_warnings_attr: bool,
233}
234
235impl Default for PlotGrid {
236 fn default() -> Self {
237 PlotGrid {
238 cells: Vec::new(),
239 ncol: None,
240 nrow: None,
241 byrow: true,
242 widths: Vec::new(),
243 heights: Vec::new(),
244 title: None,
245 subtitle: None,
246 caption: None,
247 tags: None,
248 tag_prefix: String::new(),
249 tag_suffix: String::new(),
250 collect_legends: false,
251 legend_position: GridLegendPosition::Right,
252 spacing: 0.0,
253 theme: Theme::default(),
254 axis_collect: AxisCollect::Keep,
255 op: None,
256 root_attrs: Vec::new(),
257 svg_warnings_attr: false,
258 }
259 }
260}
261
262const MAX_TRACKS: usize = 10_000;
264
265impl PlotGrid {
266 pub fn new() -> Self {
267 Self::default()
268 }
269
270 #[allow(clippy::should_implement_trait)]
272 pub fn add(mut self, cell: impl Into<GridCell>) -> Self {
273 self.cells.push(cell.into());
274 self
275 }
276
277 pub fn add_all(mut self, plots: impl IntoIterator<Item = GGPlot>) -> Self {
279 self.cells.extend(plots.into_iter().map(GridCell::from));
280 self
281 }
282
283 pub fn add_spacer(mut self) -> Self {
285 self.cells.push(GridCell::Spacer);
286 self
287 }
288
289 pub fn ncol(mut self, n: usize) -> Self {
291 self.ncol = (n > 0).then_some(n.min(MAX_TRACKS));
292 self
293 }
294
295 pub fn nrow(mut self, n: usize) -> Self {
297 self.nrow = (n > 0).then_some(n.min(MAX_TRACKS));
298 self
299 }
300
301 pub fn byrow(mut self, byrow: bool) -> Self {
303 self.byrow = byrow;
304 self
305 }
306
307 pub fn widths(mut self, widths: &[f64]) -> Self {
309 self.widths = widths.to_vec();
310 self
311 }
312
313 pub fn heights(mut self, heights: &[f64]) -> Self {
315 self.heights = heights.to_vec();
316 self
317 }
318
319 pub fn title(mut self, title: &str) -> Self {
321 self.title = Some(title.to_string());
322 self
323 }
324
325 pub fn subtitle(mut self, subtitle: &str) -> Self {
327 self.subtitle = Some(subtitle.to_string());
328 self
329 }
330
331 pub fn caption(mut self, caption: &str) -> Self {
333 self.caption = Some(caption.to_string());
334 self
335 }
336
337 pub fn tag_levels(mut self, levels: TagLevels) -> Self {
339 self.tags = Some(levels);
340 self
341 }
342
343 pub fn tag_affixes(mut self, prefix: &str, suffix: &str) -> Self {
345 self.tag_prefix = prefix.to_string();
346 self.tag_suffix = suffix.to_string();
347 self
348 }
349
350 pub fn collect_legends(mut self, collect: bool) -> Self {
353 self.collect_legends = collect;
354 self
355 }
356
357 pub fn legend_position(mut self, pos: GridLegendPosition) -> Self {
359 self.legend_position = pos;
360 self
361 }
362
363 pub fn axis_collect(mut self, mode: AxisCollect) -> Self {
367 self.axis_collect = mode;
368 self
369 }
370
371 pub fn spacing(mut self, px: f64) -> Self {
373 self.spacing = if px.is_finite() { px.max(0.0) } else { 0.0 };
374 self
375 }
376
377 pub fn theme(mut self, theme: Theme) -> Self {
380 self.theme = theme;
381 self
382 }
383
384 pub fn svg_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
391 self.root_attrs.push((name.into(), value.into()));
392 self
393 }
394
395 pub fn svg_attrs(mut self, attrs: impl IntoIterator<Item = (String, String)>) -> Self {
397 self.root_attrs.extend(attrs);
398 self
399 }
400
401 pub fn svg_warnings_attr(mut self, on: bool) -> Self {
405 self.svg_warnings_attr = on;
406 self
407 }
408
409 pub fn len(&self) -> usize {
411 self.cells.len()
412 }
413
414 pub fn is_empty(&self) -> bool {
416 self.cells.is_empty()
417 }
418
419 pub fn render_svg_native(self) -> Result<String, GGError> {
421 self.render_svg_native_with_size(800, 600)
422 }
423
424 pub fn render_svg_native_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
428 Ok(self.render_page(w, h)?.0.finish())
429 }
430
431 pub fn render_svg_native_with_warnings(
434 self,
435 w: u32,
436 h: u32,
437 ) -> Result<(String, Vec<String>), GGError> {
438 let (page, warnings) = self.render_page(w, h)?;
439 Ok((page.finish(), warnings))
440 }
441
442 pub fn render_svg_native_at(self, x: f64, y: f64, w: u32, h: u32) -> Result<String, GGError> {
445 Ok(self.render_page(w, h)?.0.finish_fragment(x, y))
446 }
447
448 fn render_page(self, w: u32, h: u32) -> Result<(SvgBackend, Vec<String>), GGError> {
449 let (w, h) = (w.max(1), h.max(1));
450 let mut theme = self.theme.clone();
451 theme.resolve_inheritance();
452 let collect = self.collect_legends;
453 let legend_pos = self.legend_position;
454 let root_attrs = self.root_attrs.clone();
455 let svg_warnings_attr = self.svg_warnings_attr;
456
457 let mut ctx = PrepCtx {
460 tags: self
461 .tags
462 .clone()
463 .map(|t| (t, self.tag_prefix.clone(), self.tag_suffix.clone())),
464 next_tag: 0,
465 next_index: 0,
466 collect: collect.then_some(legend_pos),
467 legends: Vec::new(),
468 page_w: w as f64,
469 page_h: h as f64,
470 legend_warnings: Vec::new(),
471 };
472 let prepared = prepare_grid(self, &mut ctx)?;
473 let legends = ctx.legends;
474 let legend_warnings = ctx.legend_warnings;
475
476 let full = Rect {
478 x: 0.0,
479 y: 0.0,
480 width: w as f64,
481 height: h as f64,
482 };
483 let mut page = SvgBackend::new(w, h, full.clone());
484 page.without_plot_attr();
485 let (rows, cols) = prepared.dims();
486 let data_grid_attr = ("data-grid".to_string(), format!("{rows} {cols}"));
487 page.set_root_attrs(vec![data_grid_attr.clone()]);
488 if let Some(fill) = theme
489 .plot_background
490 .fill
491 .filter(|_| theme.plot_background.visible)
492 {
493 page.draw_rect(
494 (0.0, 0.0),
495 (w as f64, h as f64),
496 &RectStyle {
497 fill: Some(fill),
498 stroke: None,
499 stroke_width: 0.0,
500 alpha: 1.0,
501 clip: false,
502 corner_radius: 0.0,
503 },
504 )
505 .map_err(GGError::Render)?;
506 }
507
508 let mut body = full;
509 if !legends.is_empty() {
510 let gap = theme.legend_spacing.max(0.0) * 2.0;
511 let pad = 8.0;
512 match legend_pos {
513 GridLegendPosition::Right => {
514 let strip_w = legends.iter().map(|l| l.w).fold(0.0, f64::max) + 2.0 * pad;
515 let strip_w = strip_w.min(body.width * 0.4);
516 body.width -= strip_w;
517 let total_h =
518 legends.iter().map(|l| l.h).sum::<f64>() + gap * (legends.len() - 1) as f64;
519 let mut y = (body.y + (body.height - total_h) / 2.0).max(body.y + pad);
520 for l in &legends {
521 place_legend(&mut page, l, body.x + body.width + pad, y);
522 y += l.h + gap;
523 }
524 }
525 GridLegendPosition::Bottom => {
526 let strip_h = legends.iter().map(|l| l.h).fold(0.0, f64::max) + 2.0 * pad;
527 let strip_h = strip_h.min(body.height * 0.4);
528 body.height -= strip_h;
529 let total_w =
530 legends.iter().map(|l| l.w).sum::<f64>() + gap * (legends.len() - 1) as f64;
531 let mut x = (body.x + (body.width - total_w) / 2.0).max(body.x + pad);
532 for l in &legends {
533 place_legend(&mut page, l, x, body.y + body.height + pad);
534 x += l.w + gap;
535 }
536 }
537 }
538 }
539
540 let mut warnings = legend_warnings;
542 render_prepared(prepared, &body, &mut page, &mut warnings)?;
543
544 let mut reserved = vec![data_grid_attr];
545 if svg_warnings_attr {
546 reserved.push(("data-warnings".to_string(), String::new()));
547 }
548 let extra = crate::render::svg_backend::validate_root_attrs(
549 &root_attrs,
550 &reserved,
551 "grid",
552 &mut warnings,
553 );
554 page.extend_root_attrs(extra);
555 if svg_warnings_attr {
556 page.extend_root_attrs(vec![(
557 "data-warnings".to_string(),
558 crate::render::svg_backend::warnings_json(&warnings),
559 )]);
560 }
561 Ok((page, warnings))
562 }
563}
564
565struct Legend {
567 body: String,
568 x0: f64,
569 y0: f64,
570 w: f64,
571 h: f64,
572}
573
574fn place_legend(page: &mut SvgBackend, l: &Legend, x: f64, y: f64) {
575 let (tx, ty) = (x - l.x0, y - l.y0);
576 if tx.is_finite() && ty.is_finite() {
577 page.push_raw(&format!(
578 "<g class=\"legend\" transform=\"translate({tx:.2} {ty:.2})\">{}</g>",
579 l.body
580 ));
581 }
582}
583
584struct PrepCtx {
585 tags: Option<(TagLevels, String, String)>,
586 next_tag: usize,
587 next_index: usize,
588 collect: Option<GridLegendPosition>,
589 legends: Vec<Legend>,
590 page_w: f64,
596 page_h: f64,
597 legend_warnings: Vec<String>,
603}
604
605struct PreparedPlot {
607 built: crate::build::BuiltPlot,
608 meta: RenderMeta,
609 panel: String,
610 svg_formatted_values: bool,
618 root_attrs: Vec<(String, String)>,
626}
627
628enum Prepared {
629 Plot(Box<PreparedPlot>),
630 Grid(Box<PreparedGrid>),
631 Spacer,
632}
633
634struct PreparedGrid {
635 cells: Vec<Prepared>,
636 ncol: usize,
637 nrow: usize,
638 byrow: bool,
639 widths: Vec<f64>,
640 heights: Vec<f64>,
641 title: Option<String>,
642 subtitle: Option<String>,
643 caption: Option<String>,
644 spacing: f64,
645 theme: Theme,
646 axis_collect: AxisCollect,
647}
648
649impl Prepared {
650 fn dims(&self) -> (usize, usize) {
651 match self {
652 Prepared::Grid(g) => (g.nrow, g.ncol),
653 _ => (1, 1),
654 }
655 }
656}
657
658fn prepare_grid(grid: PlotGrid, ctx: &mut PrepCtx) -> Result<Prepared, GGError> {
659 let n = grid.cells.len();
660 let (ncol, nrow) = match (grid.ncol, grid.nrow, grid.op) {
661 (Some(c), Some(r), _) => {
662 (c, r.max(n.div_ceil(c)))
664 }
665 (Some(c), None, _) => (c, n.div_ceil(c).max(1)),
666 (None, Some(r), _) => (n.div_ceil(r).max(1), r),
667 (None, None, Some(Op::Row)) => (n.max(1), 1),
668 (None, None, Some(Op::Col)) => (1, n.max(1)),
669 (None, None, None) => {
670 let c = ((n as f64).sqrt().ceil() as usize).max(1);
671 (c, n.div_ceil(c).max(1))
672 }
673 };
674 let mut theme = grid.theme;
675 theme.resolve_inheritance();
676 let mut cells = Vec::with_capacity(n);
677 for cell in grid.cells {
678 cells.push(match cell {
679 GridCell::Spacer => Prepared::Spacer,
680 GridCell::Grid(g) => prepare_grid(*g, ctx)?,
681 GridCell::Plot(p) => Prepared::Plot(Box::new(prepare_plot(*p, ctx)?)),
682 });
683 }
684 Ok(Prepared::Grid(Box::new(PreparedGrid {
685 cells,
686 ncol,
687 nrow,
688 byrow: grid.byrow,
689 widths: grid.widths,
690 heights: grid.heights,
691 title: grid.title,
692 subtitle: grid.subtitle,
693 caption: grid.caption,
694 spacing: grid.spacing,
695 theme,
696 axis_collect: grid.axis_collect,
697 })))
698}
699
700fn prepare_plot(mut plot: GGPlot, ctx: &mut PrepCtx) -> Result<PreparedPlot, GGError> {
701 ctx.next_index += 1;
702 if let Some((levels, pre, suf)) = &ctx.tags {
703 plot.labels.tag = Some(format!("{pre}{}{suf}", levels.tag(ctx.next_tag)));
704 ctx.next_tag += 1;
705 }
706 let panel = plot
707 .labels
708 .tag
709 .clone()
710 .unwrap_or_else(|| ctx.next_index.to_string());
711
712 let original = plot.theme.legend_position.clone();
714 let collect = ctx.collect.filter(|_| {
715 matches!(
716 original,
717 LegendPosition::Right
718 | LegendPosition::Left
719 | LegendPosition::Top
720 | LegendPosition::Bottom
721 )
722 });
723 if collect.is_some() {
724 plot.theme.legend_position = LegendPosition::None;
725 }
726 let svg_formatted_values = plot.svg_formatted_values;
727 let root_attrs = plot.root_attrs.clone();
728 let (built, mut meta) = plot.build_for_render()?;
729 if let Some(pos) = collect {
730 meta.has_legend = false;
731 if let Some(l) = capture_legend(
732 &built,
733 pos,
734 ctx.page_w,
735 ctx.page_h,
736 &mut ctx.legend_warnings,
737 )
738 .map_err(GGError::Render)?
739 {
740 if !ctx.legends.iter().any(|o| o.body == l.body) {
741 ctx.legends.push(l);
742 }
743 }
744 }
745 Ok(PreparedPlot {
746 built,
747 meta,
748 panel,
749 svg_formatted_values,
750 root_attrs,
751 })
752}
753
754fn capture_legend(
762 built: &crate::build::BuiltPlot,
763 pos: GridLegendPosition,
764 page_w: f64,
765 page_h: f64,
766 warnings: &mut Vec<String>,
774) -> Result<Option<Legend>, RenderError> {
775 let mut theme = built.theme.clone();
776 let m = theme.legend_margin.clone();
777 let (pa, lp, is_horizontal) = match pos {
779 GridLegendPosition::Right => (
780 Rect {
781 x: -m.left,
782 y: -m.top,
783 width: 0.0,
784 height: 0.0,
785 },
786 LegendPosition::Right,
787 false,
788 ),
789 GridLegendPosition::Bottom => (
790 Rect {
791 x: -m.left,
792 y: -(m.top + 30.0),
793 width: 0.0,
794 height: 0.0,
795 },
796 LegendPosition::Bottom,
797 true,
798 ),
799 };
800 theme.legend_position = lp;
801 let mut svg = SvgBackend::new(1, 1, pa.clone());
802 let mut meas = Measure::new(&mut svg, pa.clone());
803 let avail_width = (page_w + m.left).max(0.0);
804 let avail_height = (page_h + m.top).max(0.0);
805 let legend_plans = legend::plan_all_legends(
806 &built.scales,
807 &built.guide_legend,
808 &theme,
809 &built.suppressed_aes,
810 avail_width,
811 avail_height,
812 is_horizontal,
813 warnings,
814 );
815 legend::draw_legend_planned(
816 &built.scales,
817 &theme,
818 &pa,
819 avail_width,
826 &mut meas,
827 &built.guide_legend,
828 &built.suppressed_aes,
829 &legend_plans,
830 )?;
831 let ext = meas.extent();
832 if svg.body().is_empty() {
833 return Ok(None);
834 }
835 let (x0, y0, x1, y1) = match ext {
836 Some(e) => e,
837 None => return Ok(None),
838 };
839 Ok(Some(Legend {
840 body: svg.body().to_string(),
841 x0,
842 y0,
843 w: (x1 - x0).max(0.0),
844 h: (y1 - y0).max(0.0),
845 }))
846}
847
848fn tracks(weights: &[f64], n: usize, total: f64, gap: f64) -> Vec<(f64, f64)> {
850 let ws: Vec<f64> = (0..n)
851 .map(|i| {
852 if weights.is_empty() {
853 1.0
854 } else {
855 let w = weights[i % weights.len()];
856 if w.is_finite() && w > 0.0 {
857 w
858 } else {
859 1.0
860 }
861 }
862 })
863 .collect();
864 let sum: f64 = ws.iter().sum();
865 let avail = (total - gap * n.saturating_sub(1) as f64).max(0.0);
866 let mut pos = 0.0;
867 ws.iter()
868 .map(|w| {
869 let size = avail * w / sum;
870 let t = (pos, size);
871 pos += size + gap;
872 t
873 })
874 .collect()
875}
876
877fn text_style(el: &crate::theme::elements::ElementText, anchor: TextAnchor) -> TextStyle {
878 TextStyle {
879 color: el.color,
880 size: el.size,
881 anchor,
882 angle: 0.0,
883 family: (!el.family.is_empty()).then(|| el.family.clone()),
884 face: el.face,
885 }
886}
887
888fn hjust_x(el: &crate::theme::elements::ElementText, x0: f64, x1: f64) -> (f64, TextAnchor) {
889 let hj = if el.hjust.is_finite() {
890 el.hjust.clamp(0.0, 1.0)
891 } else {
892 0.0
893 };
894 let anchor = if hj <= 0.02 {
895 TextAnchor::Start
896 } else if hj >= 0.98 {
897 TextAnchor::End
898 } else {
899 TextAnchor::Middle
900 };
901 (x0 + hj * (x1 - x0), anchor)
902}
903
904fn render_prepared(
905 prepared: Prepared,
906 area: &Rect,
907 page: &mut SvgBackend,
908 warnings: &mut Vec<String>,
909) -> Result<(), GGError> {
910 match prepared {
911 Prepared::Spacer => Ok(()),
912 Prepared::Plot(p) => {
913 let PreparedPlot {
914 mut built,
915 meta,
916 panel,
917 svg_formatted_values,
918 root_attrs,
919 } = *p;
920 let (cw, ch) = (
921 area.width.floor().max(1.0) as u32,
922 area.height.floor().max(1.0) as u32,
923 );
924 let layout = GGPlot::layout_built(&mut built, &meta, cw, ch);
925 let mut backend = SvgBackend::new(cw, ch, layout.plot_area.clone());
926 backend.set_formatted_values(svg_formatted_values);
927 let mut attrs = root_data_attrs(&built);
928 attrs.push(("data-panel".into(), panel.clone()));
929 backend.set_root_attrs(attrs);
930 PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
931 let mut attr_warnings = Vec::new();
938 let group_attrs = validate_root_attrs(&root_attrs, &[], "plot", &mut attr_warnings);
939 backend.wrap_body_in_group(&group_attrs);
940 warnings.extend(
941 built
942 .warnings
943 .iter()
944 .cloned()
945 .chain(backend.take_warnings())
946 .chain(attr_warnings)
947 .map(|w| format!("panel {panel}: {w}")),
948 );
949 page.push_raw(&backend.finish_fragment(area.x, area.y));
950 Ok(())
951 }
952 Prepared::Grid(g) => {
953 let mut g = *g;
954 let theme = &g.theme;
955 let m = &theme.plot_margin;
956 let mut top = area.y;
958 let (x0, x1) = (area.x + m.left, area.x + area.width - m.right);
959 if let Some(t) = &g.title {
960 top += m.top;
961 let (tx, anchor) = hjust_x(&theme.title, x0, x1);
962 page.draw_text(
963 t,
964 (tx, top + theme.title.size * 0.8),
965 &text_style(&theme.title, anchor),
966 )
967 .map_err(GGError::Render)?;
968 top += theme.title.size * 1.6;
969 }
970 if let Some(s) = &g.subtitle {
971 if g.title.is_none() {
972 top += m.top;
973 }
974 let (sx, anchor) = hjust_x(&theme.subtitle, x0, x1);
975 page.draw_text(
976 s,
977 (sx, top + theme.subtitle.size * 0.7),
978 &text_style(&theme.subtitle, anchor),
979 )
980 .map_err(GGError::Render)?;
981 top += theme.subtitle.size * 1.5;
982 }
983 let mut bottom = area.y + area.height;
984 if let Some(c) = &g.caption {
985 let (cx, anchor) = hjust_x(&theme.caption, x0, x1);
986 bottom -= theme.caption.size * 1.8;
987 page.draw_text(
988 c,
989 (cx, bottom + theme.caption.size * 0.9),
990 &text_style(&theme.caption, anchor),
991 )
992 .map_err(GGError::Render)?;
993 }
994 let body_h = (bottom - top).max(1.0);
995 let cols = tracks(&g.widths, g.ncol, area.width, g.spacing);
996 let rows = tracks(&g.heights, g.nrow, body_h, g.spacing);
997 if g.axis_collect != AxisCollect::Keep {
998 align_axes(
999 &mut g.cells,
1000 (g.ncol, g.nrow, g.byrow),
1001 &cols,
1002 &rows,
1003 g.axis_collect,
1004 warnings,
1005 );
1006 }
1007 for (k, cell) in g.cells.into_iter().enumerate() {
1008 let (r, c) = if g.byrow {
1009 (k / g.ncol, k % g.ncol)
1010 } else {
1011 (k % g.nrow, k / g.nrow)
1012 };
1013 if r >= g.nrow || c >= g.ncol {
1014 warnings.push(format!(
1016 "plot_grid: cell {} does not fit a {}×{} grid; skipped",
1017 k + 1,
1018 g.nrow,
1019 g.ncol
1020 ));
1021 continue;
1022 }
1023 let rect = Rect {
1024 x: area.x + cols[c].0,
1025 y: top + rows[r].0,
1026 width: cols[c].1,
1027 height: rows[r].1,
1028 };
1029 render_prepared(cell, &rect, page, warnings)?;
1030 }
1031 Ok(())
1032 }
1033 }
1034}
1035
1036fn align_axes(
1042 cells: &mut [Prepared],
1043 (ncol, nrow, byrow): (usize, usize, bool),
1046 cols: &[(f64, f64)],
1047 rows: &[(f64, f64)],
1048 mode: AxisCollect,
1049 warnings: &mut Vec<String>,
1050) {
1051 let idx_of = |r: usize, c: usize| -> usize {
1052 if byrow {
1053 r * ncol + c
1054 } else {
1055 c * nrow + r
1056 }
1057 };
1058 for c in 0..ncol {
1059 let cw = cols.get(c).map_or(0.0, |t| t.1);
1060 if !cw.is_finite() || cw <= 0.0 {
1061 continue;
1062 }
1063 let plot_cells: Vec<(usize, usize)> = (0..nrow)
1067 .filter_map(|r| {
1068 let k = idx_of(r, c);
1069 (k < cells.len() && matches!(cells[k], Prepared::Plot(_))).then_some((r, k))
1070 })
1071 .collect();
1072 if plot_cells.len() < 2 {
1073 continue; }
1075 let bottom_row = plot_cells.iter().map(|&(r, _)| r).max().unwrap();
1076
1077 for &(r, k) in &plot_cells {
1082 if let Prepared::Plot(p) = &mut cells[k] {
1083 p.built.theme.axis_text_x_fit = AxisLabelFit::Shift;
1084 if mode == AxisCollect::CollectX && r != bottom_row {
1085 p.built.theme.axis_text_x = ElementText::blank();
1086 p.built.theme.axis_title_x = ElementText::blank();
1087 p.built.theme.axis_ticks_x = Some(ElementLine::blank());
1088 }
1089 }
1090 }
1091
1092 let mut natural: Vec<(f64, f64, f64, f64, f64)> = Vec::with_capacity(plot_cells.len());
1101 for &(r, k) in &plot_cells {
1102 let ch = rows.get(r).map_or(0.0, |t| t.1);
1103 match &mut cells[k] {
1104 Prepared::Plot(p) => {
1105 let layout = GGPlot::layout_built(
1106 &mut p.built,
1107 &p.meta,
1108 cw.floor().max(1.0) as u32,
1109 ch.floor().max(1.0) as u32,
1110 );
1111 natural.push((
1112 layout.plot_area.x,
1113 layout.plot_area.x + layout.plot_area.width,
1114 layout.plot_area.y,
1115 layout.plot_area.height,
1116 ch,
1117 ));
1118 }
1119 _ => unreachable!("plot_cells only holds Prepared::Plot indices"),
1120 }
1121 }
1122
1123 let target_left = natural.iter().fold(f64::MIN, |m, &(l, ..)| m.max(l));
1128 let target_right_inset = natural
1129 .iter()
1130 .fold(f64::MIN, |m, &(_, r, ..)| m.max(cw - r));
1131 let width = cw - target_right_inset - target_left;
1132 if target_left.is_finite() && target_right_inset.is_finite() {
1133 for (&(_, k), &(nl, nr, ..)) in plot_cells.iter().zip(&natural) {
1134 if let Prepared::Plot(p) = &mut cells[k] {
1135 p.built.theme.plot_margin.left += (target_left - nl).max(0.0);
1136 p.built.theme.plot_margin.right += (target_right_inset - (cw - nr)).max(0.0);
1137 }
1138 }
1139 }
1140
1141 if width.is_finite() && width > 0.0 {
1152 for (&(_, k), &(_, _, y, h, ch)) in plot_cells.iter().zip(&natural) {
1153 if let Prepared::Plot(p) = &mut cells[k] {
1154 let bottom_budget = ch - p.built.theme.plot_margin.bottom;
1155 if y + h > bottom_budget + 0.5 {
1156 let fits = (ch - y - p.built.theme.plot_margin.bottom).max(8.0);
1157 p.built.theme.aspect_ratio = Some(fits / width);
1158 p.built.theme.plot_margin.top -= (h - fits) / 2.0;
1159 }
1160 }
1161 }
1162 }
1163
1164 if mode == AxisCollect::CollectX {
1165 collect_x_domain(cells, &plot_cells, warnings);
1166 }
1167 }
1168}
1169
1170fn collect_x_domain(
1179 cells: &mut [Prepared],
1180 plot_cells: &[(usize, usize)],
1181 warnings: &mut Vec<String>,
1182) {
1183 let mut domains = Vec::with_capacity(plot_cells.len());
1184 for &(_, k) in plot_cells {
1185 let d = match &cells[k] {
1186 Prepared::Plot(p) => match p.built.scales.get(&Aesthetic::X) {
1187 Some(s) if !s.is_discrete() => s.domain(),
1188 _ => None,
1189 },
1190 _ => None,
1191 };
1192 match d {
1193 Some(d) => domains.push(d),
1194 None => {
1195 warnings.push(
1196 "plot_grid: a column's x scale is discrete or untrained; x domain not \
1197 collected"
1198 .to_string(),
1199 );
1200 return;
1201 }
1202 }
1203 }
1204 let lo = domains.iter().map(|d| d.0).fold(f64::INFINITY, f64::min);
1205 let hi = domains
1206 .iter()
1207 .map(|d| d.1)
1208 .fold(f64::NEG_INFINITY, f64::max);
1209 if lo.is_finite() && hi.is_finite() && hi >= lo {
1210 for &(_, k) in plot_cells {
1211 if let Prepared::Plot(p) = &mut cells[k] {
1212 p.built.scales.set_limits(&Aesthetic::X, lo, hi);
1213 }
1214 }
1215 }
1216}
1217
1218struct Measure<'a> {
1220 inner: &'a mut SvgBackend,
1221 pa: Rect,
1222 bbox: Option<(f64, f64, f64, f64)>,
1223}
1224
1225impl<'a> Measure<'a> {
1226 fn new(inner: &'a mut SvgBackend, pa: Rect) -> Self {
1227 Measure {
1228 inner,
1229 pa,
1230 bbox: None,
1231 }
1232 }
1233
1234 fn grow(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) {
1235 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
1236 return;
1237 }
1238 self.bbox = Some(match self.bbox {
1239 None => (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)),
1240 Some((a, b, c, d)) => (
1241 a.min(x0.min(x1)),
1242 b.min(y0.min(y1)),
1243 c.max(x0.max(x1)),
1244 d.max(y0.max(y1)),
1245 ),
1246 });
1247 }
1248
1249 fn extent(&self) -> Option<(f64, f64, f64, f64)> {
1250 self.bbox
1251 }
1252}
1253
1254impl DrawBackend for Measure<'_> {
1255 fn draw_circle(
1256 &mut self,
1257 c: (f64, f64),
1258 r: f64,
1259 style: &PointStyle,
1260 ) -> Result<(), RenderError> {
1261 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
1262 self.inner.draw_circle(c, r, style)
1263 }
1264 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1265 for p in pts {
1266 self.grow(p.0, p.1, p.0, p.1);
1267 }
1268 self.inner.draw_line(pts, style)
1269 }
1270 fn draw_quadratic(
1271 &mut self,
1272 p0: (f64, f64),
1273 ctrl: (f64, f64),
1274 p1: (f64, f64),
1275 style: &LineStyle,
1276 ) -> Result<(), RenderError> {
1277 self.grow(p0.0, p0.1, p0.0, p0.1);
1278 self.grow(ctrl.0, ctrl.1, ctrl.0, ctrl.1);
1279 self.grow(p1.0, p1.1, p1.0, p1.1);
1280 self.inner.draw_quadratic(p0, ctrl, p1, style)
1281 }
1282 fn draw_rect(
1283 &mut self,
1284 a: (f64, f64),
1285 b: (f64, f64),
1286 style: &RectStyle,
1287 ) -> Result<(), RenderError> {
1288 self.grow(a.0, a.1, b.0, b.1);
1289 self.inner.draw_rect(a, b, style)
1290 }
1291 fn draw_text(
1292 &mut self,
1293 text: &str,
1294 pos: (f64, f64),
1295 style: &TextStyle,
1296 ) -> Result<(), RenderError> {
1297 self.draw_text_with_title(text, pos, style, None)
1298 }
1299 fn draw_text_with_title(
1300 &mut self,
1301 text: &str,
1302 pos: (f64, f64),
1303 style: &TextStyle,
1304 title: Option<&str>,
1305 ) -> Result<(), RenderError> {
1306 let w = text.chars().count() as f64 * style.size * 0.6;
1314 let (x0, x1) = match style.anchor {
1315 TextAnchor::Start => (pos.0, pos.0 + w),
1316 TextAnchor::Middle => (pos.0 - w / 2.0, pos.0 + w / 2.0),
1317 TextAnchor::End => (pos.0 - w, pos.0),
1318 };
1319 self.grow(x0, pos.1 - style.size / 2.0, x1, pos.1 + style.size / 2.0);
1320 self.inner.draw_text_with_title(text, pos, style, title)
1321 }
1322 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1323 for p in pts {
1324 self.grow(p.0, p.1, p.0, p.1);
1325 }
1326 self.inner.draw_polygon(pts, style)
1327 }
1328 fn draw_shape(&mut self, c: (f64, f64), r: f64, style: &PointStyle) -> Result<(), RenderError> {
1329 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
1330 self.inner.draw_shape(c, r, style)
1331 }
1332 fn plot_area(&self) -> Rect {
1333 self.pa.clone()
1334 }
1335 fn total_area(&self) -> Rect {
1336 self.inner.total_area()
1337 }
1338 fn set_tooltip(&mut self, t: Option<String>) {
1339 self.inner.set_tooltip(t)
1340 }
1341 fn set_mark_axis(&mut self, k: Option<String>) {
1342 self.inner.set_mark_axis(k)
1343 }
1344 fn set_mark_series(&mut self, s: Option<String>) {
1345 self.inner.set_mark_series(s)
1346 }
1347 fn set_mark_value(&mut self, v: Option<String>) {
1348 self.inner.set_mark_value(v)
1349 }
1350 fn warn(&mut self, m: String) {
1351 self.inner.warn(m)
1352 }
1353 fn set_mark_geom(&mut self, geom: Option<&str>) {
1354 self.inner.set_mark_geom(geom)
1355 }
1356 fn set_mark_part(&mut self, part: Option<&str>) {
1357 self.inner.set_mark_part(part)
1358 }
1359 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1360 self.inner.set_mark_attrs(attrs)
1361 }
1362 fn wants_formatted_values(&self) -> bool {
1363 self.inner.wants_formatted_values()
1364 }
1365 fn set_mark_fvalue(&mut self, value: Option<String>) {
1366 self.inner.set_mark_fvalue(value)
1367 }
1368 fn set_mark_fx(&mut self, value: Option<String>) {
1369 self.inner.set_mark_fx(value)
1370 }
1371}
1372
1373fn join(op: Op, a: GridCell, b: GridCell) -> PlotGrid {
1376 let flatten = |c: GridCell| -> Vec<GridCell> {
1377 match c {
1378 GridCell::Grid(g) if g.op == Some(op) && g.is_plain() => g.cells,
1379 other => vec![other],
1380 }
1381 };
1382 let mut cells = flatten(a);
1383 cells.extend(flatten(b));
1384 PlotGrid {
1385 cells,
1386 op: Some(op),
1387 ..PlotGrid::default()
1388 }
1389}
1390
1391impl PlotGrid {
1392 fn is_plain(&self) -> bool {
1395 self.ncol.is_none()
1396 && self.nrow.is_none()
1397 && self.widths.is_empty()
1398 && self.heights.is_empty()
1399 && self.title.is_none()
1400 && self.subtitle.is_none()
1401 && self.caption.is_none()
1402 && self.tags.is_none()
1403 && !self.collect_legends
1404 && self.axis_collect == AxisCollect::Keep
1405 }
1406}
1407
1408macro_rules! grid_ops {
1409 ($lhs:ty, $rhs:ty) => {
1410 impl BitOr<$rhs> for $lhs {
1412 type Output = PlotGrid;
1413 fn bitor(self, rhs: $rhs) -> PlotGrid {
1414 join(Op::Row, self.into(), rhs.into())
1415 }
1416 }
1417 impl Div<$rhs> for $lhs {
1419 type Output = PlotGrid;
1420 fn div(self, rhs: $rhs) -> PlotGrid {
1421 join(Op::Col, self.into(), rhs.into())
1422 }
1423 }
1424 };
1425}
1426
1427grid_ops!(GGPlot, GGPlot);
1428grid_ops!(GGPlot, PlotGrid);
1429grid_ops!(PlotGrid, GGPlot);
1430grid_ops!(PlotGrid, PlotGrid);
1431
1432#[cfg(test)]
1433mod tests {
1434 use super::*;
1435
1436 #[test]
1437 fn tag_sequences() {
1438 let l: Vec<String> = (0..28).map(|i| TagLevels::Lower.tag(i)).collect();
1439 assert_eq!(&l[..3], &["a", "b", "c"]);
1440 assert_eq!(l[25], "z");
1441 assert_eq!(l[26], "aa");
1442 assert_eq!(l[27], "ab");
1443 assert_eq!(TagLevels::Upper.tag(1), "B");
1444 assert_eq!(TagLevels::Numeric.tag(9), "10");
1445 assert_eq!(TagLevels::LowerRoman.tag(3), "iv");
1446 assert_eq!(TagLevels::UpperRoman.tag(13), "XIV");
1447 assert_eq!(TagLevels::Custom(vec!["x".into()]).tag(1), "2");
1448 }
1449
1450 #[test]
1451 fn tracks_are_relative_and_robust() {
1452 let t = tracks(&[2.0, 1.0], 2, 300.0, 0.0);
1453 assert_eq!(t, vec![(0.0, 200.0), (200.0, 100.0)]);
1454 let t = tracks(&[f64::NAN, -1.0], 2, 100.0, 10.0);
1455 assert_eq!(t, vec![(0.0, 45.0), (55.0, 45.0)]);
1456 let t = tracks(&[], 3, 30.0, 100.0);
1457 assert!(t.iter().all(|(p, s)| p.is_finite() && *s == 0.0));
1458 }
1459
1460 #[test]
1464 fn measure_forwards_draw_quadratic_and_mark_metadata_and_grows_its_extent() {
1465 let pa = Rect {
1466 x: 0.0,
1467 y: 0.0,
1468 width: 100.0,
1469 height: 100.0,
1470 };
1471 let mut backend = SvgBackend::new(100, 100, pa.clone());
1472 backend.set_formatted_values(true);
1473 let mut meas = Measure::new(&mut backend, pa);
1474
1475 assert!(meas.extent().is_none());
1476 assert!(meas.wants_formatted_values());
1478 meas.set_mark_geom(Some("line"));
1479 meas.set_mark_part(Some("arrow"));
1480 meas.set_mark_attrs(vec![("data-x".to_string(), "v".to_string())]);
1481 meas.set_mark_fvalue(Some("1,234.5".to_string()));
1482 meas.set_mark_fx(Some("2026-10-12".to_string()));
1483 meas.draw_quadratic(
1484 (10.0, 20.0),
1485 (90.0, 5.0),
1486 (50.0, 80.0),
1487 &LineStyle::default(),
1488 )
1489 .unwrap();
1490 meas.set_mark_geom(None);
1491 meas.set_mark_part(None);
1492 meas.set_mark_attrs(Vec::new());
1493 meas.set_mark_fvalue(None);
1494 meas.set_mark_fx(None);
1495
1496 let (x0, y0, x1, y1) = meas.extent().expect("extent after drawing");
1497 assert_eq!((x0, y0, x1, y1), (10.0, 5.0, 90.0, 80.0));
1498
1499 let svg = backend.finish();
1503 assert!(svg.contains("data-part=\"arrow\""));
1504 assert!(svg.contains("data-x=\"v\""));
1505 assert!(svg.contains("gg-arrow"));
1506 assert!(svg.contains("data-fvalue=\"1,234.5\""));
1507 assert!(svg.contains("data-fx=\"2026-10-12\""));
1508 }
1509
1510 #[test]
1521 fn measure_forwards_the_title_to_its_inner_backend() {
1522 let pa = Rect {
1523 x: 0.0,
1524 y: 0.0,
1525 width: 100.0,
1526 height: 100.0,
1527 };
1528 let mut backend = SvgBackend::new(100, 100, pa.clone());
1529 let mut meas = Measure::new(&mut backend, pa);
1530 meas.draw_text_with_title(
1531 "Grp…",
1532 (10.0, 20.0),
1533 &TextStyle {
1534 size: 12.0,
1535 anchor: TextAnchor::Start,
1536 ..Default::default()
1537 },
1538 Some("Group 01"),
1539 )
1540 .unwrap();
1541 let svg = backend.finish();
1542 assert!(
1543 svg.contains("<title>Group 01</title>"),
1544 "the full label should reach the inner backend's <title>: {svg}"
1545 );
1546 }
1547}