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 legend_bottom_share: Option<f64>,
223 spacing: f64,
224 theme: Theme,
225 axis_collect: AxisCollect,
226 op: Option<Op>,
227 root_attrs: Vec<(String, String)>,
230 svg_warnings_attr: bool,
235}
236
237impl Default for PlotGrid {
238 fn default() -> Self {
239 PlotGrid {
240 cells: Vec::new(),
241 ncol: None,
242 nrow: None,
243 byrow: true,
244 widths: Vec::new(),
245 heights: Vec::new(),
246 title: None,
247 subtitle: None,
248 caption: None,
249 tags: None,
250 tag_prefix: String::new(),
251 tag_suffix: String::new(),
252 collect_legends: false,
253 legend_position: GridLegendPosition::Right,
254 legend_bottom_share: None,
255 spacing: 0.0,
256 theme: Theme::default(),
257 axis_collect: AxisCollect::Keep,
258 op: None,
259 root_attrs: Vec::new(),
260 svg_warnings_attr: false,
261 }
262 }
263}
264
265const MAX_TRACKS: usize = 10_000;
267
268impl PlotGrid {
269 pub fn new() -> Self {
270 Self::default()
271 }
272
273 #[allow(clippy::should_implement_trait)]
275 pub fn add(mut self, cell: impl Into<GridCell>) -> Self {
276 self.cells.push(cell.into());
277 self
278 }
279
280 pub fn add_all(mut self, plots: impl IntoIterator<Item = GGPlot>) -> Self {
282 self.cells.extend(plots.into_iter().map(GridCell::from));
283 self
284 }
285
286 pub fn add_spacer(mut self) -> Self {
288 self.cells.push(GridCell::Spacer);
289 self
290 }
291
292 pub fn ncol(mut self, n: usize) -> Self {
294 self.ncol = (n > 0).then_some(n.min(MAX_TRACKS));
295 self
296 }
297
298 pub fn nrow(mut self, n: usize) -> Self {
300 self.nrow = (n > 0).then_some(n.min(MAX_TRACKS));
301 self
302 }
303
304 pub fn byrow(mut self, byrow: bool) -> Self {
306 self.byrow = byrow;
307 self
308 }
309
310 pub fn widths(mut self, widths: &[f64]) -> Self {
312 self.widths = widths.to_vec();
313 self
314 }
315
316 pub fn heights(mut self, heights: &[f64]) -> Self {
318 self.heights = heights.to_vec();
319 self
320 }
321
322 pub fn title(mut self, title: &str) -> Self {
324 self.title = Some(title.to_string());
325 self
326 }
327
328 pub fn subtitle(mut self, subtitle: &str) -> Self {
330 self.subtitle = Some(subtitle.to_string());
331 self
332 }
333
334 pub fn caption(mut self, caption: &str) -> Self {
336 self.caption = Some(caption.to_string());
337 self
338 }
339
340 pub fn tag_levels(mut self, levels: TagLevels) -> Self {
342 self.tags = Some(levels);
343 self
344 }
345
346 pub fn tag_affixes(mut self, prefix: &str, suffix: &str) -> Self {
348 self.tag_prefix = prefix.to_string();
349 self.tag_suffix = suffix.to_string();
350 self
351 }
352
353 pub fn collect_legends(mut self, collect: bool) -> Self {
356 self.collect_legends = collect;
357 self
358 }
359
360 pub fn legend_position(mut self, pos: GridLegendPosition) -> Self {
362 self.legend_position = pos;
363 self
364 }
365
366 pub fn legend_bottom_when_narrow(mut self, share: f64) -> Self {
374 self.legend_bottom_share = Some(share);
375 self
376 }
377
378 pub fn axis_collect(mut self, mode: AxisCollect) -> Self {
382 self.axis_collect = mode;
383 self
384 }
385
386 pub fn spacing(mut self, px: f64) -> Self {
388 self.spacing = if px.is_finite() { px.max(0.0) } else { 0.0 };
389 self
390 }
391
392 pub fn theme(mut self, theme: Theme) -> Self {
395 self.theme = theme;
396 self
397 }
398
399 pub fn svg_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
406 self.root_attrs.push((name.into(), value.into()));
407 self
408 }
409
410 pub fn svg_attrs(mut self, attrs: impl IntoIterator<Item = (String, String)>) -> Self {
412 self.root_attrs.extend(attrs);
413 self
414 }
415
416 pub fn svg_warnings_attr(mut self, on: bool) -> Self {
420 self.svg_warnings_attr = on;
421 self
422 }
423
424 pub fn len(&self) -> usize {
426 self.cells.len()
427 }
428
429 pub fn is_empty(&self) -> bool {
431 self.cells.is_empty()
432 }
433
434 pub fn render_svg_native(self) -> Result<String, GGError> {
436 self.render_svg_native_with_size(800, 600)
437 }
438
439 pub fn render_svg_native_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
443 Ok(self.render_page(w, h)?.0.finish())
444 }
445
446 pub fn render_svg_native_with_warnings(
449 self,
450 w: u32,
451 h: u32,
452 ) -> Result<(String, Vec<String>), GGError> {
453 let (page, warnings) = self.render_page(w, h)?;
454 Ok((page.finish(), warnings))
455 }
456
457 pub fn render_svg_native_at(self, x: f64, y: f64, w: u32, h: u32) -> Result<String, GGError> {
460 Ok(self.render_page(w, h)?.0.finish_fragment(x, y))
461 }
462
463 fn render_page(self, w: u32, h: u32) -> Result<(SvgBackend, Vec<String>), GGError> {
464 let (w, h) = (w.max(1), h.max(1));
465 let mut theme = self.theme.clone();
466 theme.resolve_inheritance();
467 let collect = self.collect_legends;
468 let mut legend_pos = self.legend_position;
469 let alt_pos = self
472 .legend_bottom_share
473 .filter(|_| legend_pos == GridLegendPosition::Right)
474 .map(|share| (share, GridLegendPosition::Bottom));
475 let root_attrs = self.root_attrs.clone();
476 let svg_warnings_attr = self.svg_warnings_attr;
477
478 let mut ctx = PrepCtx {
481 tags: self
482 .tags
483 .clone()
484 .map(|t| (t, self.tag_prefix.clone(), self.tag_suffix.clone())),
485 next_tag: 0,
486 next_index: 0,
487 collect: collect.then_some(legend_pos),
488 collect_alt: alt_pos.map(|(_, p)| p),
489 legends: Vec::new(),
490 page_w: w as f64,
491 page_h: h as f64,
492 legend_warnings: Vec::new(),
493 };
494 let prepared = prepare_grid(self, &mut ctx)?;
495 let (mut legends, alt_legends): (Vec<Legend>, Vec<Option<Legend>>) =
496 ctx.legends.into_iter().unzip();
497 if let Some((share, alt)) = alt_pos {
498 let strip_w = legends.iter().map(|l| l.w).fold(0.0, f64::max) + 2.0 * LEGEND_PAD;
499 if !legends.is_empty() && strip_w > share * w as f64 {
500 legend_pos = alt;
501 legends = alt_legends.into_iter().flatten().collect();
502 }
503 }
504 let legend_warnings = ctx.legend_warnings;
505
506 let full = Rect {
508 x: 0.0,
509 y: 0.0,
510 width: w as f64,
511 height: h as f64,
512 };
513 let mut page = SvgBackend::new(w, h, full.clone());
514 page.without_plot_attr();
515 let (rows, cols) = prepared.dims();
516 let data_grid_attr = ("data-grid".to_string(), format!("{rows} {cols}"));
517 page.set_root_attrs(vec![data_grid_attr.clone()]);
518 if let Some(fill) = theme
519 .plot_background
520 .fill
521 .filter(|_| theme.plot_background.visible)
522 {
523 page.draw_rect(
524 (0.0, 0.0),
525 (w as f64, h as f64),
526 &RectStyle {
527 fill: Some(fill),
528 stroke: None,
529 stroke_width: 0.0,
530 alpha: 1.0,
531 clip: false,
532 corner_radius: 0.0,
533 },
534 )
535 .map_err(GGError::Render)?;
536 }
537
538 let mut body = full;
539 if !legends.is_empty() {
540 let gap = theme.legend_spacing.max(0.0) * 2.0;
541 let pad = LEGEND_PAD;
542 match legend_pos {
543 GridLegendPosition::Right => {
544 let strip_w = legends.iter().map(|l| l.w).fold(0.0, f64::max) + 2.0 * pad;
545 let strip_w = strip_w.min(body.width * 0.4);
546 body.width -= strip_w;
547 let total_h =
548 legends.iter().map(|l| l.h).sum::<f64>() + gap * (legends.len() - 1) as f64;
549 let mut y = (body.y + (body.height - total_h) / 2.0).max(body.y + pad);
550 for l in &legends {
551 place_legend(&mut page, l, body.x + body.width + pad, y);
552 y += l.h + gap;
553 }
554 }
555 GridLegendPosition::Bottom => {
556 let strip_h = legends.iter().map(|l| l.h).fold(0.0, f64::max) + 2.0 * pad;
557 let strip_h = strip_h.min(body.height * 0.4);
558 body.height -= strip_h;
559 let total_w =
560 legends.iter().map(|l| l.w).sum::<f64>() + gap * (legends.len() - 1) as f64;
561 let mut x = (body.x + (body.width - total_w) / 2.0).max(body.x + pad);
562 for l in &legends {
563 place_legend(&mut page, l, x, body.y + body.height + pad);
564 x += l.w + gap;
565 }
566 }
567 }
568 }
569
570 let mut warnings = legend_warnings;
572 render_prepared(prepared, &body, &mut page, &mut warnings)?;
573
574 let mut reserved = vec![data_grid_attr];
575 if svg_warnings_attr {
576 reserved.push(("data-warnings".to_string(), String::new()));
577 }
578 let extra = crate::render::svg_backend::validate_root_attrs(
579 &root_attrs,
580 &reserved,
581 "grid",
582 &mut warnings,
583 );
584 page.extend_root_attrs(extra);
585 if svg_warnings_attr {
586 page.extend_root_attrs(vec![(
587 "data-warnings".to_string(),
588 crate::render::svg_backend::warnings_json(&warnings),
589 )]);
590 }
591 Ok((page, warnings))
592 }
593}
594
595const LEGEND_PAD: f64 = 8.0;
597
598struct Legend {
600 body: String,
601 x0: f64,
602 y0: f64,
603 w: f64,
604 h: f64,
605}
606
607fn place_legend(page: &mut SvgBackend, l: &Legend, x: f64, y: f64) {
608 let (tx, ty) = (x - l.x0, y - l.y0);
609 if tx.is_finite() && ty.is_finite() {
610 page.push_raw(&format!(
611 "<g class=\"legend\" transform=\"translate({tx:.2} {ty:.2})\">{}</g>",
612 l.body
613 ));
614 }
615}
616
617struct PrepCtx {
618 tags: Option<(TagLevels, String, String)>,
619 next_tag: usize,
620 next_index: usize,
621 collect: Option<GridLegendPosition>,
622 collect_alt: Option<GridLegendPosition>,
625 legends: Vec<(Legend, Option<Legend>)>,
627 page_w: f64,
633 page_h: f64,
634 legend_warnings: Vec<String>,
640}
641
642struct PreparedPlot {
644 built: crate::build::BuiltPlot,
645 meta: RenderMeta,
646 panel: String,
647 svg_formatted_values: bool,
655 root_attrs: Vec<(String, String)>,
663}
664
665enum Prepared {
666 Plot(Box<PreparedPlot>),
667 Grid(Box<PreparedGrid>),
668 Spacer,
669}
670
671struct PreparedGrid {
672 cells: Vec<Prepared>,
673 ncol: usize,
674 nrow: usize,
675 byrow: bool,
676 widths: Vec<f64>,
677 heights: Vec<f64>,
678 title: Option<String>,
679 subtitle: Option<String>,
680 caption: Option<String>,
681 spacing: f64,
682 theme: Theme,
683 axis_collect: AxisCollect,
684}
685
686impl Prepared {
687 fn dims(&self) -> (usize, usize) {
688 match self {
689 Prepared::Grid(g) => (g.nrow, g.ncol),
690 _ => (1, 1),
691 }
692 }
693}
694
695fn prepare_grid(grid: PlotGrid, ctx: &mut PrepCtx) -> Result<Prepared, GGError> {
696 let n = grid.cells.len();
697 let (ncol, nrow) = match (grid.ncol, grid.nrow, grid.op) {
698 (Some(c), Some(r), _) => {
699 (c, r.max(n.div_ceil(c)))
701 }
702 (Some(c), None, _) => (c, n.div_ceil(c).max(1)),
703 (None, Some(r), _) => (n.div_ceil(r).max(1), r),
704 (None, None, Some(Op::Row)) => (n.max(1), 1),
705 (None, None, Some(Op::Col)) => (1, n.max(1)),
706 (None, None, None) => {
707 let c = ((n as f64).sqrt().ceil() as usize).max(1);
708 (c, n.div_ceil(c).max(1))
709 }
710 };
711 let mut theme = grid.theme;
712 theme.resolve_inheritance();
713 let mut cells = Vec::with_capacity(n);
714 for cell in grid.cells {
715 cells.push(match cell {
716 GridCell::Spacer => Prepared::Spacer,
717 GridCell::Grid(g) => prepare_grid(*g, ctx)?,
718 GridCell::Plot(p) => Prepared::Plot(Box::new(prepare_plot(*p, ctx)?)),
719 });
720 }
721 Ok(Prepared::Grid(Box::new(PreparedGrid {
722 cells,
723 ncol,
724 nrow,
725 byrow: grid.byrow,
726 widths: grid.widths,
727 heights: grid.heights,
728 title: grid.title,
729 subtitle: grid.subtitle,
730 caption: grid.caption,
731 spacing: grid.spacing,
732 theme,
733 axis_collect: grid.axis_collect,
734 })))
735}
736
737fn prepare_plot(mut plot: GGPlot, ctx: &mut PrepCtx) -> Result<PreparedPlot, GGError> {
738 ctx.next_index += 1;
739 if let Some((levels, pre, suf)) = &ctx.tags {
740 plot.labels.tag = Some(format!("{pre}{}{suf}", levels.tag(ctx.next_tag)));
741 ctx.next_tag += 1;
742 }
743 let panel = plot
744 .labels
745 .tag
746 .clone()
747 .unwrap_or_else(|| ctx.next_index.to_string());
748
749 let original = plot.theme.legend_position.clone();
751 let collect = ctx.collect.filter(|_| {
752 matches!(
753 original,
754 LegendPosition::Right
755 | LegendPosition::Left
756 | LegendPosition::Top
757 | LegendPosition::Bottom
758 )
759 });
760 if collect.is_some() {
761 plot.theme.legend_position = LegendPosition::None;
762 }
763 let svg_formatted_values = plot.svg_formatted_values;
764 let root_attrs = plot.root_attrs.clone();
765 let (built, mut meta) = plot.build_for_render()?;
766 if let Some(pos) = collect {
767 meta.has_legend = false;
768 if let Some(l) = capture_legend(
769 &built,
770 pos,
771 ctx.page_w,
772 ctx.page_h,
773 &mut ctx.legend_warnings,
774 )
775 .map_err(GGError::Render)?
776 {
777 if !ctx.legends.iter().any(|(o, _)| o.body == l.body) {
778 let alt = match ctx.collect_alt {
779 Some(alt) => {
780 capture_legend(&built, alt, ctx.page_w, ctx.page_h, &mut Vec::new())
781 .map_err(GGError::Render)?
782 }
783 None => None,
784 };
785 ctx.legends.push((l, alt));
786 }
787 }
788 }
789 Ok(PreparedPlot {
790 built,
791 meta,
792 panel,
793 svg_formatted_values,
794 root_attrs,
795 })
796}
797
798fn capture_legend(
806 built: &crate::build::BuiltPlot,
807 pos: GridLegendPosition,
808 page_w: f64,
809 page_h: f64,
810 warnings: &mut Vec<String>,
818) -> Result<Option<Legend>, RenderError> {
819 let mut theme = built.theme.clone();
820 let m = theme.legend_margin.clone();
821 let (pa, lp, is_horizontal) = match pos {
823 GridLegendPosition::Right => (
824 Rect {
825 x: -m.left,
826 y: -m.top,
827 width: 0.0,
828 height: 0.0,
829 },
830 LegendPosition::Right,
831 false,
832 ),
833 GridLegendPosition::Bottom => (
834 Rect {
835 x: -m.left,
836 y: -(m.top + 30.0),
837 width: 0.0,
838 height: 0.0,
839 },
840 LegendPosition::Bottom,
841 true,
842 ),
843 };
844 theme.legend_position = lp;
845 let mut svg = SvgBackend::new(1, 1, pa.clone());
846 let mut meas = Measure::new(&mut svg, pa.clone());
847 let avail_width = (page_w + m.left).max(0.0);
848 let avail_height = (page_h + m.top).max(0.0);
849 let legend_plans = legend::plan_all_legends(
850 &built.scales,
851 &built.guide_legend,
852 &theme,
853 &built.suppressed_aes,
854 avail_width,
855 avail_height,
856 is_horizontal,
857 warnings,
858 );
859 legend::draw_legend_planned(
860 &built.scales,
861 &theme,
862 &pa,
863 avail_width,
870 &mut meas,
871 &built.guide_legend,
872 &built.suppressed_aes,
873 &legend_plans,
874 )?;
875 let ext = meas.extent();
876 if svg.body().is_empty() {
877 return Ok(None);
878 }
879 let (x0, y0, x1, y1) = match ext {
880 Some(e) => e,
881 None => return Ok(None),
882 };
883 Ok(Some(Legend {
884 body: svg.body().to_string(),
885 x0,
886 y0,
887 w: (x1 - x0).max(0.0),
888 h: (y1 - y0).max(0.0),
889 }))
890}
891
892fn tracks(weights: &[f64], n: usize, total: f64, gap: f64) -> Vec<(f64, f64)> {
894 let ws: Vec<f64> = (0..n)
895 .map(|i| {
896 if weights.is_empty() {
897 1.0
898 } else {
899 let w = weights[i % weights.len()];
900 if w.is_finite() && w > 0.0 {
901 w
902 } else {
903 1.0
904 }
905 }
906 })
907 .collect();
908 let sum: f64 = ws.iter().sum();
909 let avail = (total - gap * n.saturating_sub(1) as f64).max(0.0);
910 let mut pos = 0.0;
911 ws.iter()
912 .map(|w| {
913 let size = avail * w / sum;
914 let t = (pos, size);
915 pos += size + gap;
916 t
917 })
918 .collect()
919}
920
921fn text_style(el: &crate::theme::elements::ElementText, anchor: TextAnchor) -> TextStyle {
922 TextStyle {
923 color: el.color,
924 size: el.size,
925 anchor,
926 angle: 0.0,
927 family: (!el.family.is_empty()).then(|| el.family.clone()),
928 face: el.face,
929 bg_colour: None,
930 bg_r: 0.0,
931 }
932}
933
934fn hjust_x(el: &crate::theme::elements::ElementText, x0: f64, x1: f64) -> (f64, TextAnchor) {
935 let hj = if el.hjust.is_finite() {
936 el.hjust.clamp(0.0, 1.0)
937 } else {
938 0.0
939 };
940 let anchor = if hj <= 0.02 {
941 TextAnchor::Start
942 } else if hj >= 0.98 {
943 TextAnchor::End
944 } else {
945 TextAnchor::Middle
946 };
947 (x0 + hj * (x1 - x0), anchor)
948}
949
950fn render_prepared(
951 prepared: Prepared,
952 area: &Rect,
953 page: &mut SvgBackend,
954 warnings: &mut Vec<String>,
955) -> Result<(), GGError> {
956 match prepared {
957 Prepared::Spacer => Ok(()),
958 Prepared::Plot(p) => {
959 let PreparedPlot {
960 mut built,
961 meta,
962 panel,
963 svg_formatted_values,
964 root_attrs,
965 } = *p;
966 let (cw, ch) = (
967 area.width.floor().max(1.0) as u32,
968 area.height.floor().max(1.0) as u32,
969 );
970 let layout = GGPlot::layout_built(&mut built, &meta, cw, ch);
971 let mut backend = SvgBackend::new(cw, ch, layout.plot_area.clone());
972 backend.set_formatted_values(svg_formatted_values);
973 let mut attrs = root_data_attrs(&built);
974 attrs.push(("data-panel".into(), panel.clone()));
975 backend.set_root_attrs(attrs);
976 PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
977 let mut attr_warnings = Vec::new();
984 let group_attrs = validate_root_attrs(&root_attrs, &[], "plot", &mut attr_warnings);
985 backend.wrap_body_in_group(&group_attrs);
986 warnings.extend(
987 built
988 .warnings
989 .iter()
990 .cloned()
991 .chain(backend.take_warnings())
992 .chain(attr_warnings)
993 .map(|w| format!("panel {panel}: {w}")),
994 );
995 page.push_raw(&backend.finish_fragment(area.x, area.y));
996 Ok(())
997 }
998 Prepared::Grid(g) => {
999 let mut g = *g;
1000 let theme = &g.theme;
1001 let m = &theme.plot_margin;
1002 let mut top = area.y;
1004 let (x0, x1) = (area.x + m.left, area.x + area.width - m.right);
1005 if let Some(t) = &g.title {
1006 top += m.top;
1007 let (tx, anchor) = hjust_x(&theme.title, x0, x1);
1008 page.draw_text(
1009 t,
1010 (tx, top + theme.title.size * 0.8),
1011 &text_style(&theme.title, anchor),
1012 )
1013 .map_err(GGError::Render)?;
1014 top += theme.title.size * 1.6;
1015 }
1016 if let Some(s) = &g.subtitle {
1017 if g.title.is_none() {
1018 top += m.top;
1019 }
1020 let (sx, anchor) = hjust_x(&theme.subtitle, x0, x1);
1021 page.draw_text(
1022 s,
1023 (sx, top + theme.subtitle.size * 0.7),
1024 &text_style(&theme.subtitle, anchor),
1025 )
1026 .map_err(GGError::Render)?;
1027 top += theme.subtitle.size * 1.5;
1028 }
1029 let mut bottom = area.y + area.height;
1030 if let Some(c) = &g.caption {
1031 let (cx, anchor) = hjust_x(&theme.caption, x0, x1);
1032 bottom -= theme.caption.size * 1.8;
1033 page.draw_text(
1034 c,
1035 (cx, bottom + theme.caption.size * 0.9),
1036 &text_style(&theme.caption, anchor),
1037 )
1038 .map_err(GGError::Render)?;
1039 }
1040 let body_h = (bottom - top).max(1.0);
1041 let cols = tracks(&g.widths, g.ncol, area.width, g.spacing);
1042 let rows = tracks(&g.heights, g.nrow, body_h, g.spacing);
1043 if g.axis_collect != AxisCollect::Keep {
1044 align_axes(
1045 &mut g.cells,
1046 (g.ncol, g.nrow, g.byrow),
1047 &cols,
1048 &rows,
1049 g.axis_collect,
1050 warnings,
1051 );
1052 }
1053 for (k, cell) in g.cells.into_iter().enumerate() {
1054 let (r, c) = if g.byrow {
1055 (k / g.ncol, k % g.ncol)
1056 } else {
1057 (k % g.nrow, k / g.nrow)
1058 };
1059 if r >= g.nrow || c >= g.ncol {
1060 warnings.push(format!(
1062 "plot_grid: cell {} does not fit a {}×{} grid; skipped",
1063 k + 1,
1064 g.nrow,
1065 g.ncol
1066 ));
1067 continue;
1068 }
1069 let rect = Rect {
1070 x: area.x + cols[c].0,
1071 y: top + rows[r].0,
1072 width: cols[c].1,
1073 height: rows[r].1,
1074 };
1075 render_prepared(cell, &rect, page, warnings)?;
1076 }
1077 Ok(())
1078 }
1079 }
1080}
1081
1082fn align_axes(
1088 cells: &mut [Prepared],
1089 (ncol, nrow, byrow): (usize, usize, bool),
1092 cols: &[(f64, f64)],
1093 rows: &[(f64, f64)],
1094 mode: AxisCollect,
1095 warnings: &mut Vec<String>,
1096) {
1097 let idx_of = |r: usize, c: usize| -> usize {
1098 if byrow {
1099 r * ncol + c
1100 } else {
1101 c * nrow + r
1102 }
1103 };
1104 for c in 0..ncol {
1105 let cw = cols.get(c).map_or(0.0, |t| t.1);
1106 if !cw.is_finite() || cw <= 0.0 {
1107 continue;
1108 }
1109 let plot_cells: Vec<(usize, usize)> = (0..nrow)
1113 .filter_map(|r| {
1114 let k = idx_of(r, c);
1115 (k < cells.len() && matches!(cells[k], Prepared::Plot(_))).then_some((r, k))
1116 })
1117 .collect();
1118 if plot_cells.len() < 2 {
1119 continue; }
1121 let bottom_row = plot_cells.iter().map(|&(r, _)| r).max().unwrap();
1122
1123 for &(r, k) in &plot_cells {
1128 if let Prepared::Plot(p) = &mut cells[k] {
1129 p.built.theme.axis_text_x_fit = AxisLabelFit::Shift;
1130 if mode == AxisCollect::CollectX && r != bottom_row {
1131 p.built.theme.axis_text_x = ElementText::blank();
1132 p.built.theme.axis_title_x = ElementText::blank();
1133 p.built.theme.axis_ticks_x = Some(ElementLine::blank());
1134 }
1135 }
1136 }
1137
1138 let mut natural: Vec<(f64, f64, f64, f64, f64)> = Vec::with_capacity(plot_cells.len());
1147 for &(r, k) in &plot_cells {
1148 let ch = rows.get(r).map_or(0.0, |t| t.1);
1149 match &mut cells[k] {
1150 Prepared::Plot(p) => {
1151 let layout = GGPlot::layout_built(
1152 &mut p.built,
1153 &p.meta,
1154 cw.floor().max(1.0) as u32,
1155 ch.floor().max(1.0) as u32,
1156 );
1157 natural.push((
1158 layout.plot_area.x,
1159 layout.plot_area.x + layout.plot_area.width,
1160 layout.plot_area.y,
1161 layout.plot_area.height,
1162 ch,
1163 ));
1164 }
1165 _ => unreachable!("plot_cells only holds Prepared::Plot indices"),
1166 }
1167 }
1168
1169 let target_left = natural.iter().fold(f64::MIN, |m, &(l, ..)| m.max(l));
1174 let target_right_inset = natural
1175 .iter()
1176 .fold(f64::MIN, |m, &(_, r, ..)| m.max(cw - r));
1177 let width = cw - target_right_inset - target_left;
1178 if target_left.is_finite() && target_right_inset.is_finite() {
1179 for (&(_, k), &(nl, nr, ..)) in plot_cells.iter().zip(&natural) {
1180 if let Prepared::Plot(p) = &mut cells[k] {
1181 p.built.theme.plot_margin.left += (target_left - nl).max(0.0);
1182 p.built.theme.plot_margin.right += (target_right_inset - (cw - nr)).max(0.0);
1183 }
1184 }
1185 }
1186
1187 if width.is_finite() && width > 0.0 {
1198 for (&(_, k), &(_, _, y, h, ch)) in plot_cells.iter().zip(&natural) {
1199 if let Prepared::Plot(p) = &mut cells[k] {
1200 let bottom_budget = ch - p.built.theme.plot_margin.bottom;
1201 if y + h > bottom_budget + 0.5 {
1202 let fits = (ch - y - p.built.theme.plot_margin.bottom).max(8.0);
1203 p.built.theme.aspect_ratio = Some(fits / width);
1204 p.built.theme.plot_margin.top -= (h - fits) / 2.0;
1205 }
1206 }
1207 }
1208 }
1209
1210 if mode == AxisCollect::CollectX {
1211 collect_x_domain(cells, &plot_cells, warnings);
1212 }
1213 }
1214}
1215
1216fn collect_x_domain(
1225 cells: &mut [Prepared],
1226 plot_cells: &[(usize, usize)],
1227 warnings: &mut Vec<String>,
1228) {
1229 let mut domains = Vec::with_capacity(plot_cells.len());
1230 for &(_, k) in plot_cells {
1231 let d = match &cells[k] {
1232 Prepared::Plot(p) => match p.built.scales.get(&Aesthetic::X) {
1233 Some(s) if !s.is_discrete() => s.domain(),
1234 _ => None,
1235 },
1236 _ => None,
1237 };
1238 match d {
1239 Some(d) => domains.push(d),
1240 None => {
1241 warnings.push(
1242 "plot_grid: a column's x scale is discrete or untrained; x domain not \
1243 collected"
1244 .to_string(),
1245 );
1246 return;
1247 }
1248 }
1249 }
1250 let lo = domains.iter().map(|d| d.0).fold(f64::INFINITY, f64::min);
1251 let hi = domains
1252 .iter()
1253 .map(|d| d.1)
1254 .fold(f64::NEG_INFINITY, f64::max);
1255 if lo.is_finite() && hi.is_finite() && hi >= lo {
1256 for &(_, k) in plot_cells {
1257 if let Prepared::Plot(p) = &mut cells[k] {
1258 p.built.scales.set_limits(&Aesthetic::X, lo, hi);
1259 }
1260 }
1261 }
1262}
1263
1264struct Measure<'a> {
1266 inner: &'a mut SvgBackend,
1267 pa: Rect,
1268 bbox: Option<(f64, f64, f64, f64)>,
1269}
1270
1271impl<'a> Measure<'a> {
1272 fn new(inner: &'a mut SvgBackend, pa: Rect) -> Self {
1273 Measure {
1274 inner,
1275 pa,
1276 bbox: None,
1277 }
1278 }
1279
1280 fn grow(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) {
1281 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
1282 return;
1283 }
1284 self.bbox = Some(match self.bbox {
1285 None => (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)),
1286 Some((a, b, c, d)) => (
1287 a.min(x0.min(x1)),
1288 b.min(y0.min(y1)),
1289 c.max(x0.max(x1)),
1290 d.max(y0.max(y1)),
1291 ),
1292 });
1293 }
1294
1295 fn extent(&self) -> Option<(f64, f64, f64, f64)> {
1296 self.bbox
1297 }
1298}
1299
1300impl DrawBackend for Measure<'_> {
1301 fn draw_circle(
1302 &mut self,
1303 c: (f64, f64),
1304 r: f64,
1305 style: &PointStyle,
1306 ) -> Result<(), RenderError> {
1307 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
1308 self.inner.draw_circle(c, r, style)
1309 }
1310 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
1311 for p in pts {
1312 self.grow(p.0, p.1, p.0, p.1);
1313 }
1314 self.inner.draw_line(pts, style)
1315 }
1316 fn draw_quadratic(
1317 &mut self,
1318 p0: (f64, f64),
1319 ctrl: (f64, f64),
1320 p1: (f64, f64),
1321 style: &LineStyle,
1322 ) -> Result<(), RenderError> {
1323 self.grow(p0.0, p0.1, p0.0, p0.1);
1324 self.grow(ctrl.0, ctrl.1, ctrl.0, ctrl.1);
1325 self.grow(p1.0, p1.1, p1.0, p1.1);
1326 self.inner.draw_quadratic(p0, ctrl, p1, style)
1327 }
1328 fn draw_rect(
1329 &mut self,
1330 a: (f64, f64),
1331 b: (f64, f64),
1332 style: &RectStyle,
1333 ) -> Result<(), RenderError> {
1334 self.grow(a.0, a.1, b.0, b.1);
1335 self.inner.draw_rect(a, b, style)
1336 }
1337 fn draw_text(
1338 &mut self,
1339 text: &str,
1340 pos: (f64, f64),
1341 style: &TextStyle,
1342 ) -> Result<(), RenderError> {
1343 self.draw_text_with_title(text, pos, style, None)
1344 }
1345 fn draw_text_with_title(
1346 &mut self,
1347 text: &str,
1348 pos: (f64, f64),
1349 style: &TextStyle,
1350 title: Option<&str>,
1351 ) -> Result<(), RenderError> {
1352 let w = text.chars().count() as f64 * style.size * 0.6;
1360 let (x0, x1) = match style.anchor {
1361 TextAnchor::Start => (pos.0, pos.0 + w),
1362 TextAnchor::Middle => (pos.0 - w / 2.0, pos.0 + w / 2.0),
1363 TextAnchor::End => (pos.0 - w, pos.0),
1364 };
1365 self.grow(x0, pos.1 - style.size / 2.0, x1, pos.1 + style.size / 2.0);
1366 self.inner.draw_text_with_title(text, pos, style, title)
1367 }
1368 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
1369 for p in pts {
1370 self.grow(p.0, p.1, p.0, p.1);
1371 }
1372 self.inner.draw_polygon(pts, style)
1373 }
1374 fn draw_shape(&mut self, c: (f64, f64), r: f64, style: &PointStyle) -> Result<(), RenderError> {
1375 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
1376 self.inner.draw_shape(c, r, style)
1377 }
1378 fn plot_area(&self) -> Rect {
1379 self.pa.clone()
1380 }
1381 fn total_area(&self) -> Rect {
1382 self.inner.total_area()
1383 }
1384 fn set_tooltip(&mut self, t: Option<String>) {
1385 self.inner.set_tooltip(t)
1386 }
1387 fn set_mark_axis(&mut self, k: Option<String>) {
1388 self.inner.set_mark_axis(k)
1389 }
1390 fn set_mark_series(&mut self, s: Option<String>) {
1391 self.inner.set_mark_series(s)
1392 }
1393 fn set_mark_value(&mut self, v: Option<String>) {
1394 self.inner.set_mark_value(v)
1395 }
1396 fn warn(&mut self, m: String) {
1397 self.inner.warn(m)
1398 }
1399 fn set_mark_geom(&mut self, geom: Option<&str>) {
1400 self.inner.set_mark_geom(geom)
1401 }
1402 fn set_mark_part(&mut self, part: Option<&str>) {
1403 self.inner.set_mark_part(part)
1404 }
1405 fn set_mark_attrs(&mut self, attrs: Vec<(String, String)>) {
1406 self.inner.set_mark_attrs(attrs)
1407 }
1408
1409 fn set_mark_class(&mut self, class: Option<String>) {
1410 self.inner.set_mark_class(class)
1411 }
1412
1413 fn set_mark_group(&mut self, group: Option<(String, String)>) {
1414 self.inner.set_mark_group(group)
1415 }
1416
1417 fn set_layer_attrs(&mut self, attrs: Vec<(String, String)>) {
1418 self.inner.set_layer_attrs(attrs)
1419 }
1420 fn wants_formatted_values(&self) -> bool {
1421 self.inner.wants_formatted_values()
1422 }
1423 fn set_mark_fvalue(&mut self, value: Option<String>) {
1424 self.inner.set_mark_fvalue(value)
1425 }
1426 fn set_mark_fx(&mut self, value: Option<String>) {
1427 self.inner.set_mark_fx(value)
1428 }
1429}
1430
1431fn join(op: Op, a: GridCell, b: GridCell) -> PlotGrid {
1434 let flatten = |c: GridCell| -> Vec<GridCell> {
1435 match c {
1436 GridCell::Grid(g) if g.op == Some(op) && g.is_plain() => g.cells,
1437 other => vec![other],
1438 }
1439 };
1440 let mut cells = flatten(a);
1441 cells.extend(flatten(b));
1442 PlotGrid {
1443 cells,
1444 op: Some(op),
1445 ..PlotGrid::default()
1446 }
1447}
1448
1449impl PlotGrid {
1450 fn is_plain(&self) -> bool {
1453 self.ncol.is_none()
1454 && self.nrow.is_none()
1455 && self.widths.is_empty()
1456 && self.heights.is_empty()
1457 && self.title.is_none()
1458 && self.subtitle.is_none()
1459 && self.caption.is_none()
1460 && self.tags.is_none()
1461 && !self.collect_legends
1462 && self.legend_bottom_share.is_none()
1463 && self.axis_collect == AxisCollect::Keep
1464 }
1465}
1466
1467macro_rules! grid_ops {
1468 ($lhs:ty, $rhs:ty) => {
1469 impl BitOr<$rhs> for $lhs {
1471 type Output = PlotGrid;
1472 fn bitor(self, rhs: $rhs) -> PlotGrid {
1473 join(Op::Row, self.into(), rhs.into())
1474 }
1475 }
1476 impl Div<$rhs> for $lhs {
1478 type Output = PlotGrid;
1479 fn div(self, rhs: $rhs) -> PlotGrid {
1480 join(Op::Col, self.into(), rhs.into())
1481 }
1482 }
1483 };
1484}
1485
1486grid_ops!(GGPlot, GGPlot);
1487grid_ops!(GGPlot, PlotGrid);
1488grid_ops!(PlotGrid, GGPlot);
1489grid_ops!(PlotGrid, PlotGrid);
1490
1491#[cfg(test)]
1492mod tests {
1493 use super::*;
1494
1495 #[test]
1496 fn tag_sequences() {
1497 let l: Vec<String> = (0..28).map(|i| TagLevels::Lower.tag(i)).collect();
1498 assert_eq!(&l[..3], &["a", "b", "c"]);
1499 assert_eq!(l[25], "z");
1500 assert_eq!(l[26], "aa");
1501 assert_eq!(l[27], "ab");
1502 assert_eq!(TagLevels::Upper.tag(1), "B");
1503 assert_eq!(TagLevels::Numeric.tag(9), "10");
1504 assert_eq!(TagLevels::LowerRoman.tag(3), "iv");
1505 assert_eq!(TagLevels::UpperRoman.tag(13), "XIV");
1506 assert_eq!(TagLevels::Custom(vec!["x".into()]).tag(1), "2");
1507 }
1508
1509 #[test]
1510 fn tracks_are_relative_and_robust() {
1511 let t = tracks(&[2.0, 1.0], 2, 300.0, 0.0);
1512 assert_eq!(t, vec![(0.0, 200.0), (200.0, 100.0)]);
1513 let t = tracks(&[f64::NAN, -1.0], 2, 100.0, 10.0);
1514 assert_eq!(t, vec![(0.0, 45.0), (55.0, 45.0)]);
1515 let t = tracks(&[], 3, 30.0, 100.0);
1516 assert!(t.iter().all(|(p, s)| p.is_finite() && *s == 0.0));
1517 }
1518
1519 #[test]
1523 fn measure_forwards_draw_quadratic_and_mark_metadata_and_grows_its_extent() {
1524 let pa = Rect {
1525 x: 0.0,
1526 y: 0.0,
1527 width: 100.0,
1528 height: 100.0,
1529 };
1530 let mut backend = SvgBackend::new(100, 100, pa.clone());
1531 backend.set_formatted_values(true);
1532 let mut meas = Measure::new(&mut backend, pa);
1533
1534 assert!(meas.extent().is_none());
1535 assert!(meas.wants_formatted_values());
1537 meas.set_mark_geom(Some("line"));
1538 meas.set_mark_part(Some("arrow"));
1539 meas.set_mark_attrs(vec![("data-x".to_string(), "v".to_string())]);
1540 meas.set_mark_fvalue(Some("1,234.5".to_string()));
1541 meas.set_mark_fx(Some("2026-10-12".to_string()));
1542 meas.draw_quadratic(
1543 (10.0, 20.0),
1544 (90.0, 5.0),
1545 (50.0, 80.0),
1546 &LineStyle::default(),
1547 )
1548 .unwrap();
1549 meas.set_mark_geom(None);
1550 meas.set_mark_part(None);
1551 meas.set_mark_attrs(Vec::new());
1552 meas.set_mark_fvalue(None);
1553 meas.set_mark_fx(None);
1554
1555 let (x0, y0, x1, y1) = meas.extent().expect("extent after drawing");
1556 assert_eq!((x0, y0, x1, y1), (10.0, 5.0, 90.0, 80.0));
1557
1558 let svg = backend.finish();
1562 assert!(svg.contains("data-part=\"arrow\""));
1563 assert!(svg.contains("data-x=\"v\""));
1564 assert!(svg.contains("gg-arrow"));
1565 assert!(svg.contains("data-fvalue=\"1,234.5\""));
1566 assert!(svg.contains("data-fx=\"2026-10-12\""));
1567 }
1568
1569 #[test]
1580 fn measure_forwards_the_title_to_its_inner_backend() {
1581 let pa = Rect {
1582 x: 0.0,
1583 y: 0.0,
1584 width: 100.0,
1585 height: 100.0,
1586 };
1587 let mut backend = SvgBackend::new(100, 100, pa.clone());
1588 let mut meas = Measure::new(&mut backend, pa);
1589 meas.draw_text_with_title(
1590 "Grp…",
1591 (10.0, 20.0),
1592 &TextStyle {
1593 size: 12.0,
1594 anchor: TextAnchor::Start,
1595 ..Default::default()
1596 },
1597 Some("Group 01"),
1598 )
1599 .unwrap();
1600 let svg = backend.finish();
1601 assert!(
1602 svg.contains("<title>Group 01</title>"),
1603 "the full label should reach the inner backend's <title>: {svg}"
1604 );
1605 }
1606}