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}
187
188impl Default for PlotGrid {
189 fn default() -> Self {
190 PlotGrid {
191 cells: Vec::new(),
192 ncol: None,
193 nrow: None,
194 byrow: true,
195 widths: Vec::new(),
196 heights: Vec::new(),
197 title: None,
198 subtitle: None,
199 caption: None,
200 tags: None,
201 tag_prefix: String::new(),
202 tag_suffix: String::new(),
203 collect_legends: false,
204 legend_position: GridLegendPosition::Right,
205 spacing: 0.0,
206 theme: Theme::default(),
207 op: None,
208 }
209 }
210}
211
212const MAX_TRACKS: usize = 10_000;
214
215impl PlotGrid {
216 pub fn new() -> Self {
217 Self::default()
218 }
219
220 #[allow(clippy::should_implement_trait)]
222 pub fn add(mut self, cell: impl Into<GridCell>) -> Self {
223 self.cells.push(cell.into());
224 self
225 }
226
227 pub fn add_all(mut self, plots: impl IntoIterator<Item = GGPlot>) -> Self {
229 self.cells.extend(plots.into_iter().map(GridCell::from));
230 self
231 }
232
233 pub fn add_spacer(mut self) -> Self {
235 self.cells.push(GridCell::Spacer);
236 self
237 }
238
239 pub fn ncol(mut self, n: usize) -> Self {
241 self.ncol = (n > 0).then_some(n.min(MAX_TRACKS));
242 self
243 }
244
245 pub fn nrow(mut self, n: usize) -> Self {
247 self.nrow = (n > 0).then_some(n.min(MAX_TRACKS));
248 self
249 }
250
251 pub fn byrow(mut self, byrow: bool) -> Self {
253 self.byrow = byrow;
254 self
255 }
256
257 pub fn widths(mut self, widths: &[f64]) -> Self {
259 self.widths = widths.to_vec();
260 self
261 }
262
263 pub fn heights(mut self, heights: &[f64]) -> Self {
265 self.heights = heights.to_vec();
266 self
267 }
268
269 pub fn title(mut self, title: &str) -> Self {
271 self.title = Some(title.to_string());
272 self
273 }
274
275 pub fn subtitle(mut self, subtitle: &str) -> Self {
277 self.subtitle = Some(subtitle.to_string());
278 self
279 }
280
281 pub fn caption(mut self, caption: &str) -> Self {
283 self.caption = Some(caption.to_string());
284 self
285 }
286
287 pub fn tag_levels(mut self, levels: TagLevels) -> Self {
289 self.tags = Some(levels);
290 self
291 }
292
293 pub fn tag_affixes(mut self, prefix: &str, suffix: &str) -> Self {
295 self.tag_prefix = prefix.to_string();
296 self.tag_suffix = suffix.to_string();
297 self
298 }
299
300 pub fn collect_legends(mut self, collect: bool) -> Self {
303 self.collect_legends = collect;
304 self
305 }
306
307 pub fn legend_position(mut self, pos: GridLegendPosition) -> Self {
309 self.legend_position = pos;
310 self
311 }
312
313 pub fn spacing(mut self, px: f64) -> Self {
315 self.spacing = if px.is_finite() { px.max(0.0) } else { 0.0 };
316 self
317 }
318
319 pub fn theme(mut self, theme: Theme) -> Self {
322 self.theme = theme;
323 self
324 }
325
326 pub fn len(&self) -> usize {
328 self.cells.len()
329 }
330
331 pub fn is_empty(&self) -> bool {
333 self.cells.is_empty()
334 }
335
336 pub fn render_svg_native(self) -> Result<String, GGError> {
338 self.render_svg_native_with_size(800, 600)
339 }
340
341 pub fn render_svg_native_with_size(self, w: u32, h: u32) -> Result<String, GGError> {
345 Ok(self.render_page(w, h)?.0.finish())
346 }
347
348 pub fn render_svg_native_with_warnings(
351 self,
352 w: u32,
353 h: u32,
354 ) -> Result<(String, Vec<String>), GGError> {
355 let (page, warnings) = self.render_page(w, h)?;
356 Ok((page.finish(), warnings))
357 }
358
359 pub fn render_svg_native_at(self, x: f64, y: f64, w: u32, h: u32) -> Result<String, GGError> {
362 Ok(self.render_page(w, h)?.0.finish_fragment(x, y))
363 }
364
365 fn render_page(self, w: u32, h: u32) -> Result<(SvgBackend, Vec<String>), GGError> {
366 let (w, h) = (w.max(1), h.max(1));
367 let mut theme = self.theme.clone();
368 theme.resolve_inheritance();
369 let collect = self.collect_legends;
370 let legend_pos = self.legend_position;
371
372 let mut ctx = PrepCtx {
375 tags: self
376 .tags
377 .clone()
378 .map(|t| (t, self.tag_prefix.clone(), self.tag_suffix.clone())),
379 next_tag: 0,
380 next_index: 0,
381 collect: collect.then_some(legend_pos),
382 legends: Vec::new(),
383 };
384 let prepared = prepare_grid(self, &mut ctx)?;
385 let legends = ctx.legends;
386
387 let full = Rect {
389 x: 0.0,
390 y: 0.0,
391 width: w as f64,
392 height: h as f64,
393 };
394 let mut page = SvgBackend::new(w, h, full.clone());
395 page.without_plot_attr();
396 let (rows, cols) = prepared.dims();
397 page.set_root_attrs(vec![("data-grid".into(), format!("{rows} {cols}"))]);
398 if let Some(fill) = theme
399 .plot_background
400 .fill
401 .filter(|_| theme.plot_background.visible)
402 {
403 page.draw_rect(
404 (0.0, 0.0),
405 (w as f64, h as f64),
406 &RectStyle {
407 fill: Some(fill),
408 stroke: None,
409 stroke_width: 0.0,
410 alpha: 1.0,
411 clip: false,
412 },
413 )
414 .map_err(GGError::Render)?;
415 }
416
417 let mut body = full;
418 if !legends.is_empty() {
419 let gap = theme.legend_spacing.max(0.0) * 2.0;
420 let pad = 8.0;
421 match legend_pos {
422 GridLegendPosition::Right => {
423 let strip_w = legends.iter().map(|l| l.w).fold(0.0, f64::max) + 2.0 * pad;
424 let strip_w = strip_w.min(body.width * 0.4);
425 body.width -= strip_w;
426 let total_h =
427 legends.iter().map(|l| l.h).sum::<f64>() + gap * (legends.len() - 1) as f64;
428 let mut y = (body.y + (body.height - total_h) / 2.0).max(body.y + pad);
429 for l in &legends {
430 place_legend(&mut page, l, body.x + body.width + pad, y);
431 y += l.h + gap;
432 }
433 }
434 GridLegendPosition::Bottom => {
435 let strip_h = legends.iter().map(|l| l.h).fold(0.0, f64::max) + 2.0 * pad;
436 let strip_h = strip_h.min(body.height * 0.4);
437 body.height -= strip_h;
438 let total_w =
439 legends.iter().map(|l| l.w).sum::<f64>() + gap * (legends.len() - 1) as f64;
440 let mut x = (body.x + (body.width - total_w) / 2.0).max(body.x + pad);
441 for l in &legends {
442 place_legend(&mut page, l, x, body.y + body.height + pad);
443 x += l.w + gap;
444 }
445 }
446 }
447 }
448
449 let mut warnings = Vec::new();
451 render_prepared(prepared, &body, &mut page, &mut warnings)?;
452 Ok((page, warnings))
453 }
454}
455
456struct Legend {
458 body: String,
459 x0: f64,
460 y0: f64,
461 w: f64,
462 h: f64,
463}
464
465fn place_legend(page: &mut SvgBackend, l: &Legend, x: f64, y: f64) {
466 let (tx, ty) = (x - l.x0, y - l.y0);
467 if tx.is_finite() && ty.is_finite() {
468 page.push_raw(&format!(
469 "<g class=\"legend\" transform=\"translate({tx:.2} {ty:.2})\">{}</g>",
470 l.body
471 ));
472 }
473}
474
475struct PrepCtx {
476 tags: Option<(TagLevels, String, String)>,
477 next_tag: usize,
478 next_index: usize,
479 collect: Option<GridLegendPosition>,
480 legends: Vec<Legend>,
481}
482
483struct PreparedPlot {
485 built: crate::build::BuiltPlot,
486 meta: RenderMeta,
487 panel: String,
488}
489
490enum Prepared {
491 Plot(Box<PreparedPlot>),
492 Grid(Box<PreparedGrid>),
493 Spacer,
494}
495
496struct PreparedGrid {
497 cells: Vec<Prepared>,
498 ncol: usize,
499 nrow: usize,
500 byrow: bool,
501 widths: Vec<f64>,
502 heights: Vec<f64>,
503 title: Option<String>,
504 subtitle: Option<String>,
505 caption: Option<String>,
506 spacing: f64,
507 theme: Theme,
508}
509
510impl Prepared {
511 fn dims(&self) -> (usize, usize) {
512 match self {
513 Prepared::Grid(g) => (g.nrow, g.ncol),
514 _ => (1, 1),
515 }
516 }
517}
518
519fn prepare_grid(grid: PlotGrid, ctx: &mut PrepCtx) -> Result<Prepared, GGError> {
520 let n = grid.cells.len();
521 let (ncol, nrow) = match (grid.ncol, grid.nrow, grid.op) {
522 (Some(c), Some(r), _) => {
523 (c, r.max(n.div_ceil(c)))
525 }
526 (Some(c), None, _) => (c, n.div_ceil(c).max(1)),
527 (None, Some(r), _) => (n.div_ceil(r).max(1), r),
528 (None, None, Some(Op::Row)) => (n.max(1), 1),
529 (None, None, Some(Op::Col)) => (1, n.max(1)),
530 (None, None, None) => {
531 let c = ((n as f64).sqrt().ceil() as usize).max(1);
532 (c, n.div_ceil(c).max(1))
533 }
534 };
535 let mut theme = grid.theme;
536 theme.resolve_inheritance();
537 let mut cells = Vec::with_capacity(n);
538 for cell in grid.cells {
539 cells.push(match cell {
540 GridCell::Spacer => Prepared::Spacer,
541 GridCell::Grid(g) => prepare_grid(*g, ctx)?,
542 GridCell::Plot(p) => Prepared::Plot(Box::new(prepare_plot(*p, ctx)?)),
543 });
544 }
545 Ok(Prepared::Grid(Box::new(PreparedGrid {
546 cells,
547 ncol,
548 nrow,
549 byrow: grid.byrow,
550 widths: grid.widths,
551 heights: grid.heights,
552 title: grid.title,
553 subtitle: grid.subtitle,
554 caption: grid.caption,
555 spacing: grid.spacing,
556 theme,
557 })))
558}
559
560fn prepare_plot(mut plot: GGPlot, ctx: &mut PrepCtx) -> Result<PreparedPlot, GGError> {
561 ctx.next_index += 1;
562 if let Some((levels, pre, suf)) = &ctx.tags {
563 plot.labels.tag = Some(format!("{pre}{}{suf}", levels.tag(ctx.next_tag)));
564 ctx.next_tag += 1;
565 }
566 let panel = plot
567 .labels
568 .tag
569 .clone()
570 .unwrap_or_else(|| ctx.next_index.to_string());
571
572 let original = plot.theme.legend_position.clone();
574 let collect = ctx.collect.filter(|_| {
575 matches!(
576 original,
577 LegendPosition::Right
578 | LegendPosition::Left
579 | LegendPosition::Top
580 | LegendPosition::Bottom
581 )
582 });
583 if collect.is_some() {
584 plot.theme.legend_position = LegendPosition::None;
585 }
586 let (built, mut meta) = plot.build_for_render()?;
587 if let Some(pos) = collect {
588 meta.has_legend = false;
589 if let Some(l) = capture_legend(&built, pos).map_err(GGError::Render)? {
590 if !ctx.legends.iter().any(|o| o.body == l.body) {
591 ctx.legends.push(l);
592 }
593 }
594 }
595 Ok(PreparedPlot { built, meta, panel })
596}
597
598fn capture_legend(
600 built: &crate::build::BuiltPlot,
601 pos: GridLegendPosition,
602) -> Result<Option<Legend>, RenderError> {
603 let mut theme = built.theme.clone();
604 let m = theme.legend_margin.clone();
605 let (pa, lp) = match pos {
607 GridLegendPosition::Right => (
608 Rect {
609 x: -m.left,
610 y: -m.top,
611 width: 0.0,
612 height: 0.0,
613 },
614 LegendPosition::Right,
615 ),
616 GridLegendPosition::Bottom => (
617 Rect {
618 x: -m.left,
619 y: -(m.top + 30.0),
620 width: 0.0,
621 height: 0.0,
622 },
623 LegendPosition::Bottom,
624 ),
625 };
626 theme.legend_position = lp;
627 let mut svg = SvgBackend::new(1, 1, pa.clone());
628 let mut meas = Measure::new(&mut svg, pa.clone());
629 legend::draw_legend(
630 &built.scales,
631 &theme,
632 &pa,
633 &mut meas,
634 &built.guide_legend,
635 &built.suppressed_aes,
636 )?;
637 let ext = meas.extent();
638 if svg.body().is_empty() {
639 return Ok(None);
640 }
641 let (x0, y0, x1, y1) = match ext {
642 Some(e) => e,
643 None => return Ok(None),
644 };
645 Ok(Some(Legend {
646 body: svg.body().to_string(),
647 x0,
648 y0,
649 w: (x1 - x0).max(0.0),
650 h: (y1 - y0).max(0.0),
651 }))
652}
653
654fn tracks(weights: &[f64], n: usize, total: f64, gap: f64) -> Vec<(f64, f64)> {
656 let ws: Vec<f64> = (0..n)
657 .map(|i| {
658 if weights.is_empty() {
659 1.0
660 } else {
661 let w = weights[i % weights.len()];
662 if w.is_finite() && w > 0.0 {
663 w
664 } else {
665 1.0
666 }
667 }
668 })
669 .collect();
670 let sum: f64 = ws.iter().sum();
671 let avail = (total - gap * n.saturating_sub(1) as f64).max(0.0);
672 let mut pos = 0.0;
673 ws.iter()
674 .map(|w| {
675 let size = avail * w / sum;
676 let t = (pos, size);
677 pos += size + gap;
678 t
679 })
680 .collect()
681}
682
683fn text_style(el: &crate::theme::elements::ElementText, anchor: TextAnchor) -> TextStyle {
684 TextStyle {
685 color: el.color,
686 size: el.size,
687 anchor,
688 angle: 0.0,
689 family: (!el.family.is_empty()).then(|| el.family.clone()),
690 face: el.face,
691 }
692}
693
694fn hjust_x(el: &crate::theme::elements::ElementText, x0: f64, x1: f64) -> (f64, TextAnchor) {
695 let hj = if el.hjust.is_finite() {
696 el.hjust.clamp(0.0, 1.0)
697 } else {
698 0.0
699 };
700 let anchor = if hj <= 0.02 {
701 TextAnchor::Start
702 } else if hj >= 0.98 {
703 TextAnchor::End
704 } else {
705 TextAnchor::Middle
706 };
707 (x0 + hj * (x1 - x0), anchor)
708}
709
710fn render_prepared(
711 prepared: Prepared,
712 area: &Rect,
713 page: &mut SvgBackend,
714 warnings: &mut Vec<String>,
715) -> Result<(), GGError> {
716 match prepared {
717 Prepared::Spacer => Ok(()),
718 Prepared::Plot(p) => {
719 let PreparedPlot {
720 mut built,
721 meta,
722 panel,
723 } = *p;
724 let (cw, ch) = (
725 area.width.floor().max(1.0) as u32,
726 area.height.floor().max(1.0) as u32,
727 );
728 let layout = GGPlot::layout_built(&mut built, &meta, cw, ch);
729 let mut backend = SvgBackend::new(cw, ch, layout.plot_area.clone());
730 let mut attrs = root_data_attrs(&built);
731 attrs.push(("data-panel".into(), panel.clone()));
732 backend.set_root_attrs(attrs);
733 PlotRenderer::render(&built, &mut backend).map_err(GGError::Render)?;
734 warnings.extend(
735 built
736 .warnings
737 .iter()
738 .cloned()
739 .chain(backend.take_warnings())
740 .map(|w| format!("panel {panel}: {w}")),
741 );
742 page.push_raw(&backend.finish_fragment(area.x, area.y));
743 Ok(())
744 }
745 Prepared::Grid(g) => {
746 let g = *g;
747 let theme = &g.theme;
748 let m = &theme.plot_margin;
749 let mut top = area.y;
751 let (x0, x1) = (area.x + m.left, area.x + area.width - m.right);
752 if let Some(t) = &g.title {
753 top += m.top;
754 let (tx, anchor) = hjust_x(&theme.title, x0, x1);
755 page.draw_text(
756 t,
757 (tx, top + theme.title.size * 0.8),
758 &text_style(&theme.title, anchor),
759 )
760 .map_err(GGError::Render)?;
761 top += theme.title.size * 1.6;
762 }
763 if let Some(s) = &g.subtitle {
764 if g.title.is_none() {
765 top += m.top;
766 }
767 let (sx, anchor) = hjust_x(&theme.subtitle, x0, x1);
768 page.draw_text(
769 s,
770 (sx, top + theme.subtitle.size * 0.7),
771 &text_style(&theme.subtitle, anchor),
772 )
773 .map_err(GGError::Render)?;
774 top += theme.subtitle.size * 1.5;
775 }
776 let mut bottom = area.y + area.height;
777 if let Some(c) = &g.caption {
778 let (cx, anchor) = hjust_x(&theme.caption, x0, x1);
779 bottom -= theme.caption.size * 1.8;
780 page.draw_text(
781 c,
782 (cx, bottom + theme.caption.size * 0.9),
783 &text_style(&theme.caption, anchor),
784 )
785 .map_err(GGError::Render)?;
786 }
787 let body_h = (bottom - top).max(1.0);
788 let cols = tracks(&g.widths, g.ncol, area.width, g.spacing);
789 let rows = tracks(&g.heights, g.nrow, body_h, g.spacing);
790 for (k, cell) in g.cells.into_iter().enumerate() {
791 let (r, c) = if g.byrow {
792 (k / g.ncol, k % g.ncol)
793 } else {
794 (k % g.nrow, k / g.nrow)
795 };
796 if r >= g.nrow || c >= g.ncol {
797 warnings.push(format!(
799 "plot_grid: cell {} does not fit a {}×{} grid; skipped",
800 k + 1,
801 g.nrow,
802 g.ncol
803 ));
804 continue;
805 }
806 let rect = Rect {
807 x: area.x + cols[c].0,
808 y: top + rows[r].0,
809 width: cols[c].1,
810 height: rows[r].1,
811 };
812 render_prepared(cell, &rect, page, warnings)?;
813 }
814 Ok(())
815 }
816 }
817}
818
819struct Measure<'a> {
821 inner: &'a mut SvgBackend,
822 pa: Rect,
823 bbox: Option<(f64, f64, f64, f64)>,
824}
825
826impl<'a> Measure<'a> {
827 fn new(inner: &'a mut SvgBackend, pa: Rect) -> Self {
828 Measure {
829 inner,
830 pa,
831 bbox: None,
832 }
833 }
834
835 fn grow(&mut self, x0: f64, y0: f64, x1: f64, y1: f64) {
836 if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
837 return;
838 }
839 self.bbox = Some(match self.bbox {
840 None => (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)),
841 Some((a, b, c, d)) => (
842 a.min(x0.min(x1)),
843 b.min(y0.min(y1)),
844 c.max(x0.max(x1)),
845 d.max(y0.max(y1)),
846 ),
847 });
848 }
849
850 fn extent(&self) -> Option<(f64, f64, f64, f64)> {
851 self.bbox
852 }
853}
854
855impl DrawBackend for Measure<'_> {
856 fn draw_circle(
857 &mut self,
858 c: (f64, f64),
859 r: f64,
860 style: &PointStyle,
861 ) -> Result<(), RenderError> {
862 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
863 self.inner.draw_circle(c, r, style)
864 }
865 fn draw_line(&mut self, pts: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError> {
866 for p in pts {
867 self.grow(p.0, p.1, p.0, p.1);
868 }
869 self.inner.draw_line(pts, style)
870 }
871 fn draw_rect(
872 &mut self,
873 a: (f64, f64),
874 b: (f64, f64),
875 style: &RectStyle,
876 ) -> Result<(), RenderError> {
877 self.grow(a.0, a.1, b.0, b.1);
878 self.inner.draw_rect(a, b, style)
879 }
880 fn draw_text(
881 &mut self,
882 text: &str,
883 pos: (f64, f64),
884 style: &TextStyle,
885 ) -> Result<(), RenderError> {
886 let w = text.chars().count() as f64 * style.size * 0.6;
887 let (x0, x1) = match style.anchor {
888 TextAnchor::Start => (pos.0, pos.0 + w),
889 TextAnchor::Middle => (pos.0 - w / 2.0, pos.0 + w / 2.0),
890 TextAnchor::End => (pos.0 - w, pos.0),
891 };
892 self.grow(x0, pos.1 - style.size / 2.0, x1, pos.1 + style.size / 2.0);
893 self.inner.draw_text(text, pos, style)
894 }
895 fn draw_polygon(&mut self, pts: &[(f64, f64)], style: &RectStyle) -> Result<(), RenderError> {
896 for p in pts {
897 self.grow(p.0, p.1, p.0, p.1);
898 }
899 self.inner.draw_polygon(pts, style)
900 }
901 fn draw_shape(&mut self, c: (f64, f64), r: f64, style: &PointStyle) -> Result<(), RenderError> {
902 self.grow(c.0 - r, c.1 - r, c.0 + r, c.1 + r);
903 self.inner.draw_shape(c, r, style)
904 }
905 fn plot_area(&self) -> Rect {
906 self.pa.clone()
907 }
908 fn total_area(&self) -> Rect {
909 self.inner.total_area()
910 }
911 fn set_tooltip(&mut self, t: Option<String>) {
912 self.inner.set_tooltip(t)
913 }
914 fn set_mark_axis(&mut self, k: Option<String>) {
915 self.inner.set_mark_axis(k)
916 }
917 fn set_mark_series(&mut self, s: Option<String>) {
918 self.inner.set_mark_series(s)
919 }
920 fn set_mark_value(&mut self, v: Option<String>) {
921 self.inner.set_mark_value(v)
922 }
923 fn warn(&mut self, m: String) {
924 self.inner.warn(m)
925 }
926}
927
928fn join(op: Op, a: GridCell, b: GridCell) -> PlotGrid {
931 let flatten = |c: GridCell| -> Vec<GridCell> {
932 match c {
933 GridCell::Grid(g) if g.op == Some(op) && g.is_plain() => g.cells,
934 other => vec![other],
935 }
936 };
937 let mut cells = flatten(a);
938 cells.extend(flatten(b));
939 PlotGrid {
940 cells,
941 op: Some(op),
942 ..PlotGrid::default()
943 }
944}
945
946impl PlotGrid {
947 fn is_plain(&self) -> bool {
950 self.ncol.is_none()
951 && self.nrow.is_none()
952 && self.widths.is_empty()
953 && self.heights.is_empty()
954 && self.title.is_none()
955 && self.subtitle.is_none()
956 && self.caption.is_none()
957 && self.tags.is_none()
958 && !self.collect_legends
959 }
960}
961
962macro_rules! grid_ops {
963 ($lhs:ty, $rhs:ty) => {
964 impl BitOr<$rhs> for $lhs {
966 type Output = PlotGrid;
967 fn bitor(self, rhs: $rhs) -> PlotGrid {
968 join(Op::Row, self.into(), rhs.into())
969 }
970 }
971 impl Div<$rhs> for $lhs {
973 type Output = PlotGrid;
974 fn div(self, rhs: $rhs) -> PlotGrid {
975 join(Op::Col, self.into(), rhs.into())
976 }
977 }
978 };
979}
980
981grid_ops!(GGPlot, GGPlot);
982grid_ops!(GGPlot, PlotGrid);
983grid_ops!(PlotGrid, GGPlot);
984grid_ops!(PlotGrid, PlotGrid);
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 #[test]
991 fn tag_sequences() {
992 let l: Vec<String> = (0..28).map(|i| TagLevels::Lower.tag(i)).collect();
993 assert_eq!(&l[..3], &["a", "b", "c"]);
994 assert_eq!(l[25], "z");
995 assert_eq!(l[26], "aa");
996 assert_eq!(l[27], "ab");
997 assert_eq!(TagLevels::Upper.tag(1), "B");
998 assert_eq!(TagLevels::Numeric.tag(9), "10");
999 assert_eq!(TagLevels::LowerRoman.tag(3), "iv");
1000 assert_eq!(TagLevels::UpperRoman.tag(13), "XIV");
1001 assert_eq!(TagLevels::Custom(vec!["x".into()]).tag(1), "2");
1002 }
1003
1004 #[test]
1005 fn tracks_are_relative_and_robust() {
1006 let t = tracks(&[2.0, 1.0], 2, 300.0, 0.0);
1007 assert_eq!(t, vec![(0.0, 200.0), (200.0, 100.0)]);
1008 let t = tracks(&[f64::NAN, -1.0], 2, 100.0, 10.0);
1009 assert_eq!(t, vec![(0.0, 45.0), (55.0, 45.0)]);
1010 let t = tracks(&[], 3, 30.0, 100.0);
1011 assert!(t.iter().all(|(p, s)| p.is_finite() && *s == 0.0));
1012 }
1013}