use crate::plot::legend::{LegendEntry, LegendGroup};
use crate::render::layout::{ComputedLayout, Layout, DEFAULT_FONT_FAMILY};
use crate::render::plots::Plot;
use crate::render::render::{
collect_legend_entries, render_legend_at, render_multiple, render_twin_y, Primitive, Scene,
TextAnchor,
};
#[derive(Debug, Clone)]
pub enum FigureLegendPosition {
Right,
RightTop,
RightMiddle,
RightBottom,
LeftTop,
LeftMiddle,
LeftBottom,
TopLeft,
TopCenter,
TopRight,
Bottom,
BottomLeft,
BottomCenter,
BottomRight,
Custom(f64, f64),
}
#[derive(Debug, Clone)]
pub enum LabelStyle {
Uppercase,
Lowercase,
Numeric,
Custom(Vec<String>),
}
#[derive(Debug, Clone)]
pub struct LabelConfig {
pub style: LabelStyle,
pub size: u32,
pub bold: bool,
}
impl Default for LabelConfig {
fn default() -> Self {
Self {
style: LabelStyle::Uppercase,
size: 16,
bold: true,
}
}
}
impl LabelConfig {
fn label_for(&self, index: usize) -> String {
match &self.style {
LabelStyle::Uppercase => {
let c = (b'A' + index as u8) as char;
c.to_string()
}
LabelStyle::Lowercase => {
let c = (b'a' + index as u8) as char;
c.to_string()
}
LabelStyle::Numeric => (index + 1).to_string(),
LabelStyle::Custom(labels) => labels.get(index).cloned().unwrap_or_default(),
}
}
}
#[derive(Debug, Clone)]
pub enum SharedAxis {
AllRows,
AllColumns,
Row(usize),
Column(usize),
RowSlice {
row: usize,
col_start: usize,
col_end: usize,
},
ColumnSlice {
col: usize,
row_start: usize,
row_end: usize,
},
}
pub struct Figure {
rows: usize,
cols: usize,
structure: Vec<Vec<usize>>,
plots: Vec<Vec<Plot>>,
layouts: Vec<Layout>,
title: Option<String>,
title_size: u32,
labels: Option<LabelConfig>,
shared_x: Vec<SharedAxis>,
shared_y: Vec<SharedAxis>,
spacing: f64,
padding: f64,
cell_width: f64,
cell_height: f64,
figure_width: Option<f64>,
figure_height: Option<f64>,
shared_legend: Option<FigureLegendPosition>,
shared_legend_entries: Option<Vec<LegendEntry>>,
keep_panel_legends: bool,
twin_y_plots: Vec<(usize, Vec<Plot>, Vec<Plot>)>,
row_heights: Vec<Option<f64>>,
col_widths: Vec<Option<f64>>,
}
impl Figure {
pub fn new(rows: usize, cols: usize) -> Self {
let structure: Vec<Vec<usize>> = (0..rows * cols).map(|i| vec![i]).collect();
Self {
rows,
cols,
structure,
plots: Vec::new(),
layouts: Vec::new(),
title: None,
title_size: 20,
labels: None,
shared_x: Vec::new(),
shared_y: Vec::new(),
spacing: 15.0,
padding: 10.0,
cell_width: 500.0,
cell_height: 380.0,
figure_width: None,
figure_height: None,
shared_legend: None,
shared_legend_entries: None,
keep_panel_legends: false,
twin_y_plots: Vec::new(),
row_heights: Vec::new(),
col_widths: Vec::new(),
}
}
pub fn with_title<S: Into<String>>(mut self, title: S) -> Self {
self.title = Some(title.into());
self
}
pub fn with_title_size(mut self, size: u32) -> Self {
self.title_size = size;
self
}
pub fn with_structure(mut self, structure: Vec<Vec<usize>>) -> Self {
self.structure = structure;
self
}
pub fn with_plots(mut self, plots: Vec<Vec<Plot>>) -> Self {
self.plots = plots;
self
}
pub fn with_layouts(mut self, layouts: Vec<Layout>) -> Self {
self.layouts = layouts;
self
}
pub fn with_labels(mut self) -> Self {
self.labels = Some(LabelConfig::default());
self
}
pub fn with_labels_numeric(mut self) -> Self {
self.labels = Some(LabelConfig {
style: LabelStyle::Numeric,
..LabelConfig::default()
});
self
}
pub fn with_labels_lowercase(mut self) -> Self {
self.labels = Some(LabelConfig {
style: LabelStyle::Lowercase,
..LabelConfig::default()
});
self
}
pub fn with_labels_custom(mut self, labels: Vec<&str>, config: LabelConfig) -> Self {
self.labels = Some(LabelConfig {
style: LabelStyle::Custom(labels.into_iter().map(|s| s.to_string()).collect()),
size: config.size,
bold: config.bold,
});
self
}
pub fn with_shared_x_all(mut self) -> Self {
self.shared_x.push(SharedAxis::AllColumns);
self
}
pub fn with_shared_y_all(mut self) -> Self {
self.shared_y.push(SharedAxis::AllRows);
self
}
pub fn with_shared_x(mut self, col: usize) -> Self {
self.shared_x.push(SharedAxis::Column(col));
self
}
pub fn with_shared_y(mut self, row: usize) -> Self {
self.shared_y.push(SharedAxis::Row(row));
self
}
pub fn with_shared_x_slice(mut self, col: usize, row_start: usize, row_end: usize) -> Self {
self.shared_x.push(SharedAxis::ColumnSlice {
col,
row_start,
row_end,
});
self
}
pub fn with_shared_y_slice(mut self, row: usize, col_start: usize, col_end: usize) -> Self {
self.shared_y.push(SharedAxis::RowSlice {
row,
col_start,
col_end,
});
self
}
pub fn with_spacing(mut self, px: f64) -> Self {
self.spacing = px;
self
}
pub fn with_padding(mut self, px: f64) -> Self {
self.padding = px;
self
}
pub fn with_cell_size(mut self, w: f64, h: f64) -> Self {
self.cell_width = w;
self.cell_height = h;
self
}
pub fn with_row_height(mut self, row: usize, height: f64) -> Self {
if self.row_heights.len() <= row {
self.row_heights.resize(row + 1, None);
}
self.row_heights[row] = Some(height);
self
}
pub fn with_col_width(mut self, col: usize, width: f64) -> Self {
if self.col_widths.len() <= col {
self.col_widths.resize(col + 1, None);
}
self.col_widths[col] = Some(width);
self
}
pub fn with_figure_size(mut self, w: f64, h: f64) -> Self {
self.figure_width = Some(w);
self.figure_height = Some(h);
self
}
pub fn with_shared_legend(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::Right);
self
}
pub fn with_shared_legend_bottom(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::Bottom);
self
}
pub fn with_shared_legend_position(mut self, pos: FigureLegendPosition) -> Self {
self.shared_legend = Some(pos);
self
}
pub fn with_shared_legend_at(mut self, x: f64, y: f64) -> Self {
self.shared_legend = Some(FigureLegendPosition::Custom(x, y));
self
}
pub fn with_shared_legend_right_top(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::RightTop);
self
}
pub fn with_shared_legend_right_middle(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::RightMiddle);
self
}
pub fn with_shared_legend_right_bottom(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::RightBottom);
self
}
pub fn with_shared_legend_left_top(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::LeftTop);
self
}
pub fn with_shared_legend_left_middle(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::LeftMiddle);
self
}
pub fn with_shared_legend_left_bottom(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::LeftBottom);
self
}
pub fn with_shared_legend_top_left(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::TopLeft);
self
}
pub fn with_shared_legend_top_center(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::TopCenter);
self
}
pub fn with_shared_legend_top_right(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::TopRight);
self
}
pub fn with_shared_legend_bottom_left(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::BottomLeft);
self
}
pub fn with_shared_legend_bottom_center(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::BottomCenter);
self
}
pub fn with_shared_legend_bottom_right(mut self) -> Self {
self.shared_legend = Some(FigureLegendPosition::BottomRight);
self
}
pub fn with_shared_legend_entries(mut self, entries: Vec<LegendEntry>) -> Self {
self.shared_legend_entries = Some(entries);
self
}
pub fn with_keep_panel_legends(mut self) -> Self {
self.keep_panel_legends = true;
self
}
pub fn with_twin_y_plots(
mut self,
cell_index: usize,
primary: Vec<Plot>,
secondary: Vec<Plot>,
) -> Self {
self.twin_y_plots.push((cell_index, primary, secondary));
self
}
pub fn render(self) -> Scene {
let Figure {
rows,
cols,
structure,
mut plots,
layouts: user_layouts,
title,
title_size,
labels,
shared_x,
shared_y,
spacing,
padding,
mut cell_width,
mut cell_height,
figure_width,
figure_height,
shared_legend,
shared_legend_entries,
keep_panel_legends,
twin_y_plots,
row_heights: explicit_row_heights,
col_widths: explicit_col_widths,
} = self;
let mut twin_y_map: std::collections::HashMap<usize, (Vec<Plot>, Vec<Plot>)> = twin_y_plots
.into_iter()
.map(|(i, p, s)| (i, (p, s)))
.collect();
validate_structure(&structure, rows, cols);
let legend_entries: Option<Vec<LegendEntry>> = if shared_legend.is_some() {
Some(if let Some(manual) = shared_legend_entries {
manual
} else {
let mut all_entries = Vec::new();
let mut seen_labels = std::collections::HashSet::new();
for panel_plots in &plots {
for entry in collect_legend_entries(panel_plots) {
if seen_labels.insert(entry.label.clone()) {
all_entries.push(entry);
}
}
}
for (primary, secondary) in twin_y_map.values() {
for entry in collect_legend_entries(primary)
.into_iter()
.chain(collect_legend_entries(secondary))
{
if seen_labels.insert(entry.label.clone()) {
all_entries.push(entry);
}
}
}
all_entries
})
} else {
None
};
let legend_spacing = 20.0;
let (legend_width, legend_height) = if let Some(ref entries) = legend_entries {
if entries.is_empty() {
(0.0, 0.0)
} else {
let max_label_len = entries.iter().map(|e| e.label.len()).max().unwrap_or(0);
let w = (max_label_len as f64 * 7.0 + 35.0).max(80.0);
let h = entries.len() as f64 * 18.0 + 20.0;
(w, h)
}
} else {
(0.0, 0.0)
};
let has_entries = legend_entries.as_ref().is_some_and(|e| !e.is_empty());
let legend_on_right = has_entries
&& matches!(
shared_legend.as_ref(),
Some(
FigureLegendPosition::Right
| FigureLegendPosition::RightTop
| FigureLegendPosition::RightMiddle
| FigureLegendPosition::RightBottom
)
);
let legend_on_left = has_entries
&& matches!(
shared_legend.as_ref(),
Some(
FigureLegendPosition::LeftTop
| FigureLegendPosition::LeftMiddle
| FigureLegendPosition::LeftBottom
)
);
let legend_on_top = has_entries
&& matches!(
shared_legend.as_ref(),
Some(
FigureLegendPosition::TopLeft
| FigureLegendPosition::TopCenter
| FigureLegendPosition::TopRight
)
);
let legend_on_bottom = has_entries
&& matches!(
shared_legend.as_ref(),
Some(
FigureLegendPosition::Bottom
| FigureLegendPosition::BottomLeft
| FigureLegendPosition::BottomCenter
| FigureLegendPosition::BottomRight
)
);
if let (Some(fw), Some(fh)) = (figure_width, figure_height) {
let legend_w_used = if legend_on_right || legend_on_left {
legend_width + legend_spacing
} else {
0.0
};
let legend_h_used = if legend_on_top || legend_on_bottom {
legend_height + legend_spacing
} else {
0.0
};
let title_h = if title.is_some() { 30.0 } else { 0.0 };
let explicit_col_total: f64 = (0..cols)
.filter_map(|c| explicit_col_widths.get(c).copied().flatten())
.sum();
let free_cols = (0..cols)
.filter(|&c| explicit_col_widths.get(c).copied().flatten().is_none())
.count();
if free_cols > 0 {
cell_width = ((fw
- legend_w_used
- 2.0 * padding
- (cols as f64 - 1.0) * spacing
- explicit_col_total)
/ free_cols as f64)
.max(1.0);
}
let explicit_row_total: f64 = (0..rows)
.filter_map(|r| explicit_row_heights.get(r).copied().flatten())
.sum();
let free_rows = (0..rows)
.filter(|&r| explicit_row_heights.get(r).copied().flatten().is_none())
.count();
if free_rows > 0 {
cell_height = ((fh
- legend_h_used
- 2.0 * padding
- (rows as f64 - 1.0) * spacing
- title_h
- explicit_row_total)
/ free_rows as f64)
.max(1.0);
}
}
let figure_title_height = if title.is_some() { 30.0 } else { 0.0 };
let mut layouts: Vec<Layout> = Vec::new();
for i in 0..structure.len() {
let layout = if i < user_layouts.len() {
clone_layout(&user_layouts[i])
} else if let Some((primary, secondary)) = twin_y_map.get(&i) {
Layout::auto_from_twin_y_plots(primary, secondary)
} else if i < plots.len() && !plots[i].is_empty() {
Layout::auto_from_plots(&plots[i])
} else {
Layout::new((0.0, 1.0), (0.0, 1.0))
};
layouts.push(layout);
}
apply_shared_axes(&structure, &shared_y, &shared_x, &mut layouts, rows, cols);
if shared_legend.is_some() && !keep_panel_legends {
for layout in layouts.iter_mut() {
layout.show_legend = false;
}
}
let mut per_row_heights: Vec<f64> = vec![cell_height; rows];
for (i, group) in structure.iter().enumerate() {
let rect = cell_rect(group, cols);
let grid_row = rect.0;
if i < plots.len() && i < layouts.len() {
for plot in &plots[i] {
if let Plot::Brick(bp) = plot {
if let Some(rh) = bp.row_height_px {
let n = bp.num_rows();
if n > 0 {
let cl = ComputedLayout::from_layout(&layouts[i]);
let overhead = cl.margin_top + cl.margin_bottom;
let desired = rh * n as f64 + overhead;
per_row_heights[grid_row] = desired;
}
}
}
}
}
}
for (r, rh) in explicit_row_heights.iter().enumerate() {
if let Some(h) = rh {
if r < rows {
per_row_heights[r] = *h;
}
}
}
let mut per_col_widths: Vec<f64> = vec![cell_width; cols];
for (c, cw) in explicit_col_widths.iter().enumerate() {
if let Some(w) = cw {
if c < cols {
per_col_widths[c] = *w;
}
}
}
let row_y_starts: Vec<f64> = {
let mut starts = vec![0.0f64; rows + 1];
for r in 0..rows {
starts[r + 1] = starts[r] + per_row_heights[r] + spacing;
}
starts
};
let col_x_starts: Vec<f64> = {
let mut starts = vec![0.0f64; cols + 1];
for c in 0..cols {
starts[c + 1] = starts[c] + per_col_widths[c] + spacing;
}
starts
};
let grid_width =
per_col_widths.iter().sum::<f64>() + (cols as f64 - 1.0) * spacing + 2.0 * padding;
let grid_height = per_row_heights.iter().sum::<f64>()
+ (rows as f64 - 1.0) * spacing
+ 2.0 * padding
+ figure_title_height;
let cell_x_offset = if legend_on_left {
legend_width + legend_spacing
} else {
0.0
};
let cell_y_offset = if legend_on_top {
legend_height + legend_spacing
} else {
0.0
};
let total_width = grid_width
+ if legend_on_right || legend_on_left {
legend_width + legend_spacing
} else {
0.0
};
let total_height = grid_height
+ if legend_on_top || legend_on_bottom {
legend_height + legend_spacing
} else {
0.0
};
let mut master = Scene::new(total_width, total_height);
let figure_theme = user_layouts
.first()
.map(|l| l.theme.clone())
.unwrap_or_default();
master.font_family = user_layouts
.first()
.and_then(|l| l.font_family.clone())
.or(figure_theme.font_family.clone())
.or(Some(DEFAULT_FONT_FAMILY.to_string()));
master.background_color = Some(figure_theme.background.clone());
master.text_color = Some(figure_theme.text_color.clone());
while plots.len() < structure.len() {
plots.push(Vec::new());
}
for (i, group) in structure.iter().enumerate() {
let rect = cell_rect(group, cols);
let col_span = rect.3 - rect.1 + 1;
let row_span = rect.2 - rect.0 + 1;
let cell_x = cell_x_offset + padding + col_x_starts[rect.1];
let cell_y = cell_y_offset + padding + figure_title_height + row_y_starts[rect.0];
let cell_w = (rect.1..rect.1 + col_span)
.map(|c| per_col_widths[c])
.sum::<f64>()
+ (col_span as f64 - 1.0) * spacing;
let cell_h = (rect.0..rect.0 + row_span)
.map(|r| per_row_heights[r])
.sum::<f64>()
+ (row_span as f64 - 1.0) * spacing;
let slot_plots = std::mem::take(&mut plots[i]);
let cell_scene_opt = if let Some((primary, secondary)) = twin_y_map.remove(&i) {
let mut layout = clone_layout(&layouts[i]);
layout.width = Some(cell_w);
layout.height = Some(cell_h);
Some(render_twin_y(primary, secondary, layout))
} else if !slot_plots.is_empty() {
let mut layout = clone_layout(&layouts[i]);
layout.width = Some(cell_w);
layout.height = Some(cell_h);
Some(render_multiple(slot_plots, layout))
} else {
None
};
if let Some(cell_scene) = cell_scene_opt {
for def in cell_scene.defs {
master.defs.push(def);
}
master.add(Primitive::GroupStart {
transform: Some(format!("translate({cell_x},{cell_y})")),
title: None,
extra_attrs: None,
});
for elem in cell_scene.elements {
master.add(elem);
}
master.add(Primitive::GroupEnd);
}
if let Some(ref config) = labels {
let label = config.label_for(i);
master.add(Primitive::Text {
x: cell_x + 8.0,
y: cell_y + config.size as f64 + 2.0,
content: label,
size: config.size,
anchor: TextAnchor::Start,
rotate: None,
bold: config.bold,
color: None,
});
}
}
if let Some(title) = title {
master.add(Primitive::Text {
x: total_width / 2.0,
y: 22.0,
content: title,
size: title_size,
anchor: TextAnchor::Middle,
rotate: None,
bold: false,
color: None,
});
}
if let (Some(ref pos), Some(ref entries)) = (&shared_legend, &legend_entries) {
if !entries.is_empty() {
let inner_right = cell_x_offset + grid_width;
let inner_bottom = cell_y_offset + grid_height;
let grid_mid_y = cell_y_offset
+ figure_title_height
+ padding
+ (grid_height - figure_title_height - 2.0 * padding) / 2.0;
let (lx, ly) = match pos {
FigureLegendPosition::Right | FigureLegendPosition::RightMiddle => (
inner_right + legend_spacing / 2.0,
grid_mid_y - legend_height / 2.0,
),
FigureLegendPosition::RightTop => (
inner_right + legend_spacing / 2.0,
cell_y_offset + figure_title_height + padding,
),
FigureLegendPosition::RightBottom => (
inner_right + legend_spacing / 2.0,
inner_bottom - padding - legend_height,
),
FigureLegendPosition::LeftMiddle => {
(legend_spacing / 2.0, grid_mid_y - legend_height / 2.0)
}
FigureLegendPosition::LeftTop => (
legend_spacing / 2.0,
cell_y_offset + figure_title_height + padding,
),
FigureLegendPosition::LeftBottom => {
(legend_spacing / 2.0, inner_bottom - padding - legend_height)
}
FigureLegendPosition::TopLeft => (
cell_x_offset + padding,
figure_title_height + legend_spacing / 2.0,
),
FigureLegendPosition::TopCenter => (
cell_x_offset + grid_width / 2.0 - legend_width / 2.0,
figure_title_height + legend_spacing / 2.0,
),
FigureLegendPosition::TopRight => (
cell_x_offset + grid_width - padding - legend_width,
figure_title_height + legend_spacing / 2.0,
),
FigureLegendPosition::Bottom | FigureLegendPosition::BottomCenter => (
cell_x_offset + grid_width / 2.0 - legend_width / 2.0,
inner_bottom + legend_spacing / 2.0,
),
FigureLegendPosition::BottomLeft => {
(cell_x_offset + padding, inner_bottom + legend_spacing / 2.0)
}
FigureLegendPosition::BottomRight => (
cell_x_offset + grid_width - padding - legend_width,
inner_bottom + legend_spacing / 2.0,
),
FigureLegendPosition::Custom(x, y) => (*x, *y),
};
let body_size = user_layouts.first().map_or(12, |l| l.body_size);
render_legend_at(
entries,
None::<&[LegendGroup]>,
None,
true,
&mut master,
lx,
ly,
legend_width,
body_size,
&figure_theme,
);
}
}
master
}
}
fn clone_layout(l: &Layout) -> Layout {
let mut new = Layout::new(l.x_range, l.y_range);
new.width = l.width;
new.height = l.height;
new.data_x_range = l.data_x_range;
new.data_y_range = l.data_y_range;
new.ticks = l.ticks;
new.show_grid = l.show_grid;
new.x_label = l.x_label.clone();
new.y_label = l.y_label.clone();
new.title = l.title.clone();
new.x_categories = l.x_categories.clone();
new.y_categories = l.y_categories.clone();
new.show_legend = l.show_legend;
new.show_colorbar = l.show_colorbar;
new.legend_position = l.legend_position;
new.legend_width = l.legend_width;
new.legend_entries = l.legend_entries.clone();
new.legend_title = l.legend_title.clone();
new.legend_groups = l.legend_groups.clone();
new.legend_box = l.legend_box;
new.legend_height = l.legend_height;
new.stats_entries = l.stats_entries.clone();
new.stats_title = l.stats_title.clone();
new.stats_position = l.stats_position;
new.stats_box = l.stats_box;
new.log_x = l.log_x;
new.log_y = l.log_y;
new.annotations = l.annotations.clone();
new.reference_lines = l.reference_lines.clone();
new.shaded_regions = l.shaded_regions.clone();
new.suppress_x_ticks = l.suppress_x_ticks;
new.suppress_y_ticks = l.suppress_y_ticks;
new.font_family = l.font_family.clone();
new.title_size = l.title_size;
new.label_size = l.label_size;
new.tick_size = l.tick_size;
new.body_size = l.body_size;
new.axis_line_width = l.axis_line_width;
new.tick_width = l.tick_width;
new.tick_length = l.tick_length;
new.grid_line_width = l.grid_line_width;
new.theme = l.theme.clone();
new.palette = None; new.x_tick_format = l.x_tick_format.clone();
new.y_tick_format = l.y_tick_format.clone();
new.colorbar_tick_format = l.colorbar_tick_format.clone();
new.y2_range = l.y2_range;
new.data_y2_range = l.data_y2_range;
new.y2_label = l.y2_label.clone();
new.log_y2 = l.log_y2;
new.y2_tick_format = l.y2_tick_format.clone();
new.suppress_y2_ticks = l.suppress_y2_ticks;
new.x_axis_min = l.x_axis_min;
new.x_axis_max = l.x_axis_max;
new.y_axis_min = l.y_axis_min;
new.y_axis_max = l.y_axis_max;
new.x_datetime = l.x_datetime.clone();
new.y_datetime = l.y_datetime.clone();
new.x_tick_rotate = l.x_tick_rotate;
new.clamp_axis = l.clamp_axis;
new.clamp_y_axis = l.clamp_y_axis;
new.x_bin_width = l.x_bin_width;
new.term_rows = l.term_rows;
new.x_tick_step = l.x_tick_step;
new.y_tick_step = l.y_tick_step;
new.minor_ticks = l.minor_ticks;
new.show_minor_grid = l.show_minor_grid;
new.x_label_offset = l.x_label_offset;
new.y_label_offset = l.y_label_offset;
new.y2_label_offset = l.y2_label_offset;
new.scale = l.scale;
new.polar_r_label_angle = l.polar_r_label_angle;
new.interactive = l.interactive;
new.equal_aspect = l.equal_aspect;
new.brick_notation_tiers = l.brick_notation_tiers;
new.title_wrap = l.title_wrap;
new.x_label_wrap = l.x_label_wrap;
new.y_label_wrap = l.y_label_wrap;
new.y2_label_wrap = l.y2_label_wrap;
new.legend_wrap = l.legend_wrap;
new.horizon_right_annot_px = l.horizon_right_annot_px;
new.gantt_right_annot_px = l.gantt_right_annot_px;
new
}
fn cell_rect(group: &[usize], cols: usize) -> (usize, usize, usize, usize) {
let mut min_row = usize::MAX;
let mut max_row = 0;
let mut min_col = usize::MAX;
let mut max_col = 0;
for &idx in group {
let row = idx / cols;
let col = idx % cols;
min_row = min_row.min(row);
max_row = max_row.max(row);
min_col = min_col.min(col);
max_col = max_col.max(col);
}
(min_row, min_col, max_row, max_col)
}
fn validate_structure(structure: &[Vec<usize>], rows: usize, cols: usize) {
let total_cells = rows * cols;
let mut seen = vec![false; total_cells];
for (group_idx, group) in structure.iter().enumerate() {
assert!(
!group.is_empty(),
"Figure structure: group {group_idx} is empty"
);
for &idx in group {
assert!(
idx < total_cells,
"Figure structure: cell index {idx} out of bounds (grid is {rows}x{cols} = {total} cells)",
total = total_cells,
);
assert!(
!seen[idx],
"Figure structure: cell index {idx} appears in multiple groups"
);
seen[idx] = true;
}
let (min_row, min_col, max_row, max_col) = cell_rect(group, cols);
let expected_count = (max_row - min_row + 1) * (max_col - min_col + 1);
assert!(
group.len() == expected_count,
"Figure structure: group {group_idx} is not a filled rectangle \
(has {} cells, expected {expected_count} for rows {min_row}..={max_row}, cols {min_col}..={max_col})",
group.len(),
);
for r in min_row..=max_row {
for c in min_col..=max_col {
let cell = r * cols + c;
assert!(
group.contains(&cell),
"Figure structure: group {group_idx} missing cell {cell} \
(row {r}, col {c}) — groups must be filled rectangles"
);
}
}
}
}
fn subplot_grid_pos(
structure: &[Vec<usize>],
subplot_idx: usize,
cols: usize,
) -> Option<(usize, usize)> {
if subplot_idx >= structure.len() {
return None;
}
let group = &structure[subplot_idx];
if group.is_empty() {
return None;
}
let (min_row, min_col, _, _) = cell_rect(group, cols);
Some((min_row, min_col))
}
fn apply_shared_axes(
structure: &[Vec<usize>],
shared_y_rules: &[SharedAxis],
shared_x_rules: &[SharedAxis],
layouts: &mut [Layout],
_rows: usize,
cols: usize,
) {
for rule in shared_y_rules {
let groups = matching_groups_for_shared_y(structure, rule, cols);
if groups.len() < 2 {
continue;
}
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &idx in &groups {
if idx < layouts.len() {
y_min = y_min.min(layouts[idx].y_range.0);
y_max = y_max.max(layouts[idx].y_range.1);
}
}
let leftmost_col = groups
.iter()
.filter_map(|&idx| subplot_grid_pos(structure, idx, cols))
.map(|(_, col)| col)
.min()
.unwrap_or(0);
for &idx in &groups {
if idx < layouts.len() {
layouts[idx].y_range = (y_min, y_max);
if let Some((_, col)) = subplot_grid_pos(structure, idx, cols) {
if col != leftmost_col {
layouts[idx].suppress_y_ticks = true;
layouts[idx].y_label = None;
}
}
}
}
}
for rule in shared_x_rules {
let groups = matching_groups_for_shared_x(structure, rule, cols);
if groups.len() < 2 {
continue;
}
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
for &idx in &groups {
if idx < layouts.len() {
x_min = x_min.min(layouts[idx].x_range.0);
x_max = x_max.max(layouts[idx].x_range.1);
}
}
let bottommost_row = groups
.iter()
.filter_map(|&idx| subplot_grid_pos(structure, idx, cols))
.map(|(row, _)| row)
.max()
.unwrap_or(0);
for &idx in &groups {
if idx < layouts.len() {
layouts[idx].x_range = (x_min, x_max);
if let Some((row, _)) = subplot_grid_pos(structure, idx, cols) {
if row != bottommost_row {
layouts[idx].suppress_x_ticks = true;
layouts[idx].x_label = None;
}
}
}
}
}
}
fn matching_groups_for_shared_y(
structure: &[Vec<usize>],
rule: &SharedAxis,
cols: usize,
) -> Vec<usize> {
let mut result = Vec::new();
for (idx, _) in structure.iter().enumerate() {
if let Some((row, col)) = subplot_grid_pos(structure, idx, cols) {
let matches = match rule {
SharedAxis::AllRows => true,
SharedAxis::Row(r) => row == *r,
SharedAxis::RowSlice {
row: r,
col_start,
col_end,
} => row == *r && col >= *col_start && col <= *col_end,
_ => false,
};
if matches {
result.push(idx);
}
}
}
result
}
fn matching_groups_for_shared_x(
structure: &[Vec<usize>],
rule: &SharedAxis,
cols: usize,
) -> Vec<usize> {
let mut result = Vec::new();
for (idx, _) in structure.iter().enumerate() {
if let Some((row, col)) = subplot_grid_pos(structure, idx, cols) {
let matches = match rule {
SharedAxis::AllColumns => true,
SharedAxis::Column(c) => col == *c,
SharedAxis::ColumnSlice {
col: c,
row_start,
row_end,
} => col == *c && row >= *row_start && row <= *row_end,
_ => false,
};
if matches {
result.push(idx);
}
}
}
result
}