///|
/// Coordinate scale used by an axis.
pub(all) enum Scale {
Linear
Log
SymLog(linthresh~ : Double, linscale~ : Double, base~ : Double)
Asinh(linear_width~ : Double)
Logit(clip~ : Bool)
}
///|
/// Return the ordinary linear scale.
pub fn linear_scale() -> Scale {
Scale::Linear
}
///|
/// Return a base-ten logarithmic scale.
pub fn log_scale() -> Scale {
Scale::Log
}
///|
/// Construct base-ten probability geometry. Tick defaults require an explicit
/// locator; the clip mode does not change the strict valid-data domain.
pub fn logit_scale(
nonpositive~ : String = "mask",
) -> Scale raise @core.DataError {
match nonpositive {
"mask" => Scale::Logit(clip=false)
"clip" => Scale::Logit(clip=true)
_ =>
raise @core.DataError::InvalidTicks(
message="'\{nonpositive}' is not a valid value for nonpositive; supported values are 'mask', 'clip'",
)
}
}
///|
/// Construct inverse-hyperbolic-sine coordinate geometry.
/// Source width validation rejects nonpositive values; NaN and positive
/// infinity remain representable. Tick defaults require an explicit locator.
pub fn asinh_scale(linear_width~ : Double = 1.0) -> Scale raise @core.DataError {
if linear_width <= 0.0 {
raise @core.DataError::InvalidTicks(
message="Scale parameter 'linear_width' must be strictly positive",
)
}
Scale::Asinh(linear_width~)
}
///|
/// Construct a symmetric logarithmic scale around zero.
pub fn symlog_scale(
linthresh~ : Double = 1.0,
linscale~ : Double = 1.0,
base~ : Double = 10.0,
) -> Scale raise @core.DataError {
if linthresh <= 0.0 || linthresh.is_nan() || linthresh.is_inf() {
raise @core.DataError::InvalidTicks(
message="symlog linthresh must be positive and finite",
)
}
if linscale <= 0.0 || linscale.is_nan() || linscale.is_inf() {
raise @core.DataError::InvalidTicks(
message="symlog linscale must be positive and finite",
)
}
if base <= 1.0 || base.is_nan() || base.is_inf() {
raise @core.DataError::InvalidTicks(
message="symlog base must be finite and greater than one",
)
}
Scale::SymLog(linthresh~, linscale~, base~)
}
///|
/// Whether this scale uses base-ten logarithmic coordinates.
pub fn Scale::is_log(self : Scale) -> Bool {
self is Log
}
///|
/// Whether this scale applies a non-linear coordinate transform.
pub fn Scale::is_nonlinear(self : Scale) -> Bool {
match self {
Linear => false
Log | SymLog(..) | Asinh(..) | Logit(..) => true
}
}
///|
/// Whether this scale is symmetric logarithmic.
pub fn Scale::is_symlog(self : Scale) -> Bool {
self is SymLog(..)
}
///|
/// Whether this scale uses inverse-hyperbolic-sine coordinates.
pub fn Scale::is_asinh(self : Scale) -> Bool {
self is Asinh(..)
}
///|
/// Whether this scale uses log-odds probability coordinates.
pub fn Scale::is_logit(self : Scale) -> Bool {
self is Logit(..)
}
///|
/// Return the Logit nonpositive mode, if configured.
pub fn Scale::logit_nonpositive(self : Scale) -> String? {
match self {
Logit(clip=true) => Some("clip")
Logit(clip=false) => Some("mask")
_ => None
}
}
///|
/// Return the Asinh coordinate width, if configured.
pub fn Scale::asinh_linear_width(self : Scale) -> Double? {
match self {
Asinh(linear_width~) => Some(linear_width)
_ => None
}
}
///|
/// Return the symmetric-log linear threshold, if configured.
pub fn Scale::symlog_linthresh(self : Scale) -> Double? {
match self {
SymLog(linthresh~, ..) => Some(linthresh)
_ => None
}
}
///|
/// Return the symmetric-log base, if configured.
pub fn Scale::symlog_base(self : Scale) -> Double? {
match self {
SymLog(base~, ..) => Some(base)
_ => None
}
}
///|
fn symlog_linscale_adjusted(linscale : Double, base : Double) -> Double {
linscale / (1.0 - 1.0 / base)
}
///|
fn symlog_transform_value(
value : Double,
linthresh : Double,
linscale : Double,
base : Double,
) -> Double {
let adjusted = symlog_linscale_adjusted(linscale, base)
let magnitude = Double::abs(value)
if magnitude <= linthresh {
value * adjusted
} else {
let sign = if value == 0.0 { 0.0 } else if value < 0.0 { -1.0 } else { 1.0 }
let log_base = @math.ln(base)
sign *
linthresh *
(adjusted - @math.ln(linthresh) / log_base + @math.ln(magnitude) / log_base)
}
}
///|
fn symlog_inverse_value(
value : Double,
linthresh : Double,
linscale : Double,
base : Double,
) -> Double {
let adjusted = symlog_linscale_adjusted(linscale, base)
let linear_limit = symlog_transform_value(
linthresh, linthresh, linscale, base,
)
let magnitude = Double::abs(value)
if magnitude <= linear_limit {
value / adjusted
} else {
let sign = if value == 0.0 { 0.0 } else if value < 0.0 { -1.0 } else { 1.0 }
sign *
linthresh *
@math.exp((magnitude / linthresh - adjusted) * @math.ln(base))
}
}
///|
/// Check whether a data value can be represented on this scale.
pub fn Scale::is_valid(self : Scale, value : Double) -> Bool {
match self {
Linear => !value.is_nan() && !value.is_inf()
Log => value > 0.0 && !value.is_nan() && !value.is_inf()
SymLog(..) | Asinh(..) => !value.is_nan() && !value.is_inf()
Logit(..) => value > 0.0 && value < 1.0
}
}
///|
/// Transform one data value into display-space scale coordinates.
pub fn Scale::transform(self : Scale, value : Double) -> Double {
match self {
Linear => value
Log => @math.log10(value)
SymLog(linthresh~, linscale~, base~) =>
symlog_transform_value(value, linthresh, linscale, base)
Asinh(linear_width~) => linear_width * @math.asinh(value / linear_width)
Logit(clip~) => {
let result = @math.log10(value / (1.0 - value))
if clip && value <= 0.0 {
-1000.0
} else if clip && value >= 1.0 {
1000.0
} else {
result
}
}
}
}
///|
/// Convert display-space scale coordinates back to data values.
pub fn Scale::inverse(self : Scale, value : Double) -> Double {
match self {
Linear => value
Log => @math.pow(10.0, value)
SymLog(linthresh~, linscale~, base~) =>
symlog_inverse_value(value, linthresh, linscale, base)
Asinh(linear_width~) => linear_width * @math.sinh(value / linear_width)
Logit(..) => 1.0 / (1.0 + @math.pow(10.0, -value))
}
}
///|
/// Apply the scale's source domain restriction using raw minimum-positive data.
/// Finite minpos is preserved, including values outside the usual data domain.
pub fn Scale::limit_range_for_scale(
self : Scale,
lower : Double,
upper : Double,
minpos : Double,
) -> (Double, Double) {
match self {
Linear | SymLog(..) | Asinh(..) => (lower, upper)
Log => {
let minpos = if minpos.is_nan() || minpos.is_inf() {
1.0e-300
} else {
minpos
}
(
if lower <= 0.0 {
minpos
} else {
lower
},
if upper <= 0.0 {
minpos
} else {
upper
},
)
}
Logit(..) => {
let minpos = if minpos.is_nan() || minpos.is_inf() {
1.0e-7
} else {
minpos
}
(
if lower <= 0.0 {
minpos
} else {
lower
},
if upper >= 1.0 {
1.0 - minpos
} else {
upper
},
)
}
}
}
///|
fn symlog_ticks(
limits : @core.Limits,
linthresh : Double,
base : Double,
) -> Array[Double] {
let lower = Double::min(limits.lower(), limits.upper())
let upper = Double::max(limits.lower(), limits.upper())
let values : Array[Double] = []
if lower <= 0.0 && upper >= 0.0 {
values.push(0.0)
}
if lower <= linthresh && upper >= linthresh {
values.push(linthresh)
}
if lower <= -linthresh && upper >= -linthresh {
values.push(-linthresh)
}
let positive_upper = Double::max(upper, linthresh)
let positive_lower = Double::max(linthresh, 0.0)
if positive_upper > positive_lower {
let first = (@math.log10(positive_lower) / @math.log10(base))
.floor()
.to_int()
let last = (@math.log10(positive_upper) / @math.log10(base)).ceil().to_int()
for exponent in first..<=last {
let value = @math.pow(base, Double::from_int(exponent))
if value >= lower && value <= upper {
values.push(value)
}
if -value >= lower && -value <= upper {
values.push(-value)
}
}
}
values.sort()
if limits.lower() <= limits.upper() {
values
} else {
values.rev()
}
}
///|
fn linear_ticks(limits : @core.Limits, count : Int32) -> Array[Double] {
if limits.lower() == limits.upper() {
return [limits.lower()]
}
let actual = if count < 2 { 2 } else { count }
let step = (limits.upper() - limits.lower()) / Double::from_int(actual - 1)
let values = Array(capacity=actual)
for i in 0.. Array[Double] {
let lower = limits.lower()
let upper = limits.upper()
if lower.is_nan() || lower.is_inf() || upper.is_nan() || upper.is_inf() {
return []
}
if scale.is_logit() && (!scale.is_valid(lower) || !scale.is_valid(upper)) {
return []
}
let transformed_lower = scale.transform(lower)
let transformed_upper = scale.transform(upper)
if transformed_lower.is_nan() ||
transformed_lower.is_inf() ||
transformed_upper.is_nan() ||
transformed_upper.is_inf() {
return []
}
if lower == upper {
return [lower]
}
let actual = if count < 2 { 2 } else { count }
let returned = Int32::min(actual, 100000)
let denominator = Double::from_int(actual - 1)
let delta = transformed_upper - transformed_lower
let result = Array(capacity=returned)
for index in 0.. Array[Double] {
ignore(count)
let lower = Double::min(limits.lower(), limits.upper())
let upper = Double::max(limits.lower(), limits.upper())
if lower <= 0.0 ||
upper <= 0.0 ||
lower.is_nan() ||
upper.is_nan() ||
lower.is_inf() ||
upper.is_inf() {
return []
}
if lower == upper {
return [lower]
}
let first = @math.log10(lower).floor().to_int()
let last = @math.log10(upper).ceil().to_int()
let decades = last - first
let values = Array(
capacity=if decades <= 2 { (decades + 1) * 9 } else { decades + 1 },
)
if decades <= 2 {
for exponent in first..<=last {
let base = @math.pow(10.0, Double::from_int(exponent))
for mantissa in 1..<=9 {
let value = base * Double::from_int(mantissa)
if value >= lower && value <= upper {
values.push(value)
}
}
}
} else {
for exponent in first..<=last {
let value = @math.pow(10.0, Double::from_int(exponent))
if value >= lower && value <= upper {
values.push(value)
}
}
}
if limits.lower() <= limits.upper() {
values
} else {
values.rev()
}
}
///|
/// Generate deterministic convenience ticks for this scale.
/// Asinh and Logit use display-uniform samplers; explicitly install a named
/// locator for the corresponding source locator's rounding and count policy.
pub fn Scale::tick_values(
self : Scale,
limits : @core.Limits,
count~ : Int32 = 6,
) -> Array[Double] {
match self {
Linear => linear_ticks(limits, count)
Log => log_ticks(limits, count)
SymLog(linthresh~, linscale~, base~) => {
ignore(linscale)
symlog_ticks(limits, linthresh, base)
}
Asinh(..) | Logit(..) => nonlinear_display_ticks(self, limits, count)
}
}
///|
/// Format one scale tick using a stable decimal representation.
pub fn Scale::format_tick(self : Scale, value : Double) -> String {
ignore(self)
Double::to_string(value)
}