use crate::portable::spec::{FormatterSpec, LocatorSpec};
use crate::portable::{PortableFormatter, PortableLocator};
pub trait Locator: Send + Sync {
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64>;
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
(vmin, vmax)
}
fn minor_tick_values(&self, major: &[f64], vmin: f64, vmax: f64) -> Vec<f64> {
let _ = major;
self.tick_values(vmin, vmax)
}
fn portable_spec(&self) -> Option<PortableLocator> {
None
}
}
pub trait Formatter: Send + Sync {
fn format(&self, value: f64, pos: Option<usize>) -> String;
fn format_ticks(&self, values: &[f64]) -> Vec<String> {
values
.iter()
.enumerate()
.map(|(i, &value)| self.format(value, Some(i)))
.collect()
}
fn portable_spec(&self) -> Option<PortableFormatter> {
None
}
}
fn floordiv(a: f64, b: f64) -> f64 {
(a / b).floor()
}
fn divmod(x: f64, step: f64) -> (f64, f64) {
let d = floordiv(x, step);
(d, x - d * step)
}
fn nonsingular(
mut vmin: f64,
mut vmax: f64,
expander: f64,
tiny: f64,
increasing: bool,
) -> (f64, f64) {
if !vmin.is_finite() || !vmax.is_finite() {
return (-expander, expander);
}
let mut swapped = false;
if vmax < vmin {
std::mem::swap(&mut vmin, &mut vmax);
swapped = true;
}
let maxabsvalue = vmin.abs().max(vmax.abs());
if maxabsvalue < (1e6 / tiny) * f64::MIN_POSITIVE {
vmin = -expander;
vmax = expander;
} else if vmax - vmin <= maxabsvalue * tiny {
vmin -= expander * maxabsvalue;
vmax += expander * maxabsvalue;
}
if swapped && !increasing {
std::mem::swap(&mut vmin, &mut vmax);
}
(vmin, vmax)
}
fn scale_range(vmin: f64, vmax: f64, n: f64, threshold: f64) -> (f64, f64) {
let dv = (vmax - vmin).abs();
let meanv = (vmax + vmin) / 2.0;
let offset = if dv == 0.0 || meanv.abs() / dv < threshold {
0.0
} else {
(10f64.powf(floordiv(meanv.abs().log10(), 1.0))).copysign(meanv)
};
let scale = 10f64.powf(floordiv((dv / n).log10(), 1.0));
(scale, offset)
}
#[derive(Clone, Copy, Debug)]
struct EdgeInteger {
step: f64,
offset: f64,
}
impl EdgeInteger {
fn new(step: f64, offset: f64) -> Self {
debug_assert!(step > 0.0, "'step' must be positive");
EdgeInteger {
step,
offset: offset.abs(),
}
}
fn closeto(&self, ms: f64, edge: f64) -> bool {
let tol = if self.offset > 0.0 {
let digits = (self.offset / self.step).log10();
(10f64.powf(digits - 12.0)).clamp(1e-10, 0.4999)
} else {
1e-10
};
(ms - edge).abs() < tol
}
fn le(&self, x: f64) -> f64 {
let (d, m) = divmod(x, self.step);
if self.closeto(m / self.step, 1.0) {
d + 1.0
} else {
d
}
}
fn ge(&self, x: f64) -> f64 {
let (d, m) = divmod(x, self.step);
if self.closeto(m / self.step, 0.0) {
d
} else {
d + 1.0
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NBins {
Fixed(usize),
Auto,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TickPrune {
#[default]
None,
Lower,
Upper,
Both,
}
pub struct MaxNLocator {
nbins: NBins,
extended_steps: Vec<f64>,
integer: bool,
symmetric: bool,
min_n_ticks: usize,
prune: TickPrune,
}
impl MaxNLocator {
const DEFAULT_STEPS: [f64; 10] = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0];
pub fn new(nbins: NBins) -> Self {
Self::with_steps(nbins, &Self::DEFAULT_STEPS, false, false, 2)
}
pub fn with_steps(
nbins: NBins,
steps: &[f64],
integer: bool,
symmetric: bool,
min_n_ticks: usize,
) -> Self {
let steps = Self::validate_steps(steps);
let extended_steps = Self::staircase(&steps);
MaxNLocator {
nbins,
extended_steps,
integer,
symmetric,
min_n_ticks: min_n_ticks.max(1),
prune: TickPrune::None,
}
}
#[must_use]
pub fn with_prune(mut self, prune: TickPrune) -> Self {
self.prune = prune;
self
}
#[must_use]
pub fn with_integer(mut self, integer: bool) -> Self {
self.integer = integer;
self
}
#[must_use]
pub fn with_symmetric(mut self, symmetric: bool) -> Self {
self.symmetric = symmetric;
self
}
#[must_use]
pub fn with_min_n_ticks(mut self, min_n_ticks: usize) -> Self {
self.min_n_ticks = min_n_ticks.max(1);
self
}
#[must_use]
pub fn nbins(&self) -> NBins {
self.nbins
}
#[must_use]
pub fn integer(&self) -> bool {
self.integer
}
#[must_use]
pub fn symmetric(&self) -> bool {
self.symmetric
}
#[must_use]
pub fn min_n_ticks(&self) -> usize {
self.min_n_ticks
}
#[must_use]
pub fn prune(&self) -> TickPrune {
self.prune
}
fn validate_steps(steps: &[f64]) -> Vec<f64> {
assert!(!steps.is_empty(), "steps must be non-empty");
for w in steps.windows(2) {
assert!(
w[1] - w[0] > 0.0,
"steps argument must be an increasing sequence between 1 and 10"
);
}
assert!(
*steps.last().unwrap() <= 10.0 && steps[0] >= 1.0,
"steps argument must be an increasing sequence between 1 and 10"
);
let mut v: Vec<f64> = Vec::with_capacity(steps.len() + 2);
if steps[0] != 1.0 {
v.push(1.0);
}
v.extend_from_slice(steps);
if *steps.last().unwrap() != 10.0 {
v.push(10.0);
}
v
}
fn staircase(steps: &[f64]) -> Vec<f64> {
let mut out = Vec::with_capacity(steps.len() * 2);
for &s in &steps[..steps.len() - 1] {
out.push(0.1 * s);
}
out.extend_from_slice(steps);
out.push(10.0 * steps[1]);
out
}
fn resolved_nbins(&self) -> f64 {
match self.nbins {
NBins::Fixed(n) => n as f64,
NBins::Auto => 9.0,
}
}
fn raw_ticks(&self, vmin: f64, vmax: f64) -> Vec<f64> {
let nbins = self.resolved_nbins();
let (scale, offset) = scale_range(vmin, vmax, nbins, 100.0);
let vmin_o = vmin - offset;
let vmax_o = vmax - offset;
let mut steps: Vec<f64> = self.extended_steps.iter().map(|&s| s * scale).collect();
if self.integer {
steps.retain(|&s| s < 1.0 || (s - s.round()).abs() < 0.001);
}
let raw_step = (vmax_o - vmin_o) / nbins;
let large_steps: Vec<bool> = steps.iter().map(|&s| s >= raw_step).collect();
let istep = large_steps
.iter()
.position(|&b| b)
.unwrap_or(steps.len() - 1);
let mut ticks: Vec<f64> = Vec::new();
for &step0 in steps[..=istep].iter().rev() {
let mut step = step0;
if self.integer && (vmax_o.floor() - vmin_o.ceil()) >= (self.min_n_ticks as f64 - 1.0) {
step = step.max(1.0);
}
let best_vmin = floordiv(vmin_o, step) * step;
let edge = EdgeInteger::new(step, offset);
let low = edge.le(vmin_o - best_vmin);
let high = edge.ge(vmax_o - best_vmin);
let n = (high - low) as i64;
ticks = (0..=n)
.map(|i| (low + i as f64) * step + best_vmin)
.collect();
let nticks = ticks
.iter()
.filter(|&&t| t <= vmax_o && t >= vmin_o)
.count();
if nticks >= self.min_n_ticks {
break;
}
}
ticks.iter().map(|&t| t + offset).collect()
}
fn prune_ticks(&self, ticks: Vec<f64>, vmin: f64, vmax: f64) -> Vec<f64> {
if self.prune == TickPrune::None || ticks.is_empty() {
return ticks;
}
let mut first = 0;
let mut last = ticks.len();
if matches!(self.prune, TickPrune::Lower | TickPrune::Both) && same_tick(ticks[first], vmin)
{
first += 1;
}
if first < last
&& matches!(self.prune, TickPrune::Upper | TickPrune::Both)
&& same_tick(ticks[last - 1], vmax)
{
last -= 1;
}
ticks[first..last].to_vec()
}
}
impl Locator for MaxNLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::MaxN {
nbins: match self.nbins {
NBins::Auto => None,
NBins::Fixed(n) => Some(n),
},
extended_steps: self.extended_steps.clone(),
integer: self.integer,
symmetric: self.symmetric,
min_n_ticks: self.min_n_ticks,
prune: self.prune,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
let (mut vmin, mut vmax) = (vmin, vmax);
if self.symmetric {
vmax = vmin.abs().max(vmax.abs());
vmin = -vmax;
}
let (vmin, vmax) = nonsingular(vmin, vmax, 1e-13, 1e-14, true);
self.prune_ticks(self.raw_ticks(vmin, vmax), vmin, vmax)
}
fn view_limits(&self, dmin: f64, dmax: f64) -> (f64, f64) {
let (mut dmin, mut dmax) = (dmin, dmax);
if self.symmetric {
dmax = dmin.abs().max(dmax.abs());
dmin = -dmax;
}
nonsingular(dmin, dmax, 1e-12, 1e-13, true)
}
}
impl Default for MaxNLocator {
fn default() -> Self {
Self::new(NBins::Fixed(10))
}
}
pub struct AutoLocator {
inner: MaxNLocator,
}
impl AutoLocator {
pub fn new() -> Self {
AutoLocator {
inner: MaxNLocator::with_steps(
NBins::Auto,
&[1.0, 2.0, 2.5, 5.0, 10.0],
false,
false,
2,
),
}
}
}
impl Default for AutoLocator {
fn default() -> Self {
Self::new()
}
}
impl Locator for AutoLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Auto))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
self.inner.tick_values(vmin, vmax)
}
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
self.inner.view_limits(vmin, vmax)
}
}
pub struct AutoMinorLocator {
subdivisions: Option<usize>,
}
impl AutoMinorLocator {
pub fn new() -> Self {
AutoMinorLocator { subdivisions: None }
}
pub fn with_subdivisions(subdivisions: usize) -> Self {
AutoMinorLocator {
subdivisions: Some(subdivisions),
}
}
#[must_use]
pub fn subdivisions(&self) -> Option<usize> {
self.subdivisions
}
pub fn tick_values_from_major(&self, major_locs: &[f64], vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() || vmin == vmax {
return Vec::new();
}
let reversed = vmax < vmin;
let (lo, hi) = if reversed { (vmax, vmin) } else { (vmin, vmax) };
let mut major_locs: Vec<f64> = major_locs
.iter()
.copied()
.filter(|value| value.is_finite())
.collect();
major_locs.sort_by(|a, b| a.partial_cmp(b).expect("finite majors are comparable"));
major_locs.dedup_by(|a, b| same_tick(*a, *b));
if major_locs.len() < 2 {
return Vec::new();
}
let major_step = major_locs
.windows(2)
.map(|pair| (pair[1] - pair[0]).abs())
.find(|step| *step > 0.0 && step.is_finite())
.unwrap_or(0.0);
if major_step <= 0.0 || !major_step.is_finite() {
return Vec::new();
}
let subdivisions = self
.subdivisions
.unwrap_or_else(|| auto_minor_subdivisions(major_step));
if subdivisions < 2 {
return Vec::new();
}
let minor_step = major_step / subdivisions as f64;
let anchor = major_locs[0];
let first = ((lo - anchor) / minor_step).ceil() as i64;
let last = ((hi - anchor) / minor_step).floor() as i64;
if last < first {
return Vec::new();
}
let mut ticks = Vec::new();
for i in first..=last {
let tick = anchor + i as f64 * minor_step;
if tick < lo - 1e-12 || tick > hi + 1e-12 {
continue;
}
if major_locs.iter().any(|major| same_tick(tick, *major)) {
continue;
}
ticks.push(tick);
}
if reversed {
ticks.reverse();
}
ticks
}
}
impl Default for AutoMinorLocator {
fn default() -> Self {
Self::new()
}
}
impl Locator for AutoMinorLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::AutoMinor {
subdivisions: self.subdivisions,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
let (lo, hi) = if vmax < vmin {
(vmax, vmin)
} else {
(vmin, vmax)
};
let major_locs = AutoLocator::new().tick_values(lo, hi);
self.tick_values_from_major(&major_locs, vmin, vmax)
}
fn minor_tick_values(&self, major: &[f64], vmin: f64, vmax: f64) -> Vec<f64> {
self.tick_values_from_major(major, vmin, vmax)
}
}
fn auto_minor_subdivisions(major_step: f64) -> usize {
let exponent = major_step.abs().log10().floor();
let mantissa = major_step / 10f64.powf(exponent);
if [1.0, 2.5, 5.0, 10.0]
.iter()
.any(|candidate| (mantissa - candidate).abs() < 1e-10)
{
5
} else {
4
}
}
fn same_tick(a: f64, b: f64) -> bool {
let scale = a.abs().max(b.abs()).max(1.0);
(a - b).abs() <= scale * 1e-12
}
pub struct MultipleLocator {
edge: EdgeInteger,
offset: f64,
}
impl MultipleLocator {
pub fn new(base: f64) -> Self {
Self::with_offset(base, 0.0)
}
pub fn with_offset(base: f64, offset: f64) -> Self {
assert!(
base.is_finite() && base > 0.0,
"multiple locator base must be finite and > 0"
);
MultipleLocator {
edge: EdgeInteger::new(base, 0.0),
offset,
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.edge.step
}
#[must_use]
pub fn offset(&self) -> f64 {
self.offset
}
}
impl Default for MultipleLocator {
fn default() -> Self {
MultipleLocator::new(1.0)
}
}
impl Locator for MultipleLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Multiple {
step: self.edge.step,
offset: self.offset,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
let (mut vmin, mut vmax) = (vmin, vmax);
if vmax < vmin {
std::mem::swap(&mut vmin, &mut vmax);
}
let step = self.edge.step;
vmin -= self.offset;
vmax -= self.offset;
let vmin = self.edge.ge(vmin) * step;
let n = floordiv(vmax - vmin + 0.001 * step, step);
let count = n as i64 + 3;
(0..count)
.map(|i| vmin - step + i as f64 * step + self.offset)
.collect()
}
fn view_limits(&self, dmin: f64, dmax: f64) -> (f64, f64) {
nonsingular(dmin, dmax, 0.001, 1e-15, true)
}
}
pub struct LinearLocator {
numticks: usize,
}
impl LinearLocator {
pub fn new(numticks: usize) -> Self {
LinearLocator { numticks }
}
#[must_use]
pub fn numticks(&self) -> usize {
self.numticks
}
}
impl Default for LinearLocator {
fn default() -> Self {
LinearLocator { numticks: 11 }
}
}
impl Locator for LinearLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::LinearN {
numticks: self.numticks,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
let (vmin, vmax) = nonsingular(vmin, vmax, 0.05, 1e-15, true);
if self.numticks == 0 {
return Vec::new();
}
if self.numticks == 1 {
return vec![vmin];
}
let n = self.numticks - 1;
let step = (vmax - vmin) / n as f64;
let mut out: Vec<f64> = (0..self.numticks).map(|i| vmin + i as f64 * step).collect();
*out.last_mut().unwrap() = vmax;
out
}
}
#[derive(Default)]
pub struct FixedLocator {
locs: Vec<f64>,
nbins: Option<usize>,
}
impl FixedLocator {
pub fn new(locs: Vec<f64>) -> Self {
FixedLocator { locs, nbins: None }
}
pub fn from_locations<I>(locations: I) -> Self
where
I: IntoIterator<Item = f64>,
{
FixedLocator {
locs: locations.into_iter().collect(),
nbins: None,
}
}
pub fn with_nbins(locs: Vec<f64>, nbins: usize) -> Self {
FixedLocator {
locs,
nbins: Some(nbins.max(2)),
}
}
#[must_use]
pub fn locations(&self) -> &[f64] {
&self.locs
}
#[must_use]
pub fn nbins(&self) -> Option<usize> {
self.nbins
}
}
impl Locator for FixedLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Fixed {
locs: self.locs.clone(),
nbins: self.nbins,
}))
}
fn tick_values(&self, _vmin: f64, _vmax: f64) -> Vec<f64> {
let nbins = match self.nbins {
None => return self.locs.clone(),
Some(n) => n,
};
let len = self.locs.len();
if len == 0 {
return Vec::new();
}
let step = ((len as f64 / nbins as f64).ceil() as usize).max(1);
let subsample = |start: usize| -> Vec<f64> {
self.locs[start..].iter().step_by(step).copied().collect()
};
let min_abs = |v: &[f64]| v.iter().map(|x| x.abs()).fold(f64::INFINITY, f64::min);
let mut ticks = subsample(0);
for i in 1..step {
let ticks1 = subsample(i);
if !ticks1.is_empty() && min_abs(&ticks1) < min_abs(&ticks) {
ticks = ticks1;
}
}
ticks
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogLocator {
base: f64,
subs: Vec<f64>,
}
impl LogLocator {
pub fn new(base: f64) -> Self {
Self::with_subs(base, vec![1.0])
}
pub fn with_subs(base: f64, subs: Vec<f64>) -> Self {
assert!(
base.is_finite() && base > 1.0,
"log locator base must be finite and > 1"
);
let mut subs: Vec<f64> = subs
.into_iter()
.filter(|s| s.is_finite() && *s >= 1.0 && *s < base)
.collect();
subs.sort_by(|a, b| a.partial_cmp(b).expect("finite subs are comparable"));
subs.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
if subs.is_empty() {
subs.push(1.0);
}
LogLocator { base, subs }
}
pub fn minor(base: f64) -> Self {
let upper = base.floor() as i32;
let subs = if upper > 2 {
(2..upper).map(f64::from).collect()
} else {
Vec::new()
};
Self::with_subs(base, subs)
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn subs(&self) -> &[f64] {
&self.subs
}
}
impl Default for LogLocator {
fn default() -> Self {
Self::new(10.0)
}
}
impl Locator for LogLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Log {
base: self.base,
subs: self.subs.clone(),
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() || vmin <= 0.0 || vmax <= 0.0 {
return Vec::new();
}
let (lo, hi) = if vmin <= vmax {
(vmin, vmax)
} else {
(vmax, vmin)
};
let log_base = self.base.ln();
let start = (lo.ln() / log_base).floor() as i32;
let end = (hi.ln() / log_base).ceil() as i32;
let mut ticks = Vec::new();
for exponent in start..=end {
let decade = self.base.powi(exponent);
for &sub in &self.subs {
let tick = sub * decade;
if tick >= lo * (1.0 - 1e-12) && tick <= hi * (1.0 + 1e-12) {
ticks.push(tick);
}
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).expect("finite ticks are comparable"));
ticks.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * a.abs().max(b.abs()).max(1.0));
if vmin > vmax {
ticks.reverse();
}
ticks
}
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
if !vmin.is_finite() || !vmax.is_finite() || vmin <= 0.0 || vmax <= 0.0 {
return (1.0, self.base);
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let log_base = self.base.ln();
let lower = self.base.powi((lo.ln() / log_base).floor() as i32);
let upper = self.base.powi((hi.ln() / log_base).ceil() as i32);
if reversed {
(upper, lower)
} else {
(lower, upper)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SymlogLocator {
base: f64,
linthresh: f64,
linear_ticks: usize,
}
impl SymlogLocator {
pub fn new(base: f64, linthresh: f64) -> Self {
Self::with_linear_ticks(base, linthresh, 3)
}
pub fn with_linear_ticks(base: f64, linthresh: f64, linear_ticks: usize) -> Self {
assert!(
base.is_finite() && base > 1.0,
"symlog locator base must be finite and > 1"
);
assert!(
linthresh.is_finite() && linthresh > 0.0,
"symlog locator linthresh must be finite and > 0"
);
SymlogLocator {
base,
linthresh,
linear_ticks: linear_ticks.max(2),
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn linthresh(&self) -> f64 {
self.linthresh
}
#[must_use]
pub fn linear_ticks(&self) -> usize {
self.linear_ticks
}
}
impl Default for SymlogLocator {
fn default() -> Self {
Self::new(10.0, 1.0)
}
}
impl Locator for SymlogLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Symlog {
base: self.base,
linthresh: self.linthresh,
linear_ticks: self.linear_ticks,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() {
return Vec::new();
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let mut ticks = Vec::new();
if lo < -self.linthresh {
let max_abs = (-lo).max(self.linthresh);
let max_exp = (max_abs / self.linthresh).log(self.base).ceil() as i32;
for exponent in (1..=max_exp).rev() {
let tick = -self.linthresh * self.base.powi(exponent);
if tick >= lo * (1.0 + 1e-12) && tick <= hi * (1.0 - 1e-12) {
ticks.push(tick);
}
}
}
let linear_lo = lo.max(-self.linthresh);
let linear_hi = hi.min(self.linthresh);
if linear_lo <= linear_hi {
let step = 2.0 * self.linthresh / (self.linear_ticks - 1) as f64;
for i in 0..self.linear_ticks {
let tick = -self.linthresh + i as f64 * step;
if tick >= linear_lo - 1e-12 && tick <= linear_hi + 1e-12 {
ticks.push(if tick.abs() < 1e-12 { 0.0 } else { tick });
}
}
}
if hi > self.linthresh {
let max_exp = (hi / self.linthresh).log(self.base).ceil() as i32;
for exponent in 1..=max_exp {
let tick = self.linthresh * self.base.powi(exponent);
if tick >= lo * (1.0 - 1e-12) && tick <= hi * (1.0 + 1e-12) {
ticks.push(tick);
}
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).expect("finite ticks are comparable"));
ticks.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * a.abs().max(b.abs()).max(1.0));
if reversed {
ticks.reverse();
}
ticks
}
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
let (lo, hi) = nonsingular(vmin, vmax, self.linthresh, 1e-13, true);
let ticks = self.tick_values(lo, hi);
match (ticks.first(), ticks.last()) {
(Some(first), Some(last)) => {
if vmin <= vmax {
(*first, *last)
} else {
(*last, *first)
}
}
_ => {
if vmin <= vmax {
(-self.linthresh, self.linthresh)
} else {
(self.linthresh, -self.linthresh)
}
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AsinhLocator {
base: f64,
linear_width: f64,
linear_ticks: usize,
}
impl AsinhLocator {
pub fn new(linear_width: f64) -> Self {
Self::with_linear_ticks(10.0, linear_width, 3)
}
pub fn with_linear_ticks(base: f64, linear_width: f64, linear_ticks: usize) -> Self {
assert!(
base.is_finite() && base > 1.0,
"asinh locator base must be finite and > 1"
);
assert!(
linear_width.is_finite() && linear_width > 0.0,
"asinh locator linear_width must be finite and > 0"
);
AsinhLocator {
base,
linear_width,
linear_ticks: linear_ticks.max(2),
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn linear_width(&self) -> f64 {
self.linear_width
}
#[must_use]
pub fn linear_ticks(&self) -> usize {
self.linear_ticks
}
}
impl Default for AsinhLocator {
fn default() -> Self {
Self::new(1.0)
}
}
impl Locator for AsinhLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Asinh {
base: self.base,
linear_width: self.linear_width,
linear_ticks: self.linear_ticks,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() {
return Vec::new();
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let mut ticks = Vec::new();
if lo < -self.linear_width {
let max_abs = (-lo).max(self.linear_width);
let max_exp = (max_abs / self.linear_width).log(self.base).ceil() as i32;
for exponent in (1..=max_exp).rev() {
let tick = -self.linear_width * self.base.powi(exponent);
if tick >= lo * (1.0 + 1e-12) && tick <= hi * (1.0 - 1e-12) {
ticks.push(tick);
}
}
}
let linear_lo = lo.max(-self.linear_width);
let linear_hi = hi.min(self.linear_width);
if linear_lo <= linear_hi {
let step = 2.0 * self.linear_width / (self.linear_ticks - 1) as f64;
for i in 0..self.linear_ticks {
let tick = -self.linear_width + i as f64 * step;
if tick >= linear_lo - 1e-12 && tick <= linear_hi + 1e-12 {
ticks.push(if tick.abs() < 1e-12 { 0.0 } else { tick });
}
}
}
if hi > self.linear_width {
let max_exp = (hi / self.linear_width).log(self.base).ceil() as i32;
for exponent in 1..=max_exp {
let tick = self.linear_width * self.base.powi(exponent);
if tick >= lo * (1.0 - 1e-12) && tick <= hi * (1.0 + 1e-12) {
ticks.push(tick);
}
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).expect("finite ticks are comparable"));
ticks.dedup_by(|a, b| (*a - *b).abs() <= 1e-12 * a.abs().max(b.abs()).max(1.0));
if reversed {
ticks.reverse();
}
ticks
}
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
let (lo, hi) = nonsingular(vmin, vmax, self.linear_width, 1e-13, true);
let lower = if lo < -self.linear_width {
-self.linear_width
* self
.base
.powi(((-lo) / self.linear_width).log(self.base).ceil() as i32)
} else {
-self.linear_width
};
let upper = if hi > self.linear_width {
self.linear_width
* self
.base
.powi((hi / self.linear_width).log(self.base).ceil() as i32)
} else {
self.linear_width
};
if vmin <= vmax {
(lower, upper)
} else {
(upper, lower)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogitLocator {
max_exponent: i32,
}
impl LogitLocator {
pub fn new() -> Self {
Self::with_max_exponent(6)
}
pub fn with_max_exponent(max_exponent: i32) -> Self {
LogitLocator {
max_exponent: max_exponent.max(1),
}
}
#[must_use]
pub fn max_exponent(&self) -> i32 {
self.max_exponent
}
}
impl Default for LogitLocator {
fn default() -> Self {
Self::new()
}
}
impl Locator for LogitLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Logit {
max_exponent: self.max_exponent,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() || vmin <= 0.0 || vmax >= 1.0 {
return Vec::new();
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let mut ticks = Vec::new();
for exponent in (1..=self.max_exponent).rev() {
let p = 10f64.powi(-exponent);
if p >= lo * (1.0 - 1e-12) && p <= hi * (1.0 + 1e-12) {
ticks.push(p);
}
}
if 0.5 >= lo && 0.5 <= hi {
ticks.push(0.5);
}
for exponent in 1..=self.max_exponent {
let p = 1.0 - 10f64.powi(-exponent);
if p >= lo * (1.0 - 1e-12) && p <= hi * (1.0 + 1e-12) {
ticks.push(p);
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).expect("finite ticks are comparable"));
ticks.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
if reversed {
ticks.reverse();
}
ticks
}
fn view_limits(&self, vmin: f64, vmax: f64) -> (f64, f64) {
let lower = 10f64.powi(-self.max_exponent);
let upper = 1.0 - lower;
if !vmin.is_finite() || !vmax.is_finite() {
return (lower, upper);
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let lo = lo.clamp(lower, upper);
let hi = hi.clamp(lower, upper);
if reversed { (hi, lo) } else { (lo, hi) }
}
}
pub struct IndexLocator {
base: f64,
offset: f64,
}
impl IndexLocator {
pub fn new(base: f64, offset: f64) -> Self {
IndexLocator {
base: if base.is_finite() && base > 0.0 {
base
} else {
1.0
},
offset: if offset.is_finite() { offset } else { 0.0 },
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn offset(&self) -> f64 {
self.offset
}
}
impl Locator for IndexLocator {
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Index {
base: self.base,
offset: self.offset,
}))
}
fn tick_values(&self, vmin: f64, vmax: f64) -> Vec<f64> {
if !vmin.is_finite() || !vmax.is_finite() {
return Vec::new();
}
let (lo, hi, reversed) = if vmin <= vmax {
(vmin, vmax, false)
} else {
(vmax, vmin, true)
};
let first = ((lo - self.offset) / self.base).ceil();
let last = ((hi - self.offset) / self.base).floor();
if last < first {
return Vec::new();
}
let count = (last - first) as usize + 1;
let mut ticks: Vec<f64> = (0..count)
.map(|i| self.offset + (first + i as f64) * self.base)
.collect();
if reversed {
ticks.reverse();
}
ticks
}
fn view_limits(&self, dmin: f64, dmax: f64) -> (f64, f64) {
nonsingular(dmin, dmax, 0.001, 1e-15, true)
}
}
#[derive(Default)]
pub struct NullLocator;
impl Locator for NullLocator {
fn tick_values(&self, _vmin: f64, _vmax: f64) -> Vec<f64> {
Vec::new()
}
fn portable_spec(&self) -> Option<PortableLocator> {
Some(PortableLocator(LocatorSpec::Null))
}
}
pub struct ScalarFormatter {
decimals: usize,
have_format: bool,
}
impl ScalarFormatter {
pub fn new() -> Self {
ScalarFormatter {
decimals: 1,
have_format: false,
}
}
pub fn with_decimals(decimals: usize) -> Self {
ScalarFormatter {
decimals,
have_format: false,
}
}
pub fn set_locs(&mut self, locs: &[f64]) {
self.have_format = true;
if let Some(decimals) = spacing_decimals(locs) {
self.decimals = decimals;
}
}
#[must_use]
pub fn decimals(&self) -> usize {
self.decimals
}
#[must_use]
pub fn has_format(&self) -> bool {
self.have_format
}
}
impl Default for ScalarFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for ScalarFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Scalar {
decimals: self.have_format.then_some(self.decimals),
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
let v = if value.abs() < 1e-8 { 0.0 } else { value };
format!("{:.*}", self.decimals, v)
}
fn format_ticks(&self, values: &[f64]) -> Vec<String> {
let decimals = spacing_decimals(values).unwrap_or(self.decimals);
values
.iter()
.map(|&value| {
let v = if value.abs() < 1e-8 { 0.0 } else { value };
format!("{v:.decimals$}")
})
.collect()
}
}
fn spacing_decimals(locs: &[f64]) -> Option<usize> {
if locs.len() < 2 {
return None;
}
let max = locs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = locs.iter().cloned().fold(f64::INFINITY, f64::min);
let mut loc_range = max - min;
if loc_range == 0.0 {
loc_range = locs.iter().map(|x| x.abs()).fold(0.0, f64::max);
}
if loc_range == 0.0 {
loc_range = 1.0;
}
let loc_range_oom = loc_range.log10().floor() as i32;
let mut sigfigs = (3 - loc_range_oom).max(0);
let thresh = 1e-3 * 10f64.powi(loc_range_oom);
while sigfigs >= 0 {
let factor = 10f64.powi(sigfigs);
let maxdev = locs
.iter()
.map(|&x| (x - (x * factor).round() / factor).abs())
.fold(0.0, f64::max);
if maxdev < thresh {
sigfigs -= 1;
} else {
break;
}
}
sigfigs += 1;
Some(sigfigs.max(0) as usize)
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogFormatter {
base: f64,
}
impl LogFormatter {
pub fn new(base: f64) -> Self {
assert!(
base.is_finite() && base > 1.0,
"log formatter base must be finite and > 1"
);
LogFormatter { base }
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
fn exponent(&self, value: f64) -> Option<i32> {
if !value.is_finite() || value <= 0.0 {
return None;
}
let exponent = (value.ln() / self.base.ln()).round();
let decade = self.base.powf(exponent);
let rel = ((value - decade) / decade).abs();
if rel <= 1e-10 {
Some(exponent as i32)
} else {
None
}
}
fn format_exponent(&self, exponent: i32, mathtext: bool) -> String {
let plain_label = (self.base == 10.0 && (-3..=4).contains(&exponent))
|| (self.base == 2.0 && (-3..=6).contains(&exponent));
let value = self.base.powi(exponent);
if plain_label {
format_decimal(value)
} else {
let label = format!("{}^{{{}}}", format_decimal(self.base), exponent);
if mathtext {
format!("${label}$")
} else {
label
}
}
}
}
impl Default for LogFormatter {
fn default() -> Self {
Self::new(10.0)
}
}
impl Formatter for LogFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Log { base: self.base }))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
let Some(exponent) = self.exponent(value) else {
return String::new();
};
self.format_exponent(exponent, false)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogFormatterMathtext {
inner: LogFormatter,
}
impl LogFormatterMathtext {
pub fn new(base: f64) -> Self {
Self {
inner: LogFormatter::new(base),
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.inner.base()
}
}
impl Default for LogFormatterMathtext {
fn default() -> Self {
Self::new(10.0)
}
}
impl Formatter for LogFormatterMathtext {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::LogMathtext {
base: self.inner.base,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
let Some(exponent) = self.inner.exponent(value) else {
return String::new();
};
self.inner.format_exponent(exponent, true)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SymlogFormatter {
base: f64,
linthresh: f64,
}
impl SymlogFormatter {
pub fn new(base: f64, linthresh: f64) -> Self {
assert!(
base.is_finite() && base > 1.0,
"symlog formatter base must be finite and > 1"
);
assert!(
linthresh.is_finite() && linthresh > 0.0,
"symlog formatter linthresh must be finite and > 0"
);
SymlogFormatter { base, linthresh }
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn linthresh(&self) -> f64 {
self.linthresh
}
fn tail_exponent(&self, value: f64) -> Option<i32> {
if !value.is_finite() || value.abs() <= self.linthresh {
return None;
}
let abs = value.abs();
let exponent = (abs / self.linthresh).log(self.base).round();
let tick = self.linthresh * self.base.powf(exponent);
let rel = ((abs - tick) / tick).abs();
if rel <= 1e-10 {
Some(exponent as i32)
} else {
None
}
}
}
impl Default for SymlogFormatter {
fn default() -> Self {
Self::new(10.0, 1.0)
}
}
impl Formatter for SymlogFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Symlog {
base: self.base,
linthresh: self.linthresh,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if value.abs() <= self.linthresh * (1.0 + 1e-12) {
return format_decimal(if value.abs() < 1e-12 { 0.0 } else { value });
}
let Some(exponent) = self.tail_exponent(value) else {
return String::new();
};
if (self.linthresh - 1.0).abs() > 1e-12 {
return format_decimal(value);
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let label = LogFormatter::new(self.base).format_exponent(exponent, false);
format!("{sign}{label}")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SymlogFormatterMathtext {
inner: SymlogFormatter,
}
impl SymlogFormatterMathtext {
pub fn new(base: f64, linthresh: f64) -> Self {
Self {
inner: SymlogFormatter::new(base, linthresh),
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.inner.base()
}
#[must_use]
pub fn linthresh(&self) -> f64 {
self.inner.linthresh()
}
}
impl Default for SymlogFormatterMathtext {
fn default() -> Self {
Self::new(10.0, 1.0)
}
}
impl Formatter for SymlogFormatterMathtext {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::SymlogMathtext {
base: self.inner.base,
linthresh: self.inner.linthresh,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if value.abs() <= self.inner.linthresh * (1.0 + 1e-12) {
return format_decimal(if value.abs() < 1e-12 { 0.0 } else { value });
}
let Some(exponent) = self.inner.tail_exponent(value) else {
return String::new();
};
if (self.inner.linthresh - 1.0).abs() > 1e-12 {
return format_decimal(value);
}
let label = LogFormatter::new(self.inner.base).format_exponent(exponent, true);
if value.is_sign_negative() && label.starts_with('$') && label.ends_with('$') {
format!("$-{}$", &label[1..label.len() - 1])
} else if value.is_sign_negative() {
format!("-{label}")
} else {
label
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AsinhFormatter {
base: f64,
linear_width: f64,
}
impl AsinhFormatter {
pub fn new(base: f64, linear_width: f64) -> Self {
assert!(
base.is_finite() && base > 1.0,
"asinh formatter base must be finite and > 1"
);
assert!(
linear_width.is_finite() && linear_width > 0.0,
"asinh formatter linear_width must be finite and > 0"
);
AsinhFormatter { base, linear_width }
}
#[must_use]
pub fn base(&self) -> f64 {
self.base
}
#[must_use]
pub fn linear_width(&self) -> f64 {
self.linear_width
}
fn tail_exponent(&self, value: f64) -> Option<i32> {
if !value.is_finite() || value.abs() <= self.linear_width {
return None;
}
let abs = value.abs();
let exponent = (abs / self.linear_width).log(self.base).round();
let tick = self.linear_width * self.base.powf(exponent);
let rel = ((abs - tick) / tick).abs();
if rel <= 1e-10 {
Some(exponent as i32)
} else {
None
}
}
}
impl Default for AsinhFormatter {
fn default() -> Self {
Self::new(10.0, 1.0)
}
}
impl Formatter for AsinhFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Asinh {
base: self.base,
linear_width: self.linear_width,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if value.abs() <= self.linear_width * (1.0 + 1e-12) {
return format_decimal(if value.abs() < 1e-12 { 0.0 } else { value });
}
let Some(exponent) = self.tail_exponent(value) else {
return String::new();
};
if (self.linear_width - 1.0).abs() > 1e-12 {
return format_decimal(value);
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let label = LogFormatter::new(self.base).format_exponent(exponent, false);
format!("{sign}{label}")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AsinhFormatterMathtext {
inner: AsinhFormatter,
}
impl AsinhFormatterMathtext {
pub fn new(base: f64, linear_width: f64) -> Self {
Self {
inner: AsinhFormatter::new(base, linear_width),
}
}
#[must_use]
pub fn base(&self) -> f64 {
self.inner.base()
}
#[must_use]
pub fn linear_width(&self) -> f64 {
self.inner.linear_width()
}
}
impl Default for AsinhFormatterMathtext {
fn default() -> Self {
Self::new(10.0, 1.0)
}
}
impl Formatter for AsinhFormatterMathtext {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::AsinhMathtext {
base: self.inner.base,
linear_width: self.inner.linear_width,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if value.abs() <= self.inner.linear_width * (1.0 + 1e-12) {
return format_decimal(if value.abs() < 1e-12 { 0.0 } else { value });
}
let Some(exponent) = self.inner.tail_exponent(value) else {
return String::new();
};
if (self.inner.linear_width - 1.0).abs() > 1e-12 {
return format_decimal(value);
}
let label = LogFormatter::new(self.inner.base).format_exponent(exponent, true);
if value.is_sign_negative() && label.starts_with('$') && label.ends_with('$') {
format!("$-{}$", &label[1..label.len() - 1])
} else if value.is_sign_negative() {
format!("-{label}")
} else {
label
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogitFormatter {
max_exponent: i32,
}
impl LogitFormatter {
pub fn new() -> Self {
Self::with_max_exponent(6)
}
pub fn with_max_exponent(max_exponent: i32) -> Self {
LogitFormatter {
max_exponent: max_exponent.max(1),
}
}
#[must_use]
pub fn max_exponent(&self) -> i32 {
self.max_exponent
}
fn lower_exponent(&self, value: f64) -> Option<i32> {
if !value.is_finite() || value <= 0.0 || value >= 0.5 {
return None;
}
let exponent = -value.log10().round() as i32;
if exponent < 1 || exponent > self.max_exponent {
return None;
}
let tick = 10f64.powi(-exponent);
if ((value - tick) / tick).abs() <= 1e-10 {
Some(exponent)
} else {
None
}
}
fn upper_exponent(&self, value: f64) -> Option<i32> {
if !value.is_finite() || value <= 0.5 || value >= 1.0 {
return None;
}
let q = 1.0 - value;
let exponent = -q.log10().round() as i32;
if exponent < 1 || exponent > self.max_exponent {
return None;
}
let tick = 10f64.powi(-exponent);
if ((q - tick) / tick).abs() <= 1e-10 {
Some(exponent)
} else {
None
}
}
}
impl Default for LogitFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for LogitFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Logit {
max_exponent: self.max_exponent,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if (value - 0.5).abs() <= 1e-12 {
return "1/2".to_owned();
}
if let Some(exponent) = self.lower_exponent(value) {
return if exponent == 1 {
"0.1".to_owned()
} else {
format!("10^{{-{exponent}}}")
};
}
if let Some(exponent) = self.upper_exponent(value) {
return if exponent == 1 {
"0.9".to_owned()
} else {
format!("1-10^{{-{exponent}}}")
};
}
String::new()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LogitFormatterMathtext {
inner: LogitFormatter,
}
impl LogitFormatterMathtext {
pub fn new() -> Self {
Self::with_max_exponent(6)
}
pub fn with_max_exponent(max_exponent: i32) -> Self {
Self {
inner: LogitFormatter::with_max_exponent(max_exponent),
}
}
#[must_use]
pub fn max_exponent(&self) -> i32 {
self.inner.max_exponent()
}
}
impl Default for LogitFormatterMathtext {
fn default() -> Self {
Self::new()
}
}
impl Formatter for LogitFormatterMathtext {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::LogitMathtext {
max_exponent: self.inner.max_exponent,
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if (value - 0.5).abs() <= 1e-12 {
return "1/2".to_owned();
}
if let Some(exponent) = self.inner.lower_exponent(value) {
return if exponent == 1 {
"0.1".to_owned()
} else {
format!("$10^{{-{exponent}}}$")
};
}
if let Some(exponent) = self.inner.upper_exponent(value) {
return if exponent == 1 {
"0.9".to_owned()
} else {
format!("$1-10^{{-{exponent}}}$")
};
}
String::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EngFormatter {
unit: String,
places: Option<usize>,
separator: String,
}
impl EngFormatter {
pub fn new() -> Self {
Self {
unit: String::new(),
places: None,
separator: String::new(),
}
}
#[must_use]
pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
self.unit = unit.into();
self
}
#[must_use]
pub fn with_places(mut self, places: usize) -> Self {
self.places = Some(places);
self
}
#[must_use]
pub fn with_separator(mut self, separator: impl Into<String>) -> Self {
self.separator = separator.into();
self
}
#[must_use]
pub fn unit(&self) -> &str {
&self.unit
}
#[must_use]
pub fn places(&self) -> Option<usize> {
self.places
}
#[must_use]
pub fn separator(&self) -> &str {
&self.separator
}
fn prefix(exponent: i32) -> &'static str {
match exponent {
-24 => "y",
-21 => "z",
-18 => "a",
-15 => "f",
-12 => "p",
-9 => "n",
-6 => "u",
-3 => "m",
0 => "",
3 => "k",
6 => "M",
9 => "G",
12 => "T",
15 => "P",
18 => "E",
21 => "Z",
24 => "Y",
_ => "",
}
}
}
impl Default for EngFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for EngFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Eng {
unit: self.unit.clone(),
places: self.places,
separator: self.separator.clone(),
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
if value == 0.0 {
let suffix = format!("{}{}", Self::prefix(0), self.unit);
return if suffix.is_empty() {
"0".to_owned()
} else {
format!("0{}{}", self.separator, suffix)
};
}
let exponent = ((value.abs().log10().floor() as i32).div_euclid(3) * 3).clamp(-24, 24);
let scaled = value / 10f64.powi(exponent);
let number = if let Some(places) = self.places {
format!("{scaled:.places$}")
} else {
format_decimal(scaled)
};
let suffix = format!("{}{}", Self::prefix(exponent), self.unit);
if suffix.is_empty() {
number
} else {
format!("{number}{}{}", self.separator, suffix)
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PercentFormatter {
xmax: f64,
decimals: usize,
symbol: String,
}
impl PercentFormatter {
pub fn new() -> Self {
Self::with_xmax(100.0)
}
pub fn fraction() -> Self {
Self::with_xmax(1.0)
}
pub fn with_xmax(xmax: f64) -> Self {
assert!(
xmax.is_finite() && xmax != 0.0,
"percent formatter xmax must be finite and non-zero"
);
Self {
xmax,
decimals: 0,
symbol: "%".to_owned(),
}
}
#[must_use]
pub fn with_decimals(mut self, decimals: usize) -> Self {
self.decimals = decimals;
self
}
#[must_use]
pub fn with_symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = symbol.into();
self
}
#[must_use]
pub fn xmax(&self) -> f64 {
self.xmax
}
#[must_use]
pub fn decimals(&self) -> usize {
self.decimals
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
}
impl Default for PercentFormatter {
fn default() -> Self {
Self::new()
}
}
impl Formatter for PercentFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Percent {
xmax: self.xmax,
decimals: self.decimals,
symbol: self.symbol.clone(),
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
let percent = value / self.xmax * 100.0;
format!("{:.*}{}", self.decimals, percent, self.symbol)
}
}
fn format_decimal(value: f64) -> String {
if (value - value.round()).abs() < 1e-10 {
format!("{:.0}", value)
} else {
let mut text = format!("{value:.12}");
while text.contains('.') && text.ends_with('0') {
text.pop();
}
if text.ends_with('.') {
text.pop();
}
text
}
}
#[derive(Default)]
pub struct NullFormatter;
impl Formatter for NullFormatter {
fn format(&self, _value: f64, _pos: Option<usize>) -> String {
String::new()
}
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Null))
}
}
#[derive(Default)]
pub struct FixedFormatter {
seq: Vec<String>,
}
impl FixedFormatter {
pub fn new(seq: Vec<String>) -> Self {
FixedFormatter { seq }
}
pub fn from_labels<I, S>(labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
FixedFormatter {
seq: labels.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn labels(&self) -> &[String] {
&self.seq
}
}
impl Formatter for FixedFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Fixed {
seq: self.seq.clone(),
}))
}
fn format(&self, _value: f64, pos: Option<usize>) -> String {
match pos {
Some(p) if p < self.seq.len() => self.seq[p].clone(),
_ => String::new(),
}
}
}
#[derive(Default)]
pub struct IndexFormatter {
labels: Vec<String>,
}
impl IndexFormatter {
pub fn new(labels: Vec<String>) -> Self {
IndexFormatter { labels }
}
pub fn from_labels<I, S>(labels: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
IndexFormatter {
labels: labels.into_iter().map(Into::into).collect(),
}
}
#[must_use]
pub fn labels(&self) -> &[String] {
&self.labels
}
}
impl Formatter for IndexFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::Index {
labels: self.labels.clone(),
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
if !value.is_finite() {
return String::new();
}
let index = value.round();
if index < 0.0 {
return String::new();
}
let index = index as usize;
self.labels.get(index).cloned().unwrap_or_default()
}
}
pub struct FuncFormatter {
func: Box<dyn Fn(f64, Option<usize>) -> String + Send + Sync>,
}
impl FuncFormatter {
pub fn new(func: Box<dyn Fn(f64, Option<usize>) -> String + Send + Sync>) -> Self {
FuncFormatter { func }
}
pub fn from_fn<F>(func: F) -> Self
where
F: Fn(f64, Option<usize>) -> String + Send + Sync + 'static,
{
Self::new(Box::new(func))
}
}
impl Formatter for FuncFormatter {
fn format(&self, value: f64, pos: Option<usize>) -> String {
(self.func)(value, pos)
}
}
pub struct FormatStrFormatter {
fmt: String,
}
impl FormatStrFormatter {
pub fn new(fmt: impl Into<String>) -> Self {
FormatStrFormatter { fmt: fmt.into() }
}
#[must_use]
pub fn template(&self) -> &str {
&self.fmt
}
}
impl Default for FormatStrFormatter {
fn default() -> Self {
FormatStrFormatter::new("%g")
}
}
impl Formatter for FormatStrFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::FormatStr {
fmt: self.fmt.clone(),
}))
}
fn format(&self, value: f64, _pos: Option<usize>) -> String {
format_percent_template(&self.fmt, value)
}
}
#[derive(Clone, Copy, Debug, Default)]
struct PercentSpec {
left_align: bool,
sign_plus: bool,
sign_space: bool,
zero_pad: bool,
width: Option<usize>,
precision: Option<usize>,
conversion: char,
}
fn format_percent_template(fmt: &str, value: f64) -> String {
let mut out = String::new();
let mut chars = fmt.char_indices().peekable();
let mut formatted_value = false;
while let Some((_, ch)) = chars.next() {
if ch != '%' {
out.push(ch);
continue;
}
if matches!(chars.peek(), Some((_, '%'))) {
chars.next();
out.push('%');
continue;
}
if formatted_value {
out.push('%');
continue;
}
let (spec, raw) = parse_percent_spec(&mut chars);
if let Some(spec) = spec {
out.push_str(&format_percent_value(value, spec));
formatted_value = true;
} else {
out.push('%');
out.push_str(&raw);
}
}
out
}
fn parse_percent_spec<I>(chars: &mut std::iter::Peekable<I>) -> (Option<PercentSpec>, String)
where
I: Iterator<Item = (usize, char)>,
{
let mut raw = String::new();
let mut spec = PercentSpec::default();
while let Some(&(_, ch)) = chars.peek() {
match ch {
'-' => spec.left_align = true,
'+' => spec.sign_plus = true,
' ' => spec.sign_space = true,
'0' => spec.zero_pad = true,
'#' => {}
_ => break,
}
raw.push(ch);
chars.next();
}
let mut width = String::new();
while let Some(&(_, ch)) = chars.peek() {
if !ch.is_ascii_digit() {
break;
}
width.push(ch);
raw.push(ch);
chars.next();
}
if !width.is_empty() {
spec.width = width.parse().ok();
}
if matches!(chars.peek(), Some((_, '.'))) {
raw.push('.');
chars.next();
let mut precision = String::new();
while let Some(&(_, ch)) = chars.peek() {
if !ch.is_ascii_digit() {
break;
}
precision.push(ch);
raw.push(ch);
chars.next();
}
spec.precision = Some(precision.parse().unwrap_or(0));
}
let Some((_, conversion)) = chars.next() else {
return (None, raw);
};
raw.push(conversion);
if matches!(conversion, 'd' | 'i' | 'f' | 'e' | 'E' | 'g' | 'G') {
spec.conversion = conversion;
(Some(spec), raw)
} else {
(None, raw)
}
}
fn format_percent_value(value: f64, spec: PercentSpec) -> String {
let precision = spec.precision.unwrap_or(6);
let mut rendered = match spec.conversion {
'd' | 'i' => format!("{}", value.trunc() as i64),
'f' => format!("{value:.precision$}"),
'e' => format!("{value:.precision$e}"),
'E' => format!("{value:.precision$E}"),
'g' => format_general(value, precision, false),
'G' => format_general(value, precision, true),
_ => value.to_string(),
};
if value >= 0.0 && spec.sign_plus {
rendered.insert(0, '+');
} else if value >= 0.0 && spec.sign_space {
rendered.insert(0, ' ');
}
let Some(width) = spec.width else {
return rendered;
};
if rendered.len() >= width {
return rendered;
}
let pad_len = width - rendered.len();
if spec.left_align {
rendered.push_str(&" ".repeat(pad_len));
} else if spec.zero_pad {
let sign_len = usize::from(rendered.starts_with(['-', '+', ' ']));
rendered.insert_str(sign_len, &"0".repeat(pad_len));
} else {
rendered.insert_str(0, &" ".repeat(pad_len));
}
rendered
}
fn format_general(value: f64, precision: usize, uppercase: bool) -> String {
if value == 0.0 {
return "0".to_owned();
}
let precision = precision.max(1);
let abs = value.abs();
let exponent = abs.log10().floor() as i32;
let mut rendered = if exponent < -4 || exponent >= precision as i32 {
let exp_precision = precision.saturating_sub(1);
if uppercase {
format!("{value:.exp_precision$E}")
} else {
format!("{value:.exp_precision$e}")
}
} else {
let decimals = (precision as i32 - exponent - 1).max(0) as usize;
format!("{value:.decimals$}")
};
if let Some(exp_pos) = rendered.find(['e', 'E']) {
let (mantissa, exponent_part) = rendered.split_at(exp_pos);
let mantissa = trim_decimal_zeros(mantissa);
rendered = format!("{mantissa}{exponent_part}");
} else {
rendered = trim_decimal_zeros(&rendered);
}
rendered
}
fn trim_decimal_zeros(value: &str) -> String {
if let Some(dot) = value.find('.') {
let mut end = value.len();
while end > dot + 1 && value.as_bytes()[end - 1] == b'0' {
end -= 1;
}
if end == dot + 1 {
end -= 1;
}
value[..end].to_owned()
} else {
value.to_owned()
}
}
pub struct StrMethodFormatter {
fmt: String,
}
impl StrMethodFormatter {
pub fn new(fmt: impl Into<String>) -> Self {
StrMethodFormatter { fmt: fmt.into() }
}
#[must_use]
pub fn template(&self) -> &str {
&self.fmt
}
}
impl Default for StrMethodFormatter {
fn default() -> Self {
StrMethodFormatter::new("{x}")
}
}
impl Formatter for StrMethodFormatter {
fn portable_spec(&self) -> Option<PortableFormatter> {
Some(PortableFormatter(FormatterSpec::StrMethod {
fmt: self.fmt.clone(),
}))
}
fn format(&self, value: f64, pos: Option<usize>) -> String {
format_str_method_template(&self.fmt, value, pos)
}
}
fn format_str_method_template(fmt: &str, value: f64, pos: Option<usize>) -> String {
let mut out = String::new();
let mut chars = fmt.char_indices().peekable();
while let Some((_, ch)) = chars.next() {
match ch {
'{' if matches!(chars.peek(), Some((_, '{'))) => {
chars.next();
out.push('{');
}
'}' if matches!(chars.peek(), Some((_, '}'))) => {
chars.next();
out.push('}');
}
'{' => {
let mut field = String::new();
let mut closed = false;
for (_, field_ch) in chars.by_ref() {
if field_ch == '}' {
closed = true;
break;
}
field.push(field_ch);
}
if closed {
if let Some(rendered) = format_str_method_field(&field, value, pos) {
out.push_str(&rendered);
} else {
out.push('{');
out.push_str(&field);
out.push('}');
}
} else {
out.push('{');
out.push_str(&field);
}
}
_ => out.push(ch),
}
}
out
}
fn format_str_method_field(field: &str, value: f64, pos: Option<usize>) -> Option<String> {
let (name, spec) = field.split_once(':').unwrap_or((field, ""));
match name {
"x" => Some(if spec.is_empty() {
value.to_string()
} else {
format_brace_numeric(value, spec)?
}),
"pos" => {
let Some(pos) = pos else {
return Some(String::new());
};
Some(if spec.is_empty() {
pos.to_string()
} else {
format_brace_integer(pos, spec)?
})
}
_ => None,
}
}
fn format_brace_numeric(value: f64, spec: &str) -> Option<String> {
let mut chars = spec.char_indices().peekable();
let (percent, raw) = parse_percent_spec(&mut chars);
if chars.peek().is_some() {
return None;
}
let mut percent = percent?;
if raw != spec {
return None;
}
if percent.conversion == '\0' {
percent.conversion = 'g';
}
Some(format_percent_value(value, percent))
}
fn format_brace_integer(value: usize, spec: &str) -> Option<String> {
let mut chars = spec.char_indices().peekable();
let (percent, raw) = parse_percent_spec(&mut chars);
if chars.peek().is_some() || raw != spec {
return None;
}
let percent = percent?;
match percent.conversion {
'd' | 'i' | 'f' | 'e' | 'E' | 'g' | 'G' => {
Some(format_percent_value(value as f64, percent))
}
_ => None,
}
}
#[cfg(feature = "portable")]
impl crate::portable::spec::LocatorSpec {
pub(crate) fn into_locator(self) -> Result<Box<dyn Locator>, crate::portable::PortableError> {
use crate::portable::PortableError;
use crate::portable::spec::LocatorSpec as L;
Ok(match self {
L::MaxN {
nbins,
extended_steps,
integer,
symmetric,
min_n_ticks,
prune,
} => Box::new(MaxNLocator {
nbins: nbins.map_or(NBins::Auto, NBins::Fixed),
extended_steps,
integer,
symmetric,
min_n_ticks: min_n_ticks.max(1),
prune,
}),
L::Auto => Box::new(AutoLocator::new()),
L::AutoMinor { subdivisions } => Box::new(AutoMinorLocator { subdivisions }),
L::Multiple { step, offset } => {
if !(step.is_finite() && step > 0.0) {
return Err(PortableError::Malformed(
"multiple locator step must be finite and > 0".to_string(),
));
}
Box::new(MultipleLocator::with_offset(step, offset))
}
L::LinearN { numticks } => Box::new(LinearLocator::new(numticks)),
L::Fixed { locs, nbins } => Box::new(FixedLocator { locs, nbins }),
L::Log { base, subs } => {
if !(base.is_finite() && base > 1.0) {
return Err(PortableError::Malformed(
"log locator base must be finite and > 1".to_string(),
));
}
Box::new(LogLocator::with_subs(base, subs))
}
L::Symlog {
base,
linthresh,
linear_ticks,
} => {
if !(base.is_finite() && base > 1.0 && linthresh.is_finite() && linthresh > 0.0) {
return Err(PortableError::Malformed(
"symlog locator needs base > 1 and linthresh > 0".to_string(),
));
}
Box::new(SymlogLocator::with_linear_ticks(
base,
linthresh,
linear_ticks,
))
}
L::Asinh {
base,
linear_width,
linear_ticks,
} => {
if !(base.is_finite()
&& base > 1.0
&& linear_width.is_finite()
&& linear_width > 0.0)
{
return Err(PortableError::Malformed(
"asinh locator needs base > 1 and linear_width > 0".to_string(),
));
}
Box::new(AsinhLocator::with_linear_ticks(
base,
linear_width,
linear_ticks,
))
}
L::Logit { max_exponent } => Box::new(LogitLocator::with_max_exponent(max_exponent)),
L::Index { base, offset } => Box::new(IndexLocator::new(base, offset)),
L::Null => Box::new(NullLocator),
L::AutoDate { maxticks } => {
Box::new(crate::axis::dates::AutoDateLocator::with_maxticks(maxticks))
}
L::AutoDateMinor { maxticks } => Box::new(
crate::axis::dates::AutoDateMinorLocator::with_maxticks(maxticks),
),
})
}
}
#[cfg(feature = "portable")]
impl crate::portable::spec::FormatterSpec {
pub(crate) fn into_formatter(self) -> Box<dyn Formatter> {
use crate::portable::spec::FormatterSpec as F;
match self {
F::Scalar { decimals } => Box::new(match decimals {
Some(decimals) => ScalarFormatter::with_decimals(decimals),
None => ScalarFormatter::new(),
}),
F::Log { base } => Box::new(LogFormatter { base }),
F::LogMathtext { base } => Box::new(LogFormatterMathtext {
inner: LogFormatter { base },
}),
F::Symlog { base, linthresh } => Box::new(SymlogFormatter { base, linthresh }),
F::SymlogMathtext { base, linthresh } => Box::new(SymlogFormatterMathtext {
inner: SymlogFormatter { base, linthresh },
}),
F::Asinh { base, linear_width } => Box::new(AsinhFormatter { base, linear_width }),
F::AsinhMathtext { base, linear_width } => Box::new(AsinhFormatterMathtext {
inner: AsinhFormatter { base, linear_width },
}),
F::Logit { max_exponent } => Box::new(LogitFormatter { max_exponent }),
F::LogitMathtext { max_exponent } => Box::new(LogitFormatterMathtext {
inner: LogitFormatter { max_exponent },
}),
F::Eng {
unit,
places,
separator,
} => Box::new(EngFormatter {
unit,
places,
separator,
}),
F::Percent {
xmax,
decimals,
symbol,
} => Box::new(PercentFormatter {
xmax,
decimals,
symbol,
}),
F::Null => Box::new(NullFormatter),
F::Fixed { seq } => Box::new(FixedFormatter { seq }),
F::Index { labels } => Box::new(IndexFormatter { labels }),
F::FormatStr { fmt } => Box::new(FormatStrFormatter { fmt }),
F::StrMethod { fmt } => Box::new(StrMethodFormatter { fmt }),
F::Date { fmt } => Box::new(crate::axis::dates::DateFormatter::new(fmt)),
F::ConciseDate => Box::new(crate::axis::dates::ConciseDateFormatter::new()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_ticks(actual: &[f64], expected: &[f64]) {
assert_eq!(
actual.len(),
expected.len(),
"length mismatch: got {actual:?}, expected {expected:?}"
);
for (a, e) in actual.iter().zip(expected) {
assert!(
(a - e).abs() < 1e-9,
"value mismatch: got {actual:?}, expected {expected:?}"
);
}
}
#[test]
fn maxn_default_0_1() {
let locs = MaxNLocator::default().tick_values(0.0, 1.0);
assert_ticks(
&locs,
&[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
);
}
#[test]
fn maxn_default_0_100() {
let locs = MaxNLocator::default().tick_values(0.0, 100.0);
assert_ticks(
&locs,
&[
0.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0,
],
);
}
#[test]
fn maxn_default_neg3_7() {
let locs = MaxNLocator::default().tick_values(-3.0, 7.0);
assert_ticks(
&locs,
&[-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0],
);
}
#[test]
fn maxn_default_0_09() {
let locs = MaxNLocator::default().tick_values(0.0, 0.9);
assert_ticks(&locs, &[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]);
}
#[test]
fn maxn_prune_lower_upper_and_both() {
let lower = MaxNLocator::new(NBins::Fixed(4))
.with_prune(TickPrune::Lower)
.tick_values(0.0, 1.0);
let upper = MaxNLocator::new(NBins::Fixed(4))
.with_prune(TickPrune::Upper)
.tick_values(0.0, 1.0);
let both = MaxNLocator::new(NBins::Fixed(4))
.with_prune(TickPrune::Both)
.tick_values(0.0, 1.0);
assert_ticks(&lower, &[0.25, 0.5, 0.75, 1.0]);
assert_ticks(&upper, &[0.0, 0.25, 0.5, 0.75]);
assert_ticks(&both, &[0.25, 0.5, 0.75]);
}
#[test]
fn maxn_prune_keeps_noncoincident_edges() {
let locs = MaxNLocator::new(NBins::Fixed(4))
.with_prune(TickPrune::Both)
.tick_values(0.1, 0.9);
assert_ticks(&locs, &[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
}
#[test]
fn maxn_integer_builder_uses_integer_steps_when_possible() {
let locs = MaxNLocator::new(NBins::Fixed(4))
.with_integer(true)
.tick_values(0.2, 3.8);
assert!(locs.iter().all(|tick| (tick - tick.round()).abs() < 1e-12));
}
#[test]
fn maxn_symmetric_builder_mirrors_about_zero() {
let locs = MaxNLocator::new(NBins::Fixed(4))
.with_symmetric(true)
.tick_values(-1.0, 3.0);
assert_ticks(&locs, &[-3.0, -1.5, 0.0, 1.5, 3.0]);
}
#[test]
fn maxn_min_ticks_builder_clamps_to_at_least_one() {
let locs = MaxNLocator::new(NBins::Fixed(1))
.with_min_n_ticks(0)
.tick_values(0.0, 1.0);
assert!(!locs.is_empty());
}
#[test]
fn maxn_exposes_builder_configuration() {
let locator = MaxNLocator::new(NBins::Fixed(4))
.with_integer(true)
.with_symmetric(true)
.with_min_n_ticks(0)
.with_prune(TickPrune::Both);
assert_eq!(locator.nbins(), NBins::Fixed(4));
assert!(locator.integer());
assert!(locator.symmetric());
assert_eq!(locator.min_n_ticks(), 1);
assert_eq!(locator.prune(), TickPrune::Both);
}
#[test]
fn auto_0_1() {
let locs = AutoLocator::new().tick_values(0.0, 1.0);
assert_ticks(&locs, &[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
}
#[test]
fn auto_neg3_7() {
let locs = AutoLocator::new().tick_values(-3.0, 7.0);
assert_ticks(&locs, &[-4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0]);
}
#[test]
fn auto_minor_uses_four_subdivisions_for_non_12510_major_steps() {
let locs = AutoMinorLocator::new().tick_values(0.0, 1.0);
assert_ticks(
&locs,
&[
0.05, 0.10, 0.15, 0.25, 0.30, 0.35, 0.45, 0.50, 0.55, 0.65, 0.70, 0.75, 0.85, 0.90,
0.95,
],
);
}
#[test]
fn auto_minor_uses_five_subdivisions_for_12510_major_steps() {
let locs = AutoMinorLocator::new().tick_values(0.0, 0.9);
assert_ticks(
&locs,
&[
0.02, 0.04, 0.06, 0.08, 0.12, 0.14, 0.16, 0.18, 0.22, 0.24, 0.26, 0.28, 0.32, 0.34,
0.36, 0.38, 0.42, 0.44, 0.46, 0.48, 0.52, 0.54, 0.56, 0.58, 0.62, 0.64, 0.66, 0.68,
0.72, 0.74, 0.76, 0.78, 0.82, 0.84, 0.86, 0.88,
],
);
}
#[test]
fn auto_minor_anchors_on_majors_offset_from_zero() {
let locs = AutoMinorLocator::new().tick_values_from_major(&[1.0, 6.0, 11.0], 1.0, 11.0);
assert_eq!(locs, vec![2.0, 3.0, 4.0, 5.0, 7.0, 8.0, 9.0, 10.0]);
}
#[test]
fn auto_minor_with_explicit_subdivisions_filters_major_ticks() {
let locs = AutoMinorLocator::with_subdivisions(2).tick_values(0.0, 1.0);
assert_ticks(&locs, &[0.1, 0.3, 0.5, 0.7, 0.9]);
}
#[test]
fn auto_minor_can_use_explicit_major_ticks() {
let locator = AutoMinorLocator::new();
let locs = locator.tick_values_from_major(&[0.0, 0.5, 1.0], 0.0, 1.0);
assert_ticks(&locs, &[0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9]);
}
#[test]
fn auto_minor_explicit_major_ticks_sort_dedup_and_filter_coincidences() {
let locator = AutoMinorLocator::with_subdivisions(2);
let locs = locator.tick_values_from_major(&[1.0, 0.0, 0.5, 0.5, f64::NAN], 0.0, 1.0);
assert_ticks(&locs, &[0.25, 0.75]);
}
#[test]
fn auto_minor_explicit_major_ticks_handle_reversed_ranges() {
let locator = AutoMinorLocator::with_subdivisions(2);
let locs = locator.tick_values_from_major(&[0.0, 0.5, 1.0], 1.0, 0.0);
assert_ticks(&locs, &[0.75, 0.25]);
}
#[test]
fn auto_minor_exposes_configured_subdivisions() {
assert_eq!(AutoMinorLocator::new().subdivisions(), None);
assert_eq!(
AutoMinorLocator::with_subdivisions(4).subdivisions(),
Some(4)
);
}
#[test]
fn auto_minor_handles_reversed_ranges() {
let locs = AutoMinorLocator::with_subdivisions(2).tick_values(1.0, 0.0);
assert_ticks(&locs, &[0.9, 0.7, 0.5, 0.3, 0.1]);
}
#[test]
fn auto_minor_guards_degenerate_inputs() {
assert!(AutoMinorLocator::new().tick_values(1.0, 1.0).is_empty());
assert!(
AutoMinorLocator::new()
.tick_values(f64::NAN, 1.0)
.is_empty()
);
assert!(
AutoMinorLocator::new()
.tick_values(0.0, f64::INFINITY)
.is_empty()
);
assert!(
AutoMinorLocator::with_subdivisions(1)
.tick_values(0.0, 1.0)
.is_empty()
);
assert!(
AutoMinorLocator::new()
.tick_values_from_major(&[0.0], 0.0, 1.0)
.is_empty()
);
assert!(
AutoMinorLocator::new()
.tick_values_from_major(&[0.0, 1.0], 0.0, f64::INFINITY)
.is_empty()
);
}
#[test]
fn multiple_half_0_2() {
let locs = MultipleLocator::new(0.5).tick_values(0.0, 2.0);
assert_ticks(&locs, &[-0.5, 0.0, 0.5, 1.0, 1.5, 2.0, 2.5]);
}
#[test]
fn multiple_locator_exposes_base_and_offset() {
let locator = MultipleLocator::with_offset(0.5, 0.25);
assert_eq!(locator.base(), 0.5);
assert_eq!(locator.offset(), 0.25);
}
#[test]
fn multiple_locator_default_uses_unit_spacing() {
let locator = MultipleLocator::default();
assert_eq!(locator.base(), 1.0);
assert_eq!(locator.offset(), 0.0);
assert_ticks(&locator.tick_values(0.0, 2.0), &[-1.0, 0.0, 1.0, 2.0, 3.0]);
}
#[test]
#[should_panic(expected = "multiple locator base must be finite and > 0")]
fn multiple_locator_rejects_nonpositive_base() {
let _ = MultipleLocator::new(0.0);
}
#[test]
#[should_panic(expected = "multiple locator base must be finite and > 0")]
fn multiple_locator_rejects_nonfinite_base() {
let _ = MultipleLocator::with_offset(f64::NAN, 0.0);
}
#[test]
fn linear_default_0_10() {
let locs = LinearLocator::default().tick_values(0.0, 10.0);
assert_ticks(
&locs,
&[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0],
);
}
#[test]
fn linear_5_0_1() {
let locator = LinearLocator::new(5);
let locs = locator.tick_values(0.0, 1.0);
assert_eq!(locator.numticks(), 5);
assert_ticks(&locs, &[0.0, 0.25, 0.5, 0.75, 1.0]);
}
#[test]
fn reversed_range_is_sorted() {
let locs = MaxNLocator::default().tick_values(1.0, 0.0);
assert_ticks(
&locs,
&[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
);
}
#[test]
fn fixed_locator_passthrough() {
let locs = FixedLocator::new(vec![1.0, 2.0, 3.0]).tick_values(0.0, 10.0);
assert_ticks(&locs, &[1.0, 2.0, 3.0]);
}
#[test]
fn fixed_locator_accepts_iterable_locations() {
let locator = FixedLocator::from_locations([0.0, 0.5, 1.0]);
assert_ticks(locator.locations(), &[0.0, 0.5, 1.0]);
assert_ticks(&locator.tick_values(-10.0, 10.0), &[0.0, 0.5, 1.0]);
assert_eq!(locator.nbins(), None);
}
#[test]
fn fixed_locator_default_has_no_locations() {
let locator = FixedLocator::default();
assert!(locator.locations().is_empty());
assert_eq!(locator.nbins(), None);
assert!(locator.tick_values(0.0, 1.0).is_empty());
}
#[test]
fn fixed_locator_subsample_keeps_zero() {
let locs = FixedLocator::with_nbins(vec![-3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0], 3)
.tick_values(0.0, 0.0);
assert!(
locs.contains(&0.0),
"expected the zero-containing subset: {locs:?}"
);
}
#[test]
fn fixed_locator_exposes_configured_locations_and_nbins() {
let locator = FixedLocator::with_nbins(vec![-1.0, 0.0, 1.0], 1);
assert_eq!(locator.locations(), &[-1.0, 0.0, 1.0]);
assert_eq!(locator.nbins(), Some(2));
assert_eq!(FixedLocator::new(vec![2.0]).nbins(), None);
}
#[test]
fn index_locator_uses_base_and_offset() {
let locator = IndexLocator::new(2.0, 1.0);
let locs = locator.tick_values(0.0, 8.0);
assert_eq!(locator.base(), 2.0);
assert_eq!(locator.offset(), 1.0);
assert_ticks(&locs, &[1.0, 3.0, 5.0, 7.0]);
}
#[test]
fn index_locator_handles_reversed_ranges() {
let locs = IndexLocator::new(2.0, 1.0).tick_values(8.0, 0.0);
assert_ticks(&locs, &[7.0, 5.0, 3.0, 1.0]);
}
#[test]
fn index_locator_guards_invalid_inputs() {
let locs = IndexLocator::new(0.0, 0.0).tick_values(0.0, 3.0);
assert_ticks(&locs, &[0.0, 1.0, 2.0, 3.0]);
let locs = IndexLocator::new(2.0, f64::NAN).tick_values(0.0, 4.0);
assert_ticks(&locs, &[0.0, 2.0, 4.0]);
assert_eq!(IndexLocator::new(2.0, f64::INFINITY).offset(), 0.0);
assert!(
IndexLocator::new(2.0, 0.0)
.tick_values(f64::NAN, 3.0)
.is_empty()
);
}
#[test]
fn log_locator_base10_decades() {
let locs = LogLocator::new(10.0).tick_values(1.0, 1000.0);
assert_ticks(&locs, &[1.0, 10.0, 100.0, 1000.0]);
}
#[test]
fn log_locator_minor_subticks_present() {
let locs = LogLocator::minor(10.0).tick_values(1.0, 20.0);
assert!(locs.contains(&2.0));
assert!(locs.contains(&9.0));
assert!(locs.contains(&20.0));
assert!(!locs.contains(&1.0));
assert!(!locs.contains(&10.0));
}
#[test]
fn log_locator_base2_decades() {
let locs = LogLocator::new(2.0).tick_values(1.0, 32.0);
assert_ticks(&locs, &[1.0, 2.0, 4.0, 8.0, 16.0, 32.0]);
}
#[test]
fn log_locator_rejects_nonpositive_or_nonfinite_domain() {
assert!(LogLocator::new(10.0).tick_values(-1.0, 100.0).is_empty());
assert!(
LogLocator::new(10.0)
.tick_values(1.0, f64::INFINITY)
.is_empty()
);
}
#[test]
fn log_locator_view_limits_snap_to_decades() {
let (lo, hi) = LogLocator::new(10.0).view_limits(3.0, 88.0);
assert_eq!((lo, hi), (1.0, 100.0));
}
#[test]
fn log_formatter_labels_major_ticks_and_hides_minor_ticks() {
let formatter = LogFormatter::new(10.0);
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(10.0, None), "10");
assert_eq!(formatter.format(100.0, None), "100");
assert_eq!(formatter.format(1000.0, None), "1000");
assert_eq!(formatter.format(2.0, None), "");
assert_eq!(formatter.format(1e6, None), "10^{6}");
}
#[test]
fn log_formatter_base2_labels_powers() {
let formatter = LogFormatter::new(2.0);
assert_eq!(formatter.base(), 2.0);
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(8.0, None), "8");
assert_eq!(formatter.format(128.0, None), "2^{7}");
assert_eq!(formatter.format(3.0, None), "");
}
#[test]
fn log_formatter_mathtext_wraps_exponent_labels() {
let formatter = LogFormatterMathtext::new(10.0);
assert_eq!(formatter.format(10.0, None), "10");
assert_eq!(formatter.format(1e6, None), "$10^{6}$");
assert_eq!(formatter.format(2.0, None), "");
}
#[test]
fn log_formatter_mathtext_honors_base2() {
let formatter = LogFormatterMathtext::new(2.0);
assert_eq!(formatter.base(), 2.0);
assert_eq!(formatter.format(8.0, None), "8");
assert_eq!(formatter.format(128.0, None), "$2^{7}$");
}
#[test]
fn symlog_locator_spans_negative_linear_and_positive_regions() {
let locs = SymlogLocator::new(10.0, 1.0).tick_values(-100.0, 100.0);
assert_ticks(&locs, &[-100.0, -10.0, -1.0, 0.0, 1.0, 10.0, 100.0]);
}
#[test]
fn symlog_locator_honors_base_and_linthresh() {
let locator = SymlogLocator::new(2.0, 0.5);
let locs = locator.tick_values(-4.0, 4.0);
assert_eq!(locator.base(), 2.0);
assert_eq!(locator.linthresh(), 0.5);
assert_eq!(locator.linear_ticks(), 3);
assert_ticks(&locs, &[-4.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 4.0]);
}
#[test]
fn symlog_locator_handles_reversed_ranges() {
let locs = SymlogLocator::new(10.0, 1.0).tick_values(100.0, -100.0);
assert_ticks(&locs, &[100.0, 10.0, 1.0, 0.0, -1.0, -10.0, -100.0]);
}
#[test]
fn symlog_locator_rejects_nonfinite_domain() {
assert!(
SymlogLocator::new(10.0, 1.0)
.tick_values(f64::NEG_INFINITY, 100.0)
.is_empty()
);
}
#[test]
fn symlog_formatter_labels_linear_and_tail_ticks() {
let formatter = SymlogFormatter::new(10.0, 1.0);
assert_eq!(formatter.base(), 10.0);
assert_eq!(formatter.linthresh(), 1.0);
assert_eq!(formatter.format(-100.0, None), "-100");
assert_eq!(formatter.format(-1.0, None), "-1");
assert_eq!(formatter.format(0.0, None), "0");
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(100.0, None), "100");
assert_eq!(formatter.format(20.0, None), "");
assert_eq!(formatter.format(1e6, None), "10^{6}");
assert_eq!(formatter.format(-1e6, None), "-10^{6}");
}
#[test]
fn symlog_formatter_nonunit_linthresh_uses_decimal_tail_labels() {
let formatter = SymlogFormatter::new(10.0, 2.0);
assert_eq!(formatter.format(2.0, None), "2");
assert_eq!(formatter.format(20.0, None), "20");
assert_eq!(formatter.format(40.0, None), "");
}
#[test]
fn symlog_formatter_mathtext_wraps_large_tail_labels() {
let formatter = SymlogFormatterMathtext::new(10.0, 1.0);
assert_eq!(formatter.base(), 10.0);
assert_eq!(formatter.linthresh(), 1.0);
assert_eq!(formatter.format(-1.0, None), "-1");
assert_eq!(formatter.format(0.0, None), "0");
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(100.0, None), "100");
assert_eq!(formatter.format(1e6, None), "$10^{6}$");
assert_eq!(formatter.format(-1e6, None), "$-10^{6}$");
assert_eq!(formatter.format(20.0, None), "");
}
#[test]
fn symlog_formatter_mathtext_nonunit_linthresh_uses_decimal_tail_labels() {
let formatter = SymlogFormatterMathtext::new(10.0, 2.0);
assert_eq!(formatter.format(2.0, None), "2");
assert_eq!(formatter.format(20.0, None), "20");
assert_eq!(formatter.format(40.0, None), "");
}
#[test]
fn asinh_locator_spans_negative_linear_and_positive_regions() {
let locs = AsinhLocator::new(1.0).tick_values(-100.0, 100.0);
assert_ticks(&locs, &[-100.0, -10.0, -1.0, 0.0, 1.0, 10.0, 100.0]);
}
#[test]
fn asinh_locator_honors_base_and_linear_width() {
let locator = AsinhLocator::with_linear_ticks(2.0, 0.5, 3);
let locs = locator.tick_values(-4.0, 4.0);
assert_eq!(locator.base(), 2.0);
assert_eq!(locator.linear_width(), 0.5);
assert_eq!(locator.linear_ticks(), 3);
assert_ticks(&locs, &[-4.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 4.0]);
}
#[test]
fn asinh_locator_handles_reversed_ranges() {
let locs = AsinhLocator::new(1.0).tick_values(100.0, -100.0);
assert_ticks(&locs, &[100.0, 10.0, 1.0, 0.0, -1.0, -10.0, -100.0]);
}
#[test]
fn asinh_locator_rejects_nonfinite_domain() {
assert!(
AsinhLocator::new(1.0)
.tick_values(f64::NEG_INFINITY, 100.0)
.is_empty()
);
}
#[test]
fn asinh_locator_view_limits_snap_to_ticks() {
let (lo, hi) = AsinhLocator::new(1.0).view_limits(-32.0, 32.0);
assert_eq!((lo, hi), (-100.0, 100.0));
}
#[test]
fn asinh_formatter_labels_linear_and_tail_ticks() {
let formatter = AsinhFormatter::new(10.0, 1.0);
assert_eq!(formatter.base(), 10.0);
assert_eq!(formatter.linear_width(), 1.0);
assert_eq!(formatter.format(-100.0, None), "-100");
assert_eq!(formatter.format(-1.0, None), "-1");
assert_eq!(formatter.format(0.0, None), "0");
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(100.0, None), "100");
assert_eq!(formatter.format(20.0, None), "");
assert_eq!(formatter.format(1e6, None), "10^{6}");
assert_eq!(formatter.format(-1e6, None), "-10^{6}");
}
#[test]
fn asinh_formatter_nonunit_width_uses_decimal_tail_labels() {
let formatter = AsinhFormatter::new(10.0, 2.0);
assert_eq!(formatter.format(2.0, None), "2");
assert_eq!(formatter.format(20.0, None), "20");
assert_eq!(formatter.format(40.0, None), "");
}
#[test]
fn asinh_formatter_mathtext_wraps_large_tail_labels() {
let formatter = AsinhFormatterMathtext::new(10.0, 1.0);
assert_eq!(formatter.base(), 10.0);
assert_eq!(formatter.linear_width(), 1.0);
assert_eq!(formatter.format(-1.0, None), "-1");
assert_eq!(formatter.format(0.0, None), "0");
assert_eq!(formatter.format(1.0, None), "1");
assert_eq!(formatter.format(100.0, None), "100");
assert_eq!(formatter.format(1e6, None), "$10^{6}$");
assert_eq!(formatter.format(-1e6, None), "$-10^{6}$");
assert_eq!(formatter.format(20.0, None), "");
}
#[test]
fn asinh_formatter_mathtext_nonunit_width_uses_decimal_tail_labels() {
let formatter = AsinhFormatterMathtext::new(10.0, 2.0);
assert_eq!(formatter.format(2.0, None), "2");
assert_eq!(formatter.format(20.0, None), "20");
assert_eq!(formatter.format(40.0, None), "");
}
#[test]
fn logit_locator_clusters_toward_zero_and_one() {
let locs = LogitLocator::with_max_exponent(3).tick_values(0.001, 0.999);
assert_ticks(&locs, &[0.001, 0.01, 0.1, 0.5, 0.9, 0.99, 0.999]);
}
#[test]
fn logit_locator_handles_reversed_ranges() {
let locs = LogitLocator::with_max_exponent(2).tick_values(0.99, 0.01);
assert_ticks(&locs, &[0.99, 0.9, 0.5, 0.1, 0.01]);
}
#[test]
fn logit_locator_rejects_outside_probability_domain() {
assert!(LogitLocator::new().tick_values(0.0, 0.99).is_empty());
assert!(LogitLocator::new().tick_values(0.01, 1.0).is_empty());
assert!(
LogitLocator::new()
.tick_values(0.01, f64::INFINITY)
.is_empty()
);
}
#[test]
fn logit_locator_view_limits_clamp_to_open_interval() {
let (lo, hi) = LogitLocator::with_max_exponent(4).view_limits(-1.0, 2.0);
assert_eq!((lo, hi), (1e-4, 1.0 - 1e-4));
}
#[test]
fn logit_formatter_labels_probability_lattice() {
let formatter = LogitFormatter::with_max_exponent(4);
assert_eq!(formatter.max_exponent(), 4);
assert_eq!(formatter.format(0.1, None), "0.1");
assert_eq!(formatter.format(0.01, None), "10^{-2}");
assert_eq!(formatter.format(0.5, None), "1/2");
assert_eq!(formatter.format(0.9, None), "0.9");
assert_eq!(formatter.format(0.99, None), "1-10^{-2}");
assert_eq!(formatter.format(0.2, None), "");
}
#[test]
fn logit_formatter_mathtext_wraps_tail_labels() {
let formatter = LogitFormatterMathtext::with_max_exponent(4);
assert_eq!(formatter.max_exponent(), 4);
assert_eq!(formatter.format(0.1, None), "0.1");
assert_eq!(formatter.format(0.01, None), "$10^{-2}$");
assert_eq!(formatter.format(0.5, None), "1/2");
assert_eq!(formatter.format(0.9, None), "0.9");
assert_eq!(formatter.format(0.99, None), "$1-10^{-2}$");
assert_eq!(formatter.format(0.2, None), "");
}
#[test]
fn eng_formatter_uses_si_prefixes() {
let formatter = EngFormatter::new();
assert_eq!(formatter.format(1_000.0, None), "1k");
assert_eq!(formatter.format(1_500_000.0, None), "1.5M");
assert_eq!(formatter.format(0.001, None), "1m");
assert_eq!(formatter.format(0.000_001, None), "1u");
assert_eq!(formatter.format(-2_000.0, None), "-2k");
assert_eq!(formatter.format(0.0, None), "0");
}
#[test]
fn eng_formatter_supports_units_separator_and_places() {
let formatter = EngFormatter::new()
.with_unit("Hz")
.with_separator(" ")
.with_places(2);
assert_eq!(formatter.unit(), "Hz");
assert_eq!(formatter.separator(), " ");
assert_eq!(formatter.places(), Some(2));
assert_eq!(formatter.format(12_300.0, None), "12.30 kHz");
assert_eq!(formatter.format(0.0, None), "0 Hz");
}
#[test]
fn percent_formatter_defaults_to_xmax_100() {
let formatter = PercentFormatter::new();
assert_eq!(formatter.format(25.0, None), "25%");
assert_eq!(formatter.format(100.0, None), "100%");
}
#[test]
fn percent_formatter_fraction_maps_one_to_hundred_percent() {
let formatter = PercentFormatter::fraction();
assert_eq!(formatter.xmax(), 1.0);
assert_eq!(formatter.format(0.25, None), "25%");
assert_eq!(formatter.format(1.0, None), "100%");
}
#[test]
fn percent_formatter_supports_fractional_xmax_and_decimals() {
let formatter = PercentFormatter::with_xmax(1.0)
.with_decimals(1)
.with_symbol(" pct");
assert_eq!(formatter.xmax(), 1.0);
assert_eq!(formatter.decimals(), 1);
assert_eq!(formatter.symbol(), " pct");
assert_eq!(formatter.format(0.125, None), "12.5 pct");
assert_eq!(formatter.format(1.0, None), "100.0 pct");
}
#[test]
fn null_locator_empty() {
assert!(NullLocator.tick_values(0.0, 10.0).is_empty());
assert!(
default_value::<NullLocator>()
.tick_values(0.0, 10.0)
.is_empty()
);
}
#[test]
fn null_formatter_empty() {
assert_eq!(NullFormatter.format(2.5, Some(0)), "");
assert_eq!(
NullFormatter.format(2.5, Some(0)),
default_value::<NullFormatter>().format(2.5, Some(0))
);
}
fn default_value<T: Default>() -> T {
T::default()
}
#[test]
fn scalar_formatter_picks_precision() {
let locs = MaxNLocator::default().tick_values(0.0, 1.0);
let mut f = ScalarFormatter::new();
assert!(!f.has_format());
f.set_locs(&locs);
assert!(f.has_format());
assert_eq!(f.decimals(), 1);
assert_eq!(f.format(0.5, Some(5)), "0.5");
assert_eq!(f.format(0.0, Some(0)), "0.0");
}
#[test]
fn scalar_formatter_can_start_with_fixed_decimals() {
let f = ScalarFormatter::with_decimals(3);
assert_eq!(f.decimals(), 3);
assert!(!f.has_format());
assert_eq!(f.format(1.25, Some(0)), "1.250");
}
#[test]
fn scalar_formatter_integers() {
let locs = MaxNLocator::default().tick_values(0.0, 100.0);
let mut f = ScalarFormatter::new();
f.set_locs(&locs);
assert_eq!(f.decimals(), 0);
assert_eq!(f.format(50.0, Some(5)), "50");
assert_eq!(f.format(0.0, Some(0)), "0");
}
#[test]
fn scalar_formatter_format_ticks_derives_precision_from_the_ticks() {
let locs = [500.0125, 500.013, 500.0135, 500.014];
let f = ScalarFormatter::new();
let labels = f.format_ticks(&locs);
assert_eq!(labels, ["500.0125", "500.0130", "500.0135", "500.0140"]);
assert_eq!(
f.format_ticks(&[0.0, 25.0, 50.0, 75.0]),
["0", "25", "50", "75"]
);
}
#[test]
fn scalar_formatter_format_ticks_falls_back_on_short_inputs() {
let f = ScalarFormatter::with_decimals(2);
assert_eq!(f.format_ticks(&[1.5]), ["1.50"]);
assert!(f.format_ticks(&[]).is_empty());
}
#[test]
fn fixed_formatter_by_position() {
let f = FixedFormatter::new(vec!["a".into(), "b".into()]);
assert_eq!(f.labels(), &["a".to_string(), "b".to_string()]);
assert_eq!(f.format(0.0, Some(0)), "a");
assert_eq!(f.format(99.0, Some(1)), "b");
assert_eq!(f.format(0.0, Some(2)), "");
assert_eq!(f.format(0.0, None), "");
}
#[test]
fn fixed_formatter_accepts_convertible_labels() {
let f = FixedFormatter::from_labels(["zero", "one", "two"]);
assert_eq!(
f.labels(),
&["zero".to_string(), "one".to_string(), "two".to_string()]
);
assert_eq!(f.format(0.0, Some(2)), "two");
assert_eq!(f.format(0.0, Some(3)), "");
}
#[test]
fn fixed_formatter_default_has_no_labels() {
let f = FixedFormatter::default();
assert!(f.labels().is_empty());
assert_eq!(f.format(0.0, Some(0)), "");
}
#[test]
fn index_formatter_uses_rounded_tick_value() {
let f = IndexFormatter::new(vec!["zero".into(), "one".into(), "two".into()]);
assert_eq!(
f.labels(),
&["zero".to_string(), "one".to_string(), "two".to_string()]
);
assert_eq!(f.format(0.2, Some(99)), "zero");
assert_eq!(f.format(0.6, None), "one");
assert_eq!(f.format(2.49, None), "two");
}
#[test]
fn index_formatter_accepts_convertible_labels() {
let f = IndexFormatter::from_labels(["zero", "one", "two"]);
assert_eq!(
f.labels(),
&["zero".to_string(), "one".to_string(), "two".to_string()]
);
assert_eq!(f.format(1.4, None), "one");
assert_eq!(f.format(2.6, None), "");
}
#[test]
fn index_formatter_default_has_no_labels() {
let f = IndexFormatter::default();
assert!(f.labels().is_empty());
assert_eq!(f.format(0.0, None), "");
}
#[test]
fn index_formatter_out_of_range_is_empty() {
let f = IndexFormatter::new(vec!["zero".into(), "one".into()]);
assert_eq!(f.format(-0.6, None), "");
assert_eq!(f.format(2.0, None), "");
assert_eq!(f.format(f64::NAN, None), "");
}
#[test]
fn func_formatter_closure() {
let f = FuncFormatter::new(Box::new(|x, _| format!("{x:.0} km")));
assert_eq!(f.format(10.0, None), "10 km");
let f = FuncFormatter::from_fn(|x, pos| format!("{pos:?}:{x:.1}"));
assert_eq!(f.format(2.25, Some(3)), "Some(3):2.2");
}
#[test]
fn format_str_formatter_fixed_and_escaped_percent() {
let f = FormatStrFormatter::new("x=%+.2f%%");
assert_eq!(f.template(), "x=%+.2f%%");
assert_eq!(f.format(1.234, None), "x=+1.23%");
assert_eq!(f.format(-1.234, None), "x=-1.23%");
}
#[test]
fn format_str_formatter_width_zero_pad_and_exp() {
let f = FormatStrFormatter::new("%08.1e");
assert_eq!(f.format(12.0, None), "0001.2e1");
}
#[test]
fn format_str_formatter_integer_conversion() {
let f = FormatStrFormatter::new("%+05d");
assert_eq!(f.format(12.9, None), "+0012");
assert_eq!(f.format(-12.9, None), "-0012");
}
#[test]
fn format_str_formatter_general_trims_decimal_zeros() {
let f = FormatStrFormatter::new("%g");
let upper = FormatStrFormatter::new("%.3G");
assert_eq!(f.format(12.300, None), "12.3");
assert_eq!(upper.format(12345.0, None), "1.23E4");
}
#[test]
fn format_str_formatter_default_uses_general_template() {
let f = FormatStrFormatter::default();
assert_eq!(f.template(), "%g");
assert_eq!(f.format(12.300, None), "12.3");
}
#[test]
fn format_str_formatter_unsupported_spec_is_preserved() {
let f = FormatStrFormatter::new("x=%q");
assert_eq!(f.format(3.0, None), "x=%q");
}
#[test]
fn str_method_formatter_template() {
let f = StrMethodFormatter::new("{x} m");
assert_eq!(f.template(), "{x} m");
assert_eq!(f.format(3.0, None), "3 m");
let g = StrMethodFormatter::new("{x}@{pos}");
assert_eq!(g.format(2.0, Some(4)), "2@4");
}
#[test]
fn str_method_formatter_default_uses_x_template() {
let f = StrMethodFormatter::default();
assert_eq!(f.template(), "{x}");
assert_eq!(f.format(3.0, Some(2)), "3");
}
#[test]
fn str_method_formatter_numeric_format_spec() {
let f = StrMethodFormatter::new("x={x:+08.1f}");
assert_eq!(f.format(12.0, None), "x=+00012.0");
assert_eq!(f.format(-12.0, None), "x=-00012.0");
}
#[test]
fn str_method_formatter_integer_format_specs() {
let f = StrMethodFormatter::new("x={x:04d} pos={pos:02d}");
assert_eq!(f.format(12.9, Some(3)), "x=0012 pos=03");
}
#[test]
fn str_method_formatter_exp_and_general_specs() {
let exp = StrMethodFormatter::new("{x:.2e}");
let general = StrMethodFormatter::new("{x:.3G}");
assert_eq!(exp.format(1234.0, None), "1.23e3");
assert_eq!(general.format(1234.0, None), "1.23E3");
}
#[test]
fn str_method_formatter_escapes_braces() {
let f = StrMethodFormatter::new("{{{x:.1f}}}");
assert_eq!(f.format(3.25, None), "{3.2}");
}
#[test]
fn str_method_formatter_preserves_unknown_fields() {
let f = StrMethodFormatter::new("{label}={x:.1f}");
assert_eq!(f.format(2.0, None), "{label}=2.0");
}
#[test]
fn str_method_formatter_missing_pos_is_empty() {
let f = StrMethodFormatter::new("{x}@{pos}");
assert_eq!(f.format(2.0, None), "2@");
}
#[test]
fn view_limits_default_identity_for_multiple() {
let (lo, hi) = MultipleLocator::new(0.5).view_limits(0.0, 2.0);
assert!((lo - 0.0).abs() < 1e-9 && (hi - 2.0).abs() < 1e-9);
}
}