use crate::figure::subplotspec::SubplotSpec;
#[derive(Debug, Clone, PartialEq)]
pub struct GridSpec {
pub nrows: usize,
pub ncols: usize,
pub left: f64,
pub right: f64,
pub bottom: f64,
pub top: f64,
pub wspace: f64,
pub hspace: f64,
pub width_ratios: Option<Vec<f64>>,
pub height_ratios: Option<Vec<f64>>,
}
impl GridSpec {
#[must_use]
pub fn new(nrows: usize, ncols: usize) -> Self {
assert!(nrows > 0, "nrows must be greater than zero");
assert!(ncols > 0, "ncols must be greater than zero");
Self {
nrows,
ncols,
left: 0.125,
right: 0.9,
bottom: 0.11,
top: 0.88,
wspace: 0.2,
hspace: 0.2,
width_ratios: None,
height_ratios: None,
}
}
#[must_use]
pub fn with_margins(mut self, left: f64, right: f64, bottom: f64, top: f64) -> Self {
self.left = left;
self.right = right;
self.bottom = bottom;
self.top = top;
self
}
#[must_use]
pub fn with_spacing(mut self, wspace: f64, hspace: f64) -> Self {
self.wspace = wspace;
self.hspace = hspace;
self
}
#[must_use]
pub fn with_width_ratios(mut self, ratios: Vec<f64>) -> Self {
assert_eq!(
ratios.len(),
self.ncols,
"width_ratios length must equal ncols"
);
self.width_ratios = Some(ratios);
self
}
#[must_use]
pub fn with_height_ratios(mut self, ratios: Vec<f64>) -> Self {
assert_eq!(
ratios.len(),
self.nrows,
"height_ratios length must equal nrows"
);
self.height_ratios = Some(ratios);
self
}
#[must_use]
pub fn get_grid_positions(&self) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
let (fig_tops, fig_bottoms) = positions_1d(
self.nrows,
self.hspace,
self.height_ratios.as_deref(),
self.bottom,
self.top,
true,
);
let (fig_lefts, fig_rights) = positions_1d(
self.ncols,
self.wspace,
self.width_ratios.as_deref(),
self.left,
self.right,
false,
);
(fig_bottoms, fig_tops, fig_lefts, fig_rights)
}
#[must_use]
pub fn subplot(&self, row: usize, col: usize) -> SubplotSpec {
assert!(row < self.nrows, "row out of range");
assert!(col < self.ncols, "col out of range");
SubplotSpec {
rows: (row, row + 1),
cols: (col, col + 1),
}
}
#[must_use]
pub fn subplot_span(
&self,
rows: std::ops::Range<usize>,
cols: std::ops::Range<usize>,
) -> SubplotSpec {
assert!(rows.start < rows.end, "row range must be non-empty");
assert!(cols.start < cols.end, "col range must be non-empty");
assert!(rows.end <= self.nrows, "row range out of bounds");
assert!(cols.end <= self.ncols, "col range out of bounds");
SubplotSpec {
rows: (rows.start, rows.end),
cols: (cols.start, cols.end),
}
}
}
fn positions_1d(
n: usize,
space: f64,
ratios: Option<&[f64]>,
lo: f64,
hi: f64,
descending: bool,
) -> (Vec<f64>, Vec<f64>) {
let nf = n as f64;
let tot = hi - lo;
let cell = tot / (nf + space * (nf - 1.0));
let sep = space * cell;
let equal = vec![1.0; n];
let ratios = ratios.unwrap_or(&equal);
let sum_ratios: f64 = ratios.iter().sum();
let norm = cell * nf / sum_ratios;
let cell_sizes: Vec<f64> = ratios.iter().map(|r| r * norm).collect();
let mut near = Vec::with_capacity(n);
let mut far = Vec::with_capacity(n);
let mut acc = 0.0;
for (i, size) in cell_sizes.iter().enumerate() {
if i > 0 {
acc += sep;
}
let start = acc;
acc += size;
let end = acc;
if descending {
near.push(hi - start);
far.push(hi - end);
} else {
near.push(lo + start);
far.push(lo + end);
}
}
(near, far)
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-12, "expected {b}, got {a}");
}
#[test]
fn single_cell_fills_inner_rectangle() {
let gs = GridSpec::new(1, 1);
let (bottoms, tops, lefts, rights) = gs.get_grid_positions();
assert_eq!(bottoms.len(), 1);
approx(lefts[0], gs.left);
approx(rights[0], gs.right);
approx(bottoms[0], gs.bottom);
approx(tops[0], gs.top);
}
#[test]
fn two_by_two_ordering_and_gaps() {
let gs = GridSpec::new(2, 2);
let (bottoms, tops, lefts, rights) = gs.get_grid_positions();
assert!(tops[0] > tops[1]);
assert!(bottoms[0] > bottoms[1]);
assert!(lefts[1] > lefts[0]);
assert!(rights[1] > rights[0]);
let row_gap = bottoms[0] - tops[1];
let col_gap = lefts[1] - rights[0];
assert!(row_gap > 0.0, "rows must not overlap");
assert!(col_gap > 0.0, "cols must not overlap");
let cell_h = tops[0] - bottoms[0];
let cell_w = rights[0] - lefts[0];
approx(row_gap, gs.hspace * cell_h);
approx(col_gap, gs.wspace * cell_w);
approx(tops[0], gs.top);
approx(bottoms[1], gs.bottom);
approx(lefts[0], gs.left);
approx(rights[1], gs.right);
}
#[test]
fn width_ratios_scale_columns() {
let gs = GridSpec::new(1, 2).with_width_ratios(vec![2.0, 1.0]);
let (_, _, lefts, rights) = gs.get_grid_positions();
let left_w = rights[0] - lefts[0];
let right_w = rights[1] - lefts[1];
approx(left_w, 2.0 * right_w);
}
#[test]
fn spanning_whole_grid_is_inner_rectangle() {
let gs = GridSpec::new(3, 4);
let span = gs.subplot_span(0..3, 0..4);
let bbox = span.get_position(&gs);
approx(bbox.xmin(), gs.left);
approx(bbox.xmax(), gs.right);
approx(bbox.ymin(), gs.bottom);
approx(bbox.ymax(), gs.top);
}
}