///|
fn validate_adjustable_name(mode : String) -> Unit raise @core.DataError {
if mode != "box" && mode != "datalim" {
raise @core.DataError::InvalidReference(
message="adjustable must be box or datalim",
)
}
}
///|
fn Axes::adjustable_targets(self : Axes, share : Bool) -> Array[Axes] {
if !share {
return [self]
}
let result = self
.x_group_states()
.map(state => { figure: self.figure, state, })
for state in self.y_group_states() {
if !result.any(axes => physical_equal(axes.state, state)) {
result.push({ figure: self.figure, state, })
}
}
result
}
///|
fn Axes::validate_adjustable(
self : Axes,
mode : String,
share : Bool,
fixed_aspect~ : Bool = false,
) -> Unit raise @core.DataError {
validate_adjustable_name(mode)
if mode == "datalim" {
for axes in self.adjustable_targets(share) {
if (fixed_aspect || axes.state.val.data_aspect is Some(_)) &&
axes.x_group_states().length() > 1 &&
axes.y_group_states().length() > 1 {
raise @core.DataError::InvalidReference(
message="datalim aspect cannot adjust axes sharing both x and y",
)
}
}
}
}
///|
fn Axes::apply_adjustable(self : Axes, mode : String, share : Bool) -> Unit {
for axes in self.adjustable_targets(share) {
axes.state.val.adjustable = mode
}
}
///|
/// Choose whether fixed aspect shrinks the box or changes data limits.
/// mode is "box" (default) or "datalim". share updates direct x/y shared
/// siblings. datalim may overwrite fixed limits while retaining their direction
/// and automatic/explicit selection. Numeric two-axis sharing is rejected here, before
/// any changes, rather than failing during a later non-throwing draw.
/// clear retains the setting; new axes start with box adjustment.
///
/// ```mbt check
/// test {
/// let ax = @scene.figure().axes()
/// try! ax.set_adjustable("datalim")
/// assert_eq(ax.get_adjustable(), "datalim")
/// ax.clear()
/// assert_eq(ax.get_adjustable(), "datalim")
/// }
/// ```
pub fn Axes::set_adjustable(
self : Axes,
mode : String,
share~ : Bool = false,
) -> Unit raise @core.DataError {
self.validate_adjustable(mode, share)
self.apply_adjustable(mode, share)
}
///|
/// Return "box" or "datalim" for the aspect adjustment strategy.
///
/// ```mbt check
/// test {
/// assert_eq(@scene.figure().axes().get_adjustable(), "box")
/// }
/// ```
pub fn Axes::get_adjustable(self : Axes) -> String {
self.state.val.adjustable
}
///|
/// Set the first/default panel's adjustment strategy, preserving gca selection.
///
/// ```mbt check
/// test {
/// let fig = @scene.figure()
/// try! fig.set_adjustable("datalim")
/// assert_eq(fig.axes().get_adjustable(), "datalim")
/// }
/// ```
pub fn Figure::set_adjustable(
self : Figure,
mode : String,
share~ : Bool = false,
) -> Unit raise @core.DataError {
validate_adjustable_name(mode)
self.axes().set_adjustable(mode, share~)
}
///|
/// Return the first/default panel's adjustment strategy.
///
/// ```mbt check
/// test {
/// assert_eq(@scene.figure().get_adjustable(), "box")
/// }
/// ```
pub fn Figure::get_adjustable(self : Figure) -> String {
self.axes().get_adjustable()
}
///|
fn Axes::commit_aspect_view(
self : Axes,
view : ((Double, Double, Double, Double), @core.Limits, @core.Limits),
) -> Unit {
if self.state.val.adjustable == "datalim" &&
self.state.val.data_aspect is Some(_) {
// panel_view validated the endpoints against these same axis scales.
try! self.state.val.x_axis.set_view_limits(view.1.lower(), view.1.upper())
try! self.state.val.y_axis.set_view_limits(view.2.lower(), view.2.upper())
}
let (left, top, width, height) = view.0
self.state.val.active_position = Some(
@gridspec.rect(left, top, width, height),
)
}
///|
/// Apply aspect immediately, updating active position and datalim views.
/// Detached handles or unrepresentable transformed ranges raise DataError.
/// In datalim mode x_limits/y_limits expose the resolved view after this call.
///
/// ```mbt check
/// test {
/// let fig = @scene.figure(width=400, height=200)
/// let ax = try! fig.add_axes(0.0, 0.0, 1.0, 1.0)
/// try! ax.plot([0.0, 1.0], [0.0, 1.0])
/// try! ax.set_xlim(0.0, 1.0)
/// try! ax.set_ylim(0.0, 1.0)
/// try! ax.set_aspect(1.0, adjustable="datalim")
/// try! ax.apply_aspect()
/// assert_eq(ax.get_xlim().unwrap().lower(), -0.5)
/// assert_eq(ax.get_position().unwrap().width(), 1.0)
/// }
/// ```
pub fn Axes::apply_aspect(self : Axes) -> Unit raise @core.DataError {
if !self.is_attached() {
raise @core.DataError::InvalidReference(
message="cannot apply aspect to detached axes",
)
}
if self.state.val.data_aspect is None && self.state.val.box_aspect is None {
self.state.val.active_position = Some(self.original_position_rect())
return
}
self.commit_aspect_view(self.resolve_aspect_view())
}
///|
fn Axes::resolve_aspect_view(
self : Axes,
) -> ((Double, Double, Double, Double), @core.Limits, @core.Limits) raise @core.DataError {
for index, state in self.figure.axes {
if physical_equal(state, self.state) {
return @svg.panel_view(self.figure.to_render_figure(), index).unwrap()
}
}
raise @core.DataError::InvalidReference(
message="cannot resolve aspect for detached axes",
)
}
///|
fn Figure::apply_position_aspects(self : Figure) -> Unit raise @core.DataError {
if !self.axes.any(state => {
state.val.box_aspect is Some(_) ||
(state.val.adjustable == "datalim" && state.val.data_aspect is Some(_))
}) {
return
}
let figure = self.to_render_figure()
let views = []
for index, state in self.axes {
if state.val.box_aspect is Some(_) ||
(state.val.adjustable == "datalim" && state.val.data_aspect is Some(_)) {
let view = @svg.panel_view(figure, index).unwrap()
views.push(({ figure: self, state, }, view))
}
}
// Resolution precedes every write, including when a later panel fails.
for (axes, view) in views {
axes.commit_aspect_view(view)
}
}