///|
/// Numeric tick formatter modeled after the public matplotlib.ticker
/// formatter family. The formatter is immutable; array-valued formatters
/// copy their labels when constructed and when queried.
pub(all) enum Formatter {
Scalar(
use_offset~ : Bool,
use_math_text~ : Bool,
power_min~ : Int32,
power_max~ : Int32
)
Fixed(Array[String])
Index(Array[String])
Format(String)
StrMethod(String)
Eng(unit~ : String, places~ : Int32, sep~ : String)
Percent(xmax~ : Double, decimals~ : Int32, symbol~ : String)
Log(base~ : Double, label_only_base~ : Bool)
Null
Func((Double) -> String)
}
///|
/// Construct a scalar formatter with Matplotlib-like power limits.
pub fn scalar_formatter(
use_offset~ : Bool = true,
use_math_text~ : Bool = false,
power_min~ : Int32 = -5,
power_max~ : Int32 = 6,
) -> Formatter raise @core.DataError {
if power_min > power_max {
raise @core.DataError::InvalidTicks(
message="scalar formatter power_min must not exceed power_max",
)
}
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~)
}
///|
/// Construct a formatter that returns labels by tick position.
pub fn fixed_formatter(labels : Array[String]) -> Formatter {
Formatter::Fixed(labels.copy())
}
///|
/// Construct a formatter that treats values as integer label indices.
pub fn index_formatter(labels : Array[String]) -> Formatter {
Formatter::Index(labels.copy())
}
///|
/// Construct a printf-style numeric formatter.
///
/// The supported conversions are `%g`, `%f`, `%e`, `%d`, and `%%`. A
/// precision such as `%.2f` is accepted for `%f` and `%e`; unsupported
/// conversions are preserved literally so a label is never silently lost.
pub fn format_str_formatter(format : String) -> Formatter {
Formatter::Format(format)
}
///|
/// Construct a Python `str.format`-style numeric formatter.
pub fn str_method_formatter(format : String) -> Formatter {
Formatter::StrMethod(format)
}
///|
/// Construct an engineering-notation formatter.
pub fn eng_formatter(
unit~ : String = "",
places~ : Int32 = -1,
sep~ : String = " ",
) -> Formatter raise @core.DataError {
if places < -1 || places > 15 {
raise @core.DataError::InvalidTicks(
message="engineering formatter places must be -1 or in 0..15",
)
}
Formatter::Eng(unit~, places~, sep~)
}
///|
/// Construct a percentage formatter. `decimals=-1` selects precision from the
/// displayed tick range, matching Matplotlib's automatic percentage labels.
pub fn percent_formatter(
xmax~ : Double = 100.0,
decimals~ : Int32 = -1,
symbol~ : String = "%",
) -> Formatter raise @core.DataError {
if xmax.is_nan() || xmax.is_inf() || xmax == 0.0 {
raise @core.DataError::InvalidTicks(
message="percent formatter xmax must be finite and non-zero",
)
}
if decimals < -1 || decimals > 15 {
raise @core.DataError::InvalidTicks(
message="percent formatter decimals must be -1 or in 0..15",
)
}
Formatter::Percent(xmax~, decimals~, symbol~)
}
///|
/// Construct a logarithmic formatter.
pub fn log_formatter(
base~ : Double = 10.0,
label_only_base~ : Bool = false,
) -> Formatter raise @core.DataError {
if base.is_nan() || base.is_inf() || base <= 1.0 {
raise @core.DataError::InvalidTicks(
message="log formatter base must be finite and greater than one",
)
}
Formatter::Log(base~, label_only_base~)
}
///|
/// Construct a formatter that suppresses all labels.
pub fn null_formatter() -> Formatter {
Formatter::Null
}
///|
/// Construct a formatter backed by a user function.
pub fn func_formatter(function : (Double) -> String) -> Formatter {
Formatter::Func(function)
}
///|
/// Copy a formatter without sharing mutable label storage.
pub fn Formatter::copy(self : Formatter) -> Formatter {
match self {
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~) =>
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~)
Formatter::Fixed(labels) => Formatter::Fixed(labels.copy())
Formatter::Index(labels) => Formatter::Index(labels.copy())
Formatter::Format(format) => Formatter::Format(format)
Formatter::StrMethod(format) => Formatter::StrMethod(format)
Formatter::Eng(unit~, places~, sep~) => Formatter::Eng(unit~, places~, sep~)
Formatter::Percent(xmax~, decimals~, symbol~) =>
Formatter::Percent(xmax~, decimals~, symbol~)
Formatter::Log(base~, label_only_base~) =>
Formatter::Log(base~, label_only_base~)
Formatter::Null => Formatter::Null
Formatter::Func(function) => Formatter::Func(function)
}
}
///|
/// Format one value. For position-dependent formatters use `format_values`.
pub fn Formatter::format_value(self : Formatter, value : Double) -> String {
match self {
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~) => {
ignore(use_math_text)
scalar_layout([value], None, use_offset, power_min, power_max).labels([
value,
])[0]
}
Formatter::Fixed(labels) =>
if labels.length() == 0 {
""
} else {
labels[0]
}
Formatter::Index(labels) => format_index(labels, value)
Formatter::Format(format) => format_printf(format, value)
Formatter::StrMethod(format) => format_str_method(format, value)
Formatter::Eng(unit~, places~, sep~) =>
format_engineering(value, unit, places, sep)
Formatter::Percent(xmax~, decimals~, symbol~) =>
format_percent(value, xmax, decimals, symbol, Double::abs(value))
Formatter::Log(base~, label_only_base~) =>
format_log(value, base, label_only_base)
Formatter::Null => ""
Formatter::Func(function) => function(value)
}
}
///|
/// Format a complete tick list, preserving position-based formatter rules.
pub fn Formatter::format_values(
self : Formatter,
values : Array[Double],
) -> Array[String] {
self.format_values_internal(values, None)
}
///|
/// Internal implementation shared by standalone and viewport-aware calls.
fn Formatter::format_values_internal(
self : Formatter,
values : Array[Double],
view : @core.Limits?,
) -> Array[String] {
match self {
Formatter::Fixed(labels) => {
let result : Array[String] = []
for index in 0.. values.map(value => format_index(labels, value))
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~) => {
ignore(use_math_text)
scalar_layout(values, view, use_offset, power_min, power_max).labels(
values,
)
}
Formatter::Percent(xmax~, decimals~, symbol~) =>
format_percent_values(values, xmax, decimals, symbol, view)
Formatter::Null => values.map(_ => "")
Formatter::StrMethod(format) => {
let result : Array[String] = []
for index, value in values {
result.push(format_str_method_at(format, value, index))
}
result
}
_ => values.map(value => self.format_value(value))
}
}
///|
/// Format a tick list when the caller knows the visible display range.
///
/// This is useful for percentage formatters: a viewport can be wider than the
/// supplied major ticks, and Matplotlib chooses automatic decimal places from
/// that viewport rather than from one tick value.
pub fn Formatter::format_values_with_range(
self : Formatter,
values : Array[Double],
display_range : Double,
) -> Array[String] {
match self {
Formatter::Percent(xmax~, decimals~, symbol~) =>
values.map(value => {
format_percent(value, xmax, decimals, symbol, display_range)
})
_ => self.format_values(values)
}
}
///|
/// Format labels using an optional viewport. This named form is convenient
/// for renderers that already carry a `Limits` value and preserves the simple
/// `format_values(values)` call for standalone use.
pub fn Formatter::format_values_with_view(
self : Formatter,
values : Array[Double],
view : @core.Limits?,
) -> Array[String] {
self.format_values_internal(values, view)
}
///|
/// Return the additive offset selected for a scalar formatter. This is
/// exposed separately so an axis or backend can render the offset text.
pub fn Formatter::offset(self : Formatter, values : Array[Double]) -> Double {
self.offset_with_view(values, None)
}
///|
/// Return a scalar formatter's additive offset using an optional viewport.
pub fn Formatter::offset_with_view(
self : Formatter,
values : Array[Double],
view : @core.Limits?,
) -> Double {
match self {
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~) => {
ignore(use_math_text)
scalar_layout(values, view, use_offset, power_min, power_max).offset
}
_ => 0.0
}
}
///|
/// Return the scalar formatter's scientific multiplier and additive offset.
/// `use_offset=false` disables only the additive term. `use_math_text=true`
/// uses Unicode multiplication and superscripts instead of TeX markup.
pub fn Formatter::offset_label(
self : Formatter,
values : Array[Double],
) -> String {
self.offset_label_with_view(values, None)
}
///|
/// Return the scalar formatter's scientific multiplier and additive offset
/// using an optional viewport.
pub fn Formatter::offset_label_with_view(
self : Formatter,
values : Array[Double],
view : @core.Limits?,
) -> String {
match self {
Formatter::Scalar(use_offset~, use_math_text~, power_min~, power_max~) => {
let layout = scalar_layout(values, view, use_offset, power_min, power_max)
layout.offset_label(use_math_text)
}
_ => ""
}
}