///|
/// Function geometry is captured when the owner installs a scale. The stored
/// Linear/Log role supplies its default tickers and limit-range policy.
priv struct AxisFunctionScale {
forward : (Double) -> Double
inverse : (Double) -> Double
logarithmic : Bool
}
///|
/// Install a numeric child function scale without borrowing parent ticker state.
/// The owner separately compares old/new default limits and performs autoscale.
pub fn Axis::configure_function_scale(
self : Axis,
forward : (Double) -> Double,
inverse : (Double) -> Double,
logarithmic~ : Bool = false,
) -> Unit raise @core.DataError {
self.scale = if logarithmic {
@scale.Scale::Log
} else {
@scale.Scale::Linear
}
self.function_scale = Some({ forward, inverse, logarithmic, })
self.install_scale_defaults()
self.mark_scale_defaults()
}
///|
/// Return the installed coordinate context, independent of a live parent scale.
pub fn Axis::coordinate_scale_name(self : Axis) -> String {
match self.function_scale {
Some(context) =>
if context.logarithmic {
"functionlog"
} else {
"function"
}
None =>
match self.scale {
@scale.Linear => "linear"
@scale.Log => "log"
@scale.SymLog(..) => "symlog"
@scale.Asinh(..) => "asinh"
@scale.Logit(..) => "logit"
}
}
}
///|
/// Capture the installed transform; later owner resets cannot change this value.
pub fn Axis::coordinate_transform(self : Axis) -> (Double) -> Double {
match self.function_scale {
Some(context) =>
fn(value) {
let primary = (context.inverse)(value)
if context.logarithmic {
if primary <= 0.0 {
-1000.0
} else {
@scale.Scale::Log.transform(primary)
}
} else {
primary
}
}
None => {
let scale = self.scale
value => scale.transform(value)
}
}
}
///|
/// Functionlog uses the source InvertedLogTransform's exp(x * ln(10)) chain.
pub fn Axis::coordinate_inverse(self : Axis) -> (Double) -> Double {
match self.function_scale {
Some(context) =>
fn(value) {
let primary = if context.logarithmic {
@math.exp(value * @math.ln(10.0))
} else {
value
}
(context.forward)(primary)
}
None => {
let scale = self.scale
value => scale.inverse(value)
}
}
}
///|
/// The finite one-dimensional source tick-location route used by child axes.
/// View expansion is a raw interval operation, before installing FixedLocator;
/// it preserves inversion, auto-enable and pending-request state.
pub fn Axis::set_source_ticks(
self : Axis,
values : Array[Double],
) -> Unit raise @core.DataError {
for value in values {
if value.is_nan() || value.is_inf() {
raise @core.DataError::InvalidTicks(
message="tick positions must be finite",
)
}
}
let locator = @ticker.FixedLocator::new(values.copy())
if !values.is_empty() {
ignore(self.lifecycle_view_interval_checked())
// The first view callback may mutate the caller's original sequence.
// FixedLocator already copied it, but source min(ticks) still reads it now.
if values.is_empty() {
raise @core.DataError::InvalidTicks(
message="min() iterable argument is empty",
)
}
// Python min/max retain the first equal or unordered input, including
// signed zero and a NaN introduced by the preceding callback.
let mut ticks_min = values[0]
let mut ticks_max = values[0]
for value in values {
if value < ticks_min {
ticks_min = value
}
if value > ticks_max {
ticks_max = value
}
}
// The generated interval setter reads its raw Bbox through its captured
// getter, independently of an overridden public get_view_interval method.
let first = self.stored_view.lower()
let last = self.stored_view.upper()
let reversed = !(first < last)
let old_lower = if reversed { last } else { first }
let old_upper = if reversed { first } else { last }
let lower = if old_lower < ticks_min { old_lower } else { ticks_min }
let upper = if old_upper > ticks_max { old_upper } else { ticks_max }
let view = if reversed {
@core.limits(upper, lower)
} else {
@core.limits(lower, upper)
}
self.stored_view = view
self.inverted = view.lower() > view.upper()
if self.autoscale_enabled {
self.limits = None
self.auto_view = Some(view)
} else {
self.limits = Some(view)
self.auto_view = None
}
if self.tick_layout_view is Some(_) {
self.tick_layout_view = Some((view.lower(), view.upper()))
}
// Original _set_tick_locations rereads the view before installing the
// locator. A failing provider here leaves the expanded view and old slot.
ignore(self.lifecycle_view_interval_checked())
}
self.set_locator(locator.as_locator())
}