///|
/// Construct a regular subplot grid using this figure's display dimensions.
pub fn Figure::grid_spec(
self : Figure,
rows : Int32,
columns : Int32,
hspace? : Double,
vspace? : Double,
width_ratios~ : Array[Double] = [],
height_ratios~ : Array[Double] = [],
) -> @gridspec.GridSpec raise @gridspec.LayoutError {
ignore(self)
@gridspec.gridspec(
rows,
columns,
hspace?,
vspace?,
width_ratios~,
height_ratios~,
)
}
///|
/// Compute subplot rectangles for a regular grid on this figure.
pub fn Figure::grid_cells(
self : Figure,
rows : Int32,
columns : Int32,
hspace? : Double,
vspace? : Double,
width_ratios~ : Array[Double] = [],
height_ratios~ : Array[Double] = [],
) -> Array[@gridspec.Rect] raise @gridspec.LayoutError {
let grid = self.grid_spec(
rows,
columns,
hspace?,
vspace?,
width_ratios~,
height_ratios~,
)
let result = Array(capacity=rows * columns)
for row in 0.. (Double, Double, Double, Double) {
(self.subplot_left, self.subplot_bottom, self.subplot_right, self.subplot_top)
}
///|
fn finite_layout_value(value : Double) -> Bool {
!value.is_nan() && !value.is_inf()
}
///|
/// Add an axes panel at a normalized Figure rectangle.
///
/// The four arguments use Matplotlib's lower-left origin: `left` and `bottom`
/// locate the panel, while `width` and `height` describe its size. The panel
/// is independent from GridSpec and remains at this position after later
/// `subplots_adjust` calls. Zero width or height is allowed; negative sizes
/// and non-finite coordinates are rejected without changing the figure.
/// Rectangles may extend beyond the figure.
///
/// ```mbt check
/// test {
/// let fig = @scene.figure(width=400, height=300)
/// let axes = try! fig.add_axes(0.1, 0.2, 0.5, 0.4)
/// let rect = axes.rect().unwrap()
/// assert_eq(
/// (rect.x(), rect.y(), rect.width(), rect.height()),
/// (40.0, 120.0, 200.0, 120.0),
/// )
/// }
/// ```
pub fn Figure::add_axes(
self : Figure,
left : Double,
bottom : Double,
width : Double,
height : Double,
) -> Axes raise @gridspec.LayoutError {
let rect = self.position_rect(left, bottom, width, height)
let state = new_axes_state(Some(rect), None)
self.insert_axes_state(state)
{ figure: self, state, }
}
///|
fn Figure::position_rect(
self : Figure,
left : Double,
bottom : Double,
width : Double,
height : Double,
) -> @gridspec.Rect raise @gridspec.LayoutError {
if !finite_layout_value(left) ||
!finite_layout_value(bottom) ||
!finite_layout_value(width) ||
!finite_layout_value(height) ||
!finite_layout_value(left + width) ||
!finite_layout_value(bottom + height) ||
width < 0.0 ||
height < 0.0 {
raise @gridspec.LayoutError::InvalidBounds
}
let figure_width = Double::from_int(self.width)
let figure_height = Double::from_int(self.height)
let rect = @gridspec.rect(
figure_width * left,
figure_height * (1.0 - bottom - height),
figure_width * width,
figure_height * height,
)
if !finite_layout_value(rect.x()) ||
!finite_layout_value(rect.y()) ||
!finite_layout_value(rect.width()) ||
!finite_layout_value(rect.height()) ||
!finite_layout_value(rect.x() + rect.width()) ||
!finite_layout_value(rect.y() + rect.height()) {
raise @gridspec.LayoutError::InvalidBounds
}
rect
}
///|
/// Adjust subplot bounds and spacing, including already-created nested axes.
///
/// Bounds are fractions of the figure width/height, measured from the bottom
/// left. `wspace` controls horizontal gaps and `hspace` vertical gaps, as
/// fractions of the average panel size. Existing grid constructors retain
/// their horizontal `hspace` / vertical `vspace` names. Explicit grid spacing
/// takes precedence over figure spacing. Unspecified values are preserved.
///
/// Bounds must be finite with left < right and bottom < top; they may extend
/// beyond the figure. Spacing must be finite and nonnegative. Invalid calls
/// leave the existing layout untouched. Initial spacing remains 0.1.
///
/// ```mbt check
/// test {
/// let fig = @scene.figure(width=400, height=300)
/// let axes = try! fig.subplots(1, 2)
/// try! fig.subplots_adjust(
/// left=0.25,
/// bottom=0.25,
/// right=0.75,
/// top=0.75,
/// wspace=0.5,
/// )
/// assert_eq(fig.subplot_bounds(), (0.25, 0.25, 0.75, 0.75))
/// match axes[0].rect() {
/// Some(rect) => assert_eq(rect.width(), 80.0)
/// None => fail("expected a subplot rectangle")
/// }
/// }
/// ```
pub fn Figure::subplots_adjust(
self : Figure,
left? : Double,
bottom? : Double,
right? : Double,
top? : Double,
wspace? : Double,
hspace? : Double,
) -> Unit raise @gridspec.LayoutError {
let next_left = left.unwrap_or(self.subplot_left)
let next_bottom = bottom.unwrap_or(self.subplot_bottom)
let next_right = right.unwrap_or(self.subplot_right)
let next_top = top.unwrap_or(self.subplot_top)
if !finite_layout_value(next_left) ||
!finite_layout_value(next_bottom) ||
!finite_layout_value(next_right) ||
!finite_layout_value(next_top) ||
!finite_layout_value(next_right - next_left) ||
!finite_layout_value(next_top - next_bottom) ||
next_left >= next_right ||
next_bottom >= next_top {
raise @gridspec.LayoutError::InvalidBounds
}
for spacing in [wspace, hspace] {
if spacing is Some(value) && (!finite_layout_value(value) || value < 0.0) {
raise @gridspec.LayoutError::InvalidSpacing
}
}
// The convenience figure starts with an implicit single subplot. Attach a
// spec in place so handles and plotted data survive its first adjustment.
for state in self.axes {
if state.val.subplot_spec is None && state.val.rect is None {
state.val.subplot_spec = Some(@gridspec.gridspec(1, 1).cell_spec(0, 0))
}
if state.val.subplot_spec is Some(_) {
state.val.position_override = None
state.val.active_position = None
}
}
self.subplot_left = next_left
self.subplot_bottom = next_bottom
self.subplot_right = next_right
self.subplot_top = next_top
if wspace is Some(_) {
self.subplot_wspace = wspace
}
if hspace is Some(_) {
self.subplot_hspace = hspace
}
}
///|
fn Figure::subplot_rect(
self : Figure,
spec : @gridspec.SubplotSpec,
) -> @gridspec.Rect {
let wspace = self.subplot_wspace
let hspace = self.subplot_hspace
spec.rect_with_bounds(
self.width,
self.height,
self.subplot_left,
self.subplot_bottom,
self.subplot_right,
self.subplot_top,
wspace?,
hspace?,
)
}