// Adapted from Matplotlib v3.11.2 ScalarFormatter; see docs/licenses/LICENSE-MATPLOTLIB
// and docs/THIRD_PARTY_NOTICES.md. Python state and NumPy operations are replaced
// by an immutable layout computed for each MoonBit tick list and viewport.
///|
priv struct ScalarLayout {
offset : Double
exponent : Int32
precision : Int32
}
///|
fn scalar_layout(
values : Array[Double],
view : @core.Limits?,
use_offset : Bool,
power_min : Int32,
power_max : Int32,
) -> ScalarLayout {
let (lower, upper) = formatter_extent(values, view)
let visible = values.filter(value => {
!value.is_nan() && !value.is_inf() && value >= lower && value <= upper
})
let offset = if use_offset { scalar_offset(visible) } else { 0.0 }
let exponent = scalar_order(
visible,
upper - lower,
offset,
power_min,
power_max,
)
let scaled = values
.filter(value => !value.is_nan() && !value.is_inf())
.map(value => divide_decimal(value - offset, exponent))
let span_values = scaled.copy()
if scaled.length() < 2 {
span_values.push(divide_decimal(lower - offset, exponent))
span_values.push(divide_decimal(upper - offset, exponent))
}
let precision = scalar_precision(scaled, span_values)
{ offset, exponent, precision, }
}
///|
/// A finite viewport or, when none is supplied, the finite tick extent.
fn formatter_extent(
values : Array[Double],
view : @core.Limits?,
) -> (Double, Double) {
if view is Some(limits) && limits.is_finite() {
return (
Double::min(limits.lower(), limits.upper()),
Double::max(limits.lower(), limits.upper()),
)
}
let mut extent : (Double, Double)? = None
for value in values {
if value.is_nan() || value.is_inf() {
continue
}
extent = Some(
match extent {
None => (value, value)
Some((lower, upper)) =>
(Double::min(lower, value), Double::max(upper, value))
},
)
}
extent.unwrap_or((0.0, 0.0))
}
///|
/// Divide by a decimal power without constructing an underflowed 10^-324.
fn divide_decimal(value : Double, exponent : Int32) -> Double {
if exponent < -308 {
value * 1.0e308 / @math.pow(10.0, (exponent + 308).to_double())
} else if exponent > 308 {
value / 1.0e308 / @math.pow(10.0, (exponent - 308).to_double())
} else {
value / @math.pow(10.0, exponent.to_double())
}
}
///|
/// Additive offset selection is independent of scientific power limits.
fn scalar_offset(values : Array[Double]) -> Double {
let (lower, upper) = formatter_extent(values, None)
if lower == upper || (lower <= 0.0 && upper >= 0.0) {
return 0.0
}
let abs_min = Double::min(Double::abs(lower), Double::abs(upper))
let abs_max = Double::max(Double::abs(lower), Double::abs(upper))
let sign = if lower < 0.0 { -1.0 } else { 1.0 }
let top_order = decimal_exponent(abs_max) + 1
let mut order = top_order
while order > -324 &&
divide_decimal(abs_min, order).floor() ==
divide_decimal(abs_max, order).floor() {
order -= 1
}
order += 1
if divide_decimal(abs_max - abs_min, order) <= 1.0e-2 {
order = top_order
while order > -324 &&
divide_decimal(abs_max, order).floor() -
divide_decimal(abs_min, order).floor() <=
1.0 {
order -= 1
}
order += 1
}
let prefix = divide_decimal(abs_max, order).floor()
// rcParams["axes.formatter.offset_threshold"] defaults to 4.
if prefix < 1000.0 {
return 0.0
}
let offset = sign * divide_decimal(prefix, -order)
if offset.is_inf() || offset.is_nan() {
0.0
} else {
offset
}
}
///|
fn scalar_order(
visible : Array[Double],
display_range : Double,
offset : Double,
power_min : Int32,
power_max : Int32,
) -> Int32 {
if visible.is_empty() {
return 0
}
if power_min == power_max && power_min != 0 {
return power_min
}
let mut magnitude = 0.0
for value in visible {
magnitude = Double::max(magnitude, Double::abs(value))
}
if offset != 0.0 && display_range > 0.0 {
magnitude = display_range
}
let exponent = if magnitude == 0.0 { 0 } else { decimal_exponent(magnitude) }
if exponent <= power_min || exponent >= power_max {
exponent
} else {
0
}
}
///|
fn scalar_precision(
scaled : Array[Double],
span_values : Array[Double],
) -> Int32 {
let (lower, upper) = formatter_extent(span_values, None)
let mut span = upper - lower
if span == 0.0 {
span = Double::max(Double::abs(lower), Double::abs(upper))
}
if span == 0.0 || span.is_inf() {
span = 1.0
}
let order = decimal_exponent(span)
let mut places = Int32::max(0, Int32::min(100, 3 - order))
let threshold = 1.0e-3 * @math.pow(10.0, order.to_double())
while places >= 0 {
let mut max_error = 0.0
for value in scaled {
if value.is_nan() || value.is_inf() {
continue
}
let rounded = try! @string.parse_double(format_fixed(value, places)[:])
max_error = Double::max(max_error, Double::abs(value - rounded))
}
if max_error < threshold {
places -= 1
} else {
break
}
}
Int32::min(100, places + 1)
}
///|
fn ScalarLayout::labels(
self : ScalarLayout,
values : Array[Double],
) -> Array[String] {
values.map(value => {
if numeric_special(value) is Some(text) {
return text
}
let residual = divide_decimal(value - self.offset, self.exponent)
let cleaned = if Double::abs(residual) < 1.0e-8 { 0.0 } else { residual }
format_fixed(cleaned, self.precision)
})
}