///|
/// A contour/tick level generator modeled after Matplotlib's Locator family.
///
/// The adapter supports immutable configurations and a live MultipleLocator.
/// Callers can choose fixed spacing with an offset, evenly
/// spaced linear levels, a MaxN-style "nice" level set, an explicit fixed set,
/// or logarithmic levels.
pub(all) enum Locator {
NamedLogit(LogitLocator)
NamedAsinh(AsinhLocator)
NamedSymLog(SymmetricalLogLocator)
NamedLog(LogLocator)
Null(NullLocator)
NamedMaxN(MaxNLocator)
NamedAuto(AutoLocator)
NamedLinear(LinearLocator)
Index(IndexLocator)
NamedFixed(FixedLocator)
Multiple(MultipleLocator)
Linear(count~ : Int32)
MaxN(count~ : Int32, integer~ : Bool, symmetric~ : Bool, prune~ : String)
Fixed(Array[Double])
Log(count~ : Int32, base~ : Double)
LogWithSubs(
count~ : Int32,
base~ : Double,
subs~ : Array[Double],
numdecs~ : Int32
)
}
///|
/// A minor tick generator used between major locator values.
pub(all) enum MinorLocator {
NamedLogit(LogitLocator)
NamedAsinh(AsinhLocator)
NamedSymLog(SymmetricalLogLocator)
NamedLog(LogLocator)
Null(NullLocator)
NamedAutoMinor(AutoMinorLocator)
NamedMaxN(MaxNLocator)
NamedAuto(AutoLocator)
NamedLinear(LinearLocator)
Index(IndexLocator)
NamedFixed(FixedLocator)
Multiple(MultipleLocator)
Auto(subdivisions~ : Int32)
Fixed(Array[Double])
Log(subdivisions~ : Int32, base~ : Double)
}
///|
/// Construct a linear locator with at least two requested levels.
pub fn linear_locator(count~ : Int32 = 7) -> Locator {
let actual = if count < 2 { 2 } else { count }
Locator::Linear(count=actual)
}
///|
/// Construct a MaxN-style locator with human-readable step sizes.
///
/// `count` is the requested number of visible ticks. The generator chooses
/// steps from the familiar 1/2/2.5/5 decade sequence, optionally restricts
/// them to integer values, mirrors the range around zero, and can prune one
/// or both endpoints.
pub fn maxn_locator(
count~ : Int32 = 7,
integer~ : Bool = false,
symmetric~ : Bool = false,
prune~ : String = "",
) -> Locator raise @core.DataError {
if count < 2 {
raise @core.DataError::InvalidTicks(
message="maxn locator count must be at least two",
)
}
if prune != "" && prune != "lower" && prune != "upper" && prune != "both" {
raise @core.DataError::InvalidTicks(
message="maxn locator prune must be empty, lower, upper, or both",
)
}
Locator::MaxN(count~, integer~, symmetric~, prune~)
}
///|
/// Construct an automatic minor locator that subdivides each major interval.
pub fn auto_minor_locator(
subdivisions~ : Int32 = 4,
) -> MinorLocator raise @core.DataError {
if subdivisions < 1 {
raise @core.DataError::InvalidTicks(
message="minor locator subdivisions must be positive",
)
}
MinorLocator::Auto(subdivisions~)
}
///|
/// Construct a logarithmic minor locator.
pub fn log_minor_locator(
subdivisions~ : Int32 = 9,
base~ : Double = 10.0,
) -> MinorLocator raise @core.DataError {
if subdivisions < 1 {
raise @core.DataError::InvalidTicks(
message="log minor locator subdivisions must be positive",
)
}
if base.is_nan() || base.is_inf() || base <= 1.0 {
raise @core.DataError::InvalidTicks(
message="log minor locator base must be finite and greater than one",
)
}
MinorLocator::Log(subdivisions~, base~)
}
///|
/// Construct a fixed minor locator from finite values.
pub fn fixed_minor_locator(
values : Array[Double],
) -> MinorLocator raise @core.DataError {
for value in values {
if value.is_nan() || value.is_inf() {
raise @core.DataError::InvalidTicks(
message="fixed minor locator values must be finite",
)
}
}
MinorLocator::Fixed(values.copy())
}
///|
/// Construct a fixed locator from a finite, strictly increasing level list.
pub fn fixed_locator(values : Array[Double]) -> Locator raise @core.DataError {
if values.length() == 0 {
raise @core.DataError::InvalidTicks(
message="fixed locator requires at least one value",
)
}
for index, value in values {
if value.is_nan() || value.is_inf() {
raise @core.DataError::InvalidTicks(
message="fixed locator values must be finite",
)
}
if index > 0 && value <= values[index - 1] {
raise @core.DataError::InvalidTicks(
message="fixed locator values must be strictly increasing",
)
}
}
Locator::Fixed(values.copy())
}
///|
/// Construct a logarithmic locator with a finite base greater than one.
pub fn log_locator(
count~ : Int32 = 7,
base~ : Double = 10.0,
subs~ : Array[Double] = [1.0],
numdecs~ : Int32 = 4,
) -> Locator raise @core.DataError {
if count < 2 {
raise @core.DataError::InvalidTicks(
message="log locator count must be at least two",
)
}
if base.is_nan() || base.is_inf() || base <= 1.0 {
raise @core.DataError::InvalidTicks(
message="log locator base must be finite and greater than one",
)
}
if numdecs < 1 {
raise @core.DataError::InvalidTicks(
message="log locator numdecs must be positive",
)
}
if subs.length() == 0 {
raise @core.DataError::InvalidTicks(
message="log locator subs must not be empty",
)
}
let normalized_subs = subs.copy()
for value in normalized_subs {
if value.is_nan() || value.is_inf() || value <= 0.0 {
raise @core.DataError::InvalidTicks(
message="log locator subs must be finite and positive",
)
}
}
normalized_subs.sort()
if normalized_subs.length() == 1 && normalized_subs[0] == 1.0 && numdecs == 4 {
Locator::Log(count~, base~)
} else {
Locator::LogWithSubs(count~, base~, subs=normalized_subs, numdecs~)
}
}
///|
/// Return evenly spaced levels in the requested range.
pub fn Locator::tick_values(
self : Locator,
lower : Double,
upper : Double,
) -> Array[Double] {
match self {
Null(value) => return value.call()
NamedLogit(_)
| NamedAsinh(_)
| NamedSymLog(_)
| NamedLog(_)
| NamedMaxN(_)
| NamedAuto(_)
| NamedLinear(_)
| Index(_)
| NamedFixed(_) =>
return self.checked_tick_values(lower, upper) catch { _ => [] }
_ => ()
}
if lower.is_nan() || lower.is_inf() || upper.is_nan() || upper.is_inf() {
return []
}
let first = Double::min(lower, upper)
let last = Double::max(lower, upper)
let ascending = lower <= upper
match self {
Locator::NamedLogit(_)
| Locator::NamedAsinh(_)
| Locator::NamedSymLog(_)
| Locator::NamedLog(_)
| Locator::Null(_)
| Locator::NamedMaxN(_)
| Locator::NamedAuto(_)
| Locator::NamedLinear(_)
| Locator::Index(_)
| Locator::NamedFixed(_) => []
Locator::Multiple(value) => {
let values = value.tick_values(first, last) catch { _ => [] }
if ascending {
values
} else {
values.rev()
}
}
Locator::Linear(count~) => {
let values = linear_ticks(@core.limits(first, last), count~)
if ascending {
values
} else {
values.rev()
}
}
Locator::MaxN(count~, integer~, symmetric~, prune~) => {
let values = maxn_ticks(
@core.limits(first, last),
count,
integer,
symmetric,
prune,
)
if ascending {
values
} else {
values.rev()
}
}
Locator::Fixed(values) => {
let result : Array[Double] = []
for value in values {
if value >= first && value <= last {
result.push(value)
}
}
result
}
Locator::Log(count~, base~) =>
log_locator_ticks(first, last, count, base, [1.0], 4, false, ascending)
Locator::LogWithSubs(count~, base~, subs~, numdecs~) =>
log_locator_ticks(
first, last, count, base, subs, numdecs, true, ascending,
)
}
}
///|
/// Generate logarithmic ticks with optional per-decade subdivisions.
fn log_locator_ticks(
first : Double,
last : Double,
count : Int32,
base : Double,
subs : Array[Double],
numdecs : Int32,
limit_decades : Bool,
ascending : Bool,
) -> Array[Double] {
if first <= 0.0 || last <= 0.0 {
return []
}
let lower_power = (@math.log10(first) / @math.log10(base)).floor().to_int()
let upper_power = (@math.log10(last) / @math.log10(base)).ceil().to_int()
let span = Int32::max(0, upper_power - lower_power)
let actual_count = Int32::max(2, count)
if subs.length() == 1 && subs[0] == 1.0 {
let decade_stride = if limit_decades {
Int32::max(
1,
(span + Int32::max(1, numdecs) - 1) / Int32::max(1, numdecs),
)
} else {
1
}
let values = if span <= 0 {
[first]
} else if decade_stride > 1 {
let result : Array[Double] = []
let mut power = lower_power
while power <= upper_power {
let value = @math.pow(base, Double::from_int(power))
if value >= first && value <= last {
result.push(value)
}
power += decade_stride
}
if result.length() == 0 {
[first, last]
} else {
result
}
} else if span + 1 <= actual_count {
let result : Array[Double] = []
for power in lower_power..<=upper_power {
let value = @math.pow(base, Double::from_int(power))
if value >= first && value <= last {
result.push(value)
}
}
if result.length() == 0 {
[first, last]
} else {
result
}
} else {
let result = Array(capacity=actual_count)
for index in 0..= first && value <= last && !contains_tick(result, value) {
result.push(value)
}
}
power += stride
}
result.sort()
let values = if result.length() == 0 { [first, last] } else { result }
if ascending {
values
} else {
values.rev()
}
}
}
///|
/// Return minor tick positions for a major tick list and data range.
pub fn MinorLocator::tick_values(
self : MinorLocator,
major : Array[Double],
lower : Double,
upper : Double,
) -> Array[Double] {
match self {
Null(value) => return value.call()
NamedAutoMinor(value) =>
return value.tick_values(lower, upper) catch { _ => [] }
NamedLogit(_)
| NamedAsinh(_)
| NamedSymLog(_)
| NamedLog(_)
| NamedMaxN(_)
| NamedAuto(_)
| NamedLinear(_)
| Index(_)
| NamedFixed(_) =>
return self.checked_tick_values(major, lower, upper) catch { _ => [] }
_ => ()
}
if lower.is_nan() || lower.is_inf() || upper.is_nan() || upper.is_inf() {
return []
}
let first = Double::min(lower, upper)
let last = Double::max(lower, upper)
let result : Array[Double] = []
match self {
MinorLocator::NamedLogit(_)
| MinorLocator::NamedAsinh(_)
| MinorLocator::NamedSymLog(_)
| MinorLocator::NamedLog(_)
| MinorLocator::Null(_)
| MinorLocator::NamedAutoMinor(_)
| MinorLocator::NamedMaxN(_)
| MinorLocator::NamedAuto(_)
| MinorLocator::NamedLinear(_)
| MinorLocator::Index(_)
| MinorLocator::NamedFixed(_) => ()
MinorLocator::Multiple(value) => {
let values = value.tick_values(first, last) catch { _ => [] }
for value in values {
if value >= first && value <= last && !contains_tick(major, value) {
result.push(value)
}
}
}
MinorLocator::Fixed(values) =>
for value in values {
if value >= first && value <= last && !contains_tick(major, value) {
result.push(value)
}
}
MinorLocator::Auto(subdivisions~) => {
let actual = Int32::max(1, subdivisions)
if major.length() >= 2 {
for index in 0..<(major.length() - 1) {
let start = major[index]
let end = major[index + 1]
for part in 1..= first && value <= last && !contains_tick(major, value) {
result.push(value)
}
}
}
}
}
MinorLocator::Log(subdivisions~, base~) => {
if first <= 0.0 || last <= 0.0 {
return []
}
let lower_power = (@math.log10(first) / @math.log10(base))
.floor()
.to_int()
let upper_power = (@math.log10(last) / @math.log10(base)).ceil().to_int()
let actual = Int32::max(1, subdivisions)
for power in lower_power..<=upper_power {
let decade = @math.pow(base, Double::from_int(power))
for part in 1..<=actual {
let value = decade * Double::from_int(part)
if value >= first && value <= last && !contains_tick(major, value) {
result.push(value)
}
}
}
}
}
result.sort()
if lower <= upper {
result
} else {
result.rev()
}
}
///|
/// Copy arrays while retaining live named locator handles.
/// Use the named object copy() method for a separate parameter configuration.
pub fn Locator::copy(self : Locator) -> Locator {
match self {
Locator::NamedLogit(value) => Locator::NamedLogit(value)
Locator::NamedAsinh(value) => Locator::NamedAsinh(value)
Locator::NamedSymLog(value) => Locator::NamedSymLog(value)
Locator::NamedLog(value) => Locator::NamedLog(value)
Locator::Null(value) => Locator::Null(value)
Locator::NamedMaxN(value) => Locator::NamedMaxN(value)
Locator::NamedAuto(value) => Locator::NamedAuto(value)
Locator::NamedLinear(value) => Locator::NamedLinear(value)
Locator::Index(value) => Locator::Index(value)
Locator::NamedFixed(value) => Locator::NamedFixed(value)
Locator::Multiple(value) => Locator::Multiple(value)
Locator::Linear(count~) => Locator::Linear(count~)
Locator::MaxN(count~, integer~, symmetric~, prune~) =>
Locator::MaxN(count~, integer~, symmetric~, prune~)
Locator::Fixed(values) => Locator::Fixed(values.copy())
Locator::Log(count~, base~) => Locator::Log(count~, base~)
Locator::LogWithSubs(count~, base~, subs~, numdecs~) =>
Locator::LogWithSubs(count~, base~, subs=subs.copy(), numdecs~)
}
}
///|
/// Copy arrays while retaining live named locator handles.
pub fn MinorLocator::copy(self : MinorLocator) -> MinorLocator {
match self {
MinorLocator::NamedLogit(value) => MinorLocator::NamedLogit(value)
MinorLocator::NamedAsinh(value) => MinorLocator::NamedAsinh(value)
MinorLocator::NamedSymLog(value) => MinorLocator::NamedSymLog(value)
MinorLocator::NamedLog(value) => MinorLocator::NamedLog(value)
MinorLocator::Null(value) => MinorLocator::Null(value)
MinorLocator::NamedAutoMinor(value) => MinorLocator::NamedAutoMinor(value)
MinorLocator::NamedMaxN(value) => MinorLocator::NamedMaxN(value)
MinorLocator::NamedAuto(value) => MinorLocator::NamedAuto(value)
MinorLocator::NamedLinear(value) => MinorLocator::NamedLinear(value)
MinorLocator::Index(value) => MinorLocator::Index(value)
MinorLocator::NamedFixed(value) => MinorLocator::NamedFixed(value)
MinorLocator::Multiple(value) => MinorLocator::Multiple(value)
MinorLocator::Auto(subdivisions~) => MinorLocator::Auto(subdivisions~)
MinorLocator::Fixed(values) => MinorLocator::Fixed(values.copy())
MinorLocator::Log(subdivisions~, base~) =>
MinorLocator::Log(subdivisions~, base~)
}
}
///|
/// Return a copy of fixed levels, or an empty array for generated locators.
pub fn Locator::fixed_values(self : Locator) -> Array[Double] {
match self {
Locator::NamedFixed(value) => value.locations().to_array() catch { _ => [] }
Locator::Fixed(values) => values.copy()
_ => []
}
}
///|
/// Return whether a tick list already contains a value.
fn contains_tick(values : Array[Double], value : Double) -> Bool {
for candidate in values {
if candidate == value {
return true
}
}
false
}
///|
/// Choose a MaxN step from a stable decimal sequence.
fn maxn_step(span : Double, count : Int32, integer : Bool) -> Double {
let raw = span / Double::from_int(Int32::max(1, count - 1))
if raw <= 0.0 || raw.is_nan() || raw.is_inf() {
return 1.0
}
let exponent = @math.log10(raw).floor()
let scale = @math.pow(10.0, exponent)
let fraction = raw / scale
let base_step = if fraction <= 1.0 {
1.0
} else if fraction <= 2.0 {
2.0
} else if fraction <= 2.5 {
2.5
} else if fraction <= 5.0 {
5.0
} else {
10.0
}
let step = base_step * scale
if integer && step < 1.0 {
1.0
} else {
step
}
}
///|
/// Generate MaxN-style ticks while retaining the requested direction.
fn maxn_ticks(
limits : @core.Limits,
count : Int32,
integer : Bool,
symmetric : Bool,
prune : String,
) -> Array[Double] {
if !limits.is_finite() {
return []
}
if limits.lower() == limits.upper() {
return [limits.lower()]
}
let ascending = limits.lower() <= limits.upper()
let original_lower = Double::min(limits.lower(), limits.upper())
let original_upper = Double::max(limits.lower(), limits.upper())
let (lower, upper) = if symmetric {
let radius = Double::max(
Double::abs(original_lower),
Double::abs(original_upper),
)
(-radius, radius)
} else {
(original_lower, original_upper)
}
let step = maxn_step(upper - lower, count, integer)
let first_index = (lower / step).ceil().to_int()
let last_index = (upper / step).floor().to_int()
let values : Array[Double] = []
for index in first_index..<=last_index {
let value = Double::from_int(index) * step
if value >= lower - step * 1.0e-12 && value <= upper + step * 1.0e-12 {
values.push(if Double::abs(value) < step * 1.0e-12 { 0.0 } else { value })
}
}
if values.length() < 2 {
values.clear()
values.push(lower)
values.push(upper)
}
let pruned = Array(capacity=values.length())
for index, value in values {
let remove_lower = (prune == "lower" || prune == "both") && index == 0
let remove_upper = (prune == "upper" || prune == "both") &&
index == values.length() - 1
if !remove_lower && !remove_upper {
pruned.push(value)
}
}
if ascending {
pruned
} else {
pruned.rev()
}
}