use crate::scale::Scale;
#[derive(Debug, Clone)]
pub struct Tick {
pub value: f64,
pub label: String,
}
#[derive(Debug, Clone)]
pub struct TickSet {
pub positions: Vec<f64>,
pub labels: Vec<String>,
}
impl TickSet {
pub fn into_ticks(self) -> Vec<Tick> {
self.positions
.into_iter()
.zip(self.labels)
.map(|(value, label)| Tick { value, label })
.collect()
}
}
pub fn generate_ticks(
data_min: f64,
data_max: f64,
target_count: usize,
scale: &Scale,
) -> Vec<Tick> {
let tick_set = match scale {
Scale::Linear => generate_linear_ticks(data_min, data_max, target_count),
Scale::Log10 => generate_log_ticks(data_min, data_max, target_count),
Scale::SymLog { linthresh } => {
generate_symlog_ticks(data_min, data_max, target_count, *linthresh)
}
};
tick_set.into_ticks()
}
const Q: [f64; 6] = [1.0, 5.0, 2.0, 2.5, 4.0, 3.0];
const W_SIMPLICITY: f64 = 0.2;
const W_COVERAGE: f64 = 0.25;
const W_DENSITY: f64 = 0.35;
const W_LEGIBILITY: f64 = 0.2;
fn generate_linear_ticks(data_min: f64, data_max: f64, target_count: usize) -> TickSet {
if !data_min.is_finite() || !data_max.is_finite() {
return make_tick_set(vec![0.0]);
}
let (dmin, dmax) = if (data_max - data_min).abs() < f64::EPSILON * 100.0 {
if data_min == 0.0 {
(-1.0, 1.0)
} else {
let delta = data_min.abs() * 0.1;
(data_min - delta, data_min + delta)
}
} else if data_min > data_max {
(data_max, data_min)
} else {
(data_min, data_max)
};
let target = target_count.max(2) as f64;
let range = dmax - dmin;
let mut best_score = f64::NEG_INFINITY;
let mut best_ticks: Option<Vec<f64>> = None;
for (qi, &q) in Q.iter().enumerate() {
let j_min = 1_usize;
let j_max = (target as usize * 3).max(12);
for j in j_min..=j_max {
let j_f = j as f64;
let density = density_score(j_f + 1.0, target, range);
let max_possible = W_SIMPLICITY + W_COVERAGE + W_DENSITY * density + W_LEGIBILITY;
if max_possible < best_score {
continue;
}
let ideal_step = range / j_f;
let k_float = (ideal_step / q).log10().floor();
for k_offset in -2_i32..=2 {
let k = k_float as i32 + k_offset;
let step = q * 10.0_f64.powi(k);
if step <= 0.0 || !step.is_finite() {
continue;
}
let i_min = ((dmin / step).ceil() - j_f) as i64;
let i_max = (dmin / step).floor() as i64 + 1;
for i in i_min..=i_max {
let tick_min = i as f64 * step;
let tick_max = tick_min + j_f * step;
if tick_max < dmax - step * 0.5 {
continue;
}
if tick_min > dmin + step * 0.5 {
continue;
}
let num_ticks = j + 1;
let ticks: Vec<f64> = (0..num_ticks)
.map(|t| {
let v = tick_min + t as f64 * step;
snap_to_step(v, step)
})
.collect();
let simplicity = simplicity_score(qi, &ticks);
let coverage = coverage_score(tick_min, tick_max, dmin, dmax);
let density = density_score(num_ticks as f64, target, range);
let legibility = legibility_score(&ticks);
let score = W_SIMPLICITY * simplicity
+ W_COVERAGE * coverage
+ W_DENSITY * density
+ W_LEGIBILITY * legibility;
if score > best_score {
best_score = score;
best_ticks = Some(ticks);
}
}
}
}
}
let ticks = best_ticks.unwrap_or_else(|| {
let step = range / target;
(0..=target as usize)
.map(|i| dmin + i as f64 * step)
.collect()
});
make_tick_set(ticks)
}
fn simplicity_score(q_index: usize, ticks: &[f64]) -> f64 {
let q_len = Q.len() as f64;
let q_penalty = q_index as f64 / q_len;
let zero_bonus = if ticks.iter().any(|&v| v.abs() < f64::EPSILON * 100.0) {
1.0
} else {
0.0
};
1.0 - q_penalty + zero_bonus * 0.2
}
fn coverage_score(tick_min: f64, tick_max: f64, dmin: f64, dmax: f64) -> f64 {
let data_range = dmax - dmin;
if data_range <= 0.0 {
return 1.0;
}
if tick_min > dmin + data_range * 0.001 || tick_max < dmax - data_range * 0.001 {
return 0.0;
}
let tick_range = tick_max - tick_min;
let overshoot_ratio = (tick_range - data_range) / data_range;
(1.0 - 0.5 * overshoot_ratio * overshoot_ratio).max(0.0)
}
fn density_score(num_ticks: f64, target: f64, _range: f64) -> f64 {
let ratio = if target > 0.0 {
num_ticks / target
} else {
1.0
};
let raw = 2.0 - ratio.max(1.0 / ratio);
raw.clamp(0.0, 1.0)
}
fn legibility_score(ticks: &[f64]) -> f64 {
if ticks.is_empty() {
return 1.0;
}
let total: f64 = ticks.iter().map(|&v| single_legibility(v)).sum();
total / ticks.len() as f64
}
fn single_legibility(value: f64) -> f64 {
let label = format_tick(value);
let len = label.len();
if len <= 3 {
1.0
} else if len <= 5 {
0.9
} else if len <= 7 {
0.75
} else if len <= 10 {
0.5
} else {
0.3
}
}
fn generate_log_ticks(data_min: f64, data_max: f64, target_count: usize) -> TickSet {
let lo = data_min.max(f64::EPSILON);
let hi = data_max.max(lo);
let log_lo = lo.log10().floor() as i32;
let log_hi = hi.log10().ceil() as i32;
let decades = (log_hi - log_lo) as usize;
if decades <= 1 {
return generate_linear_ticks(lo, hi, target_count);
}
let mut positions = Vec::new();
if decades <= 3 {
for exp in log_lo..=log_hi {
let base = 10.0_f64.powi(exp);
for &mult in &[1.0, 2.0, 5.0] {
let val = base * mult;
if val >= lo * 0.999 && val <= hi * 1.001 {
positions.push(val);
}
}
}
} else {
let skip = ((decades as f64) / (target_count.max(2) as f64)).ceil() as i32;
let skip = skip.max(1);
let mut exp = log_lo;
while exp <= log_hi {
let val = 10.0_f64.powi(exp);
if val >= lo * 0.999 && val <= hi * 1.001 {
positions.push(val);
}
exp += skip;
}
let last = 10.0_f64.powi(log_hi);
if positions.last().map_or(true, |&v| (v - last).abs() > f64::EPSILON)
&& last <= hi * 1.001 {
positions.push(last);
}
}
if positions.is_empty() {
positions.push(lo);
positions.push(hi);
}
make_tick_set(positions)
}
pub fn generate_log_minor_ticks(data_min: f64, data_max: f64) -> Vec<f64> {
let lo = data_min.max(f64::EPSILON);
let hi = data_max.max(lo);
let log_lo = lo.log10().floor() as i32;
let log_hi = hi.log10().ceil() as i32;
let mut positions = Vec::new();
for exp in log_lo..=log_hi {
let base = 10.0_f64.powi(exp);
for mult in 2..=9 {
let val = base * mult as f64;
if val >= lo * 0.999 && val <= hi * 1.001 {
positions.push(val);
}
}
}
positions
}
fn generate_symlog_ticks(data_min: f64, data_max: f64, target_count: usize, linthresh: f64) -> TickSet {
if linthresh <= 0.0 || !linthresh.is_finite() {
return generate_linear_ticks(data_min, data_max, target_count);
}
let mut positions = Vec::new();
if data_min <= 0.0 && data_max >= 0.0 {
positions.push(0.0);
}
if linthresh <= data_max && linthresh >= data_min {
positions.push(linthresh);
}
if -linthresh >= data_min && -linthresh <= data_max {
positions.push(-linthresh);
}
if data_max > linthresh {
let log_lo = linthresh.log10().ceil() as i32;
let log_hi = data_max.abs().log10().ceil() as i32;
for exp in log_lo..=log_hi {
let val = 10.0_f64.powi(exp);
if val > linthresh && val <= data_max * 1.001 {
positions.push(val);
}
}
}
if data_min < -linthresh {
let log_lo = linthresh.log10().ceil() as i32;
let log_hi = data_min.abs().log10().ceil() as i32;
for exp in log_lo..=log_hi {
let val = -10.0_f64.powi(exp);
if val < -linthresh && val >= data_min * 1.001 {
positions.push(val);
}
}
}
let lin_lo = data_min.max(-linthresh);
let lin_hi = data_max.min(linthresh);
if lin_hi > lin_lo {
let lin_ticks = generate_linear_ticks(lin_lo, lin_hi, (target_count / 3).max(2));
for &pos in &lin_ticks.positions {
positions.push(pos);
}
}
positions.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
positions.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON * 100.0);
if positions.len() < 2 {
return generate_linear_ticks(data_min, data_max, target_count);
}
make_tick_set(positions)
}
pub fn format_tick_value(value: f64) -> String {
format_tick(value)
}
fn format_tick(value: f64) -> String {
if value == 0.0 {
return "0".to_string();
}
let abs = value.abs();
if (0.001..1_000_000.0).contains(&abs) {
let decimals = needed_decimals(value);
let formatted = format!("{:.prec$}", value, prec = decimals);
strip_trailing_zeros(&formatted)
} else {
let formatted = format!("{:.6e}", value);
clean_scientific(&formatted)
}
}
fn needed_decimals(value: f64) -> usize {
let abs = value.abs();
if abs == abs.floor() && abs < 1e15 {
return 0;
}
for d in 1..=10 {
let factor = 10.0_f64.powi(d as i32);
let rounded = (value * factor).round() / factor;
if (rounded - value).abs() < f64::EPSILON * abs.max(1.0) * 10.0 {
return d;
}
}
10
}
fn strip_trailing_zeros(s: &str) -> String {
if !s.contains('.') {
return s.to_string();
}
let trimmed = s.trim_end_matches('0');
let trimmed = trimmed.trim_end_matches('.');
trimmed.to_string()
}
fn clean_scientific(s: &str) -> String {
if let Some(e_pos) = s.find('e') {
let mantissa = &s[..e_pos];
let exponent = &s[e_pos..]; let cleaned_mantissa = strip_trailing_zeros(mantissa);
format!("{}{}", cleaned_mantissa, exponent)
} else {
s.to_string()
}
}
fn snap_to_step(value: f64, step: f64) -> f64 {
if step == 0.0 {
return value;
}
let n = (value / step).round();
let mut result = n * step;
let magnitude = step.abs().log10().floor() as i32;
let mantissa = step.abs() / 10.0_f64.powi(magnitude);
let mantissa_decimals = {
let mut d = 0usize;
for test_d in 0..=5 {
let factor = 10.0_f64.powi(test_d as i32);
let scaled = mantissa * factor;
if (scaled - scaled.round()).abs() < 1e-6 {
d = test_d;
break;
}
d = test_d;
}
d
};
let total_decimals = (mantissa_decimals as i32 - magnitude).max(0) as u32;
if total_decimals <= 15 {
let factor = 10.0_f64.powi(total_decimals as i32);
result = (result * factor).round() / factor;
}
if result.abs() < step.abs() * 1e-10 {
0.0
} else {
result
}
}
fn make_tick_set(positions: Vec<f64>) -> TickSet {
let labels = positions.iter().map(|&v| format_tick(v)).collect();
TickSet { positions, labels }
}
#[cfg(test)]
mod tests {
use super::*;
fn positions(ticks: &[Tick]) -> Vec<f64> {
ticks.iter().map(|t| t.value).collect()
}
fn labels(ticks: &[Tick]) -> Vec<&str> {
ticks.iter().map(|t| t.label.as_str()).collect()
}
fn assert_nice(ticks: &[Tick]) {
assert!(!ticks.is_empty(), "tick set should not be empty");
for w in ticks.windows(2) {
assert!(
w[1].value >= w[0].value,
"ticks must be sorted: {} came before {}",
w[0].value,
w[1].value
);
}
}
fn assert_covers(ticks: &[Tick], dmin: f64, dmax: f64) {
let first = ticks.first().unwrap().value;
let last = ticks.last().unwrap().value;
let step = if ticks.len() >= 2 {
ticks[1].value - ticks[0].value
} else {
(dmax - dmin).abs().max(1.0)
};
assert!(
first <= dmin + step * 0.01,
"first tick {} should be <= data_min {} (step={})",
first,
dmin,
step
);
assert!(
last >= dmax - step * 0.01,
"last tick {} should be >= data_max {} (step={})",
last,
dmax,
step
);
}
#[test]
fn range_0_10() {
let ticks = generate_ticks(0.0, 10.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, 0.0, 10.0);
assert_eq!(positions(&ticks), vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
assert_eq!(labels(&ticks), vec!["0", "2", "4", "6", "8", "10"]);
}
#[test]
fn range_0_1() {
let ticks = generate_ticks(0.0, 1.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, 0.0, 1.0);
assert_eq!(positions(&ticks), vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]);
assert_eq!(labels(&ticks), vec!["0", "0.2", "0.4", "0.6", "0.8", "1"]);
}
#[test]
fn range_neg5_pos5() {
let ticks = generate_ticks(-5.0, 5.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, -5.0, 5.0);
let pos = positions(&ticks);
assert!(
pos.contains(&0.0),
"ticks for [-5,5] should include zero: {:?}",
pos
);
assert!(*pos.first().unwrap() <= -5.0);
assert!(*pos.last().unwrap() >= 5.0);
}
#[test]
fn range_0_100() {
let ticks = generate_ticks(0.0, 100.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, 0.0, 100.0);
assert_eq!(positions(&ticks), vec![0.0, 20.0, 40.0, 60.0, 80.0, 100.0]);
assert_eq!(labels(&ticks), vec!["0", "20", "40", "60", "80", "100"]);
}
#[test]
fn range_0_1e6() {
let ticks = generate_ticks(0.0, 1_000_000.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, 0.0, 1_000_000.0);
assert_eq!(
positions(&ticks),
vec![0.0, 200_000.0, 400_000.0, 600_000.0, 800_000.0, 1_000_000.0]
);
}
#[test]
fn range_0001_001() {
let ticks = generate_ticks(0.001, 0.01, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, 0.001, 0.01);
let pos = positions(&ticks);
let first = *pos.first().unwrap();
let last = *pos.last().unwrap();
assert!(first <= 0.001 + 1e-12);
assert!(last >= 0.01 - 1e-12);
}
#[test]
fn tick_count_reasonable() {
for (lo, hi) in &[
(0.0, 10.0),
(0.0, 1.0),
(-100.0, 100.0),
(0.0, 0.005),
(1.0, 2.0),
] {
let ticks = generate_ticks(*lo, *hi, 6, &Scale::Linear);
assert!(
ticks.len() >= 3 && ticks.len() <= 15,
"range [{}, {}] produced {} ticks (expected 3-15): {:?}",
lo,
hi,
ticks.len(),
positions(&ticks)
);
}
}
#[test]
fn degenerate_same_min_max() {
let ticks = generate_ticks(5.0, 5.0, 6, &Scale::Linear);
assert!(!ticks.is_empty(), "should produce ticks even for degenerate range");
}
#[test]
fn degenerate_zero_range() {
let ticks = generate_ticks(0.0, 0.0, 6, &Scale::Linear);
assert!(!ticks.is_empty());
}
#[test]
fn reversed_range() {
let ticks = generate_ticks(10.0, 0.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert!(ticks.first().unwrap().value <= 0.0 + 0.01);
assert!(ticks.last().unwrap().value >= 10.0 - 0.01);
}
#[test]
fn log_ticks_basic() {
let ticks = generate_ticks(1.0, 10000.0, 5, &Scale::Log10);
assert_nice(&ticks);
assert!(!ticks.is_empty());
for t in &ticks {
assert!(t.value > 0.0, "log tick should be positive: {}", t.value);
}
}
#[test]
fn log_ticks_narrow() {
let ticks = generate_ticks(1.0, 5.0, 5, &Scale::Log10);
assert!(!ticks.is_empty());
}
#[test]
fn format_zero() {
assert_eq!(format_tick(0.0), "0");
}
#[test]
fn format_integer() {
assert_eq!(format_tick(42.0), "42");
assert_eq!(format_tick(-7.0), "-7");
}
#[test]
fn format_decimal() {
assert_eq!(format_tick(0.5), "0.5");
assert_eq!(format_tick(2.5), "2.5");
assert_eq!(format_tick(0.25), "0.25");
}
#[test]
fn format_no_trailing_zeros() {
assert_eq!(format_tick(1.0), "1");
assert_eq!(format_tick(10.0), "10");
assert_eq!(format_tick(0.2), "0.2");
}
#[test]
fn format_scientific() {
let label = format_tick(1e-8);
assert!(
label.contains('e'),
"very small numbers should use scientific notation: {}",
label
);
}
#[test]
fn symlog_ticks() {
let ticks = generate_ticks(-100.0, 100.0, 6, &Scale::SymLog { linthresh: 1.0 });
assert_nice(&ticks);
let pos = positions(&ticks);
assert!(
pos.contains(&0.0),
"symlog ticks for symmetric range should include zero: {:?}",
pos
);
}
#[test]
fn strip_zeros() {
assert_eq!(strip_trailing_zeros("1.200"), "1.2");
assert_eq!(strip_trailing_zeros("3.0"), "3");
assert_eq!(strip_trailing_zeros("100"), "100");
assert_eq!(strip_trailing_zeros("0.00100"), "0.001");
}
#[test]
fn density_score_perfect() {
let s = density_score(6.0, 6.0, 10.0);
assert!((s - 1.0).abs() < 1e-10, "perfect density score should be 1.0, got {}", s);
}
#[test]
fn density_score_degrades() {
let s6 = density_score(6.0, 6.0, 10.0);
let s12 = density_score(12.0, 6.0, 10.0);
assert!(s6 > s12, "density should degrade as tick count diverges from target");
}
#[test]
fn coverage_score_perfect() {
let s = coverage_score(0.0, 10.0, 0.0, 10.0);
assert!(
(s - 1.0).abs() < 1e-10,
"perfect coverage should be 1.0, got {}",
s
);
}
#[test]
fn coverage_score_overshoot() {
let s_tight = coverage_score(0.0, 10.0, 0.0, 10.0);
let s_wide = coverage_score(-5.0, 15.0, 0.0, 10.0);
assert!(
s_tight > s_wide,
"tighter coverage should score higher: {} vs {}",
s_tight,
s_wide
);
}
#[test]
fn simplicity_prefers_earlier_q() {
let ticks_with_zero = vec![0.0, 1.0, 2.0];
let s0 = simplicity_score(0, &ticks_with_zero); let s2 = simplicity_score(2, &ticks_with_zero); assert!(s0 > s2, "q=1 should score higher on simplicity than q=2");
}
#[test]
fn large_range_no_panic() {
let ticks = generate_ticks(0.0, 1e12, 6, &Scale::Linear);
assert_nice(&ticks);
assert!(!ticks.is_empty());
}
#[test]
fn tiny_range_no_panic() {
let ticks = generate_ticks(1e-10, 2e-10, 6, &Scale::Linear);
assert_nice(&ticks);
assert!(!ticks.is_empty());
}
#[test]
fn negative_range() {
let ticks = generate_ticks(-100.0, -10.0, 6, &Scale::Linear);
assert_nice(&ticks);
assert_covers(&ticks, -100.0, -10.0);
for t in &ticks {
assert!(t.value <= 0.0, "ticks for negative range should be non-positive: {}", t.value);
}
}
#[test]
fn tick_set_into_ticks() {
let ts = make_tick_set(vec![0.0, 5.0, 10.0]);
let ticks = ts.into_ticks();
assert_eq!(ticks.len(), 3);
assert_eq!(ticks[0].value, 0.0);
assert_eq!(ticks[0].label, "0");
assert_eq!(ticks[1].value, 5.0);
assert_eq!(ticks[1].label, "5");
assert_eq!(ticks[2].value, 10.0);
assert_eq!(ticks[2].label, "10");
}
#[test]
fn log_ticks_powers_of_10() {
let ticks = generate_ticks(1.0, 10_000.0, 7, &Scale::Log10);
assert_nice(&ticks);
let pos = positions(&ticks);
assert!(pos.contains(&1.0), "should include 10^0 = 1: {:?}", pos);
assert!(pos.contains(&10.0), "should include 10^1 = 10: {:?}", pos);
assert!(pos.contains(&100.0), "should include 10^2 = 100: {:?}", pos);
assert!(pos.contains(&1000.0), "should include 10^3 = 1000: {:?}", pos);
assert!(pos.contains(&10000.0), "should include 10^4 = 10000: {:?}", pos);
}
#[test]
fn log_ticks_all_positive() {
let ticks = generate_ticks(0.01, 1_000_000.0, 7, &Scale::Log10);
for t in &ticks {
assert!(t.value > 0.0, "log tick must be positive, got {}", t.value);
}
}
#[test]
fn log_ticks_large_range() {
let ticks = generate_ticks(1e-5, 1e5, 7, &Scale::Log10);
assert_nice(&ticks);
assert!(ticks.len() >= 3, "should have at least 3 ticks: {:?}", positions(&ticks));
}
#[test]
fn log_ticks_small_values() {
let ticks = generate_ticks(0.001, 0.1, 5, &Scale::Log10);
assert!(!ticks.is_empty());
for t in &ticks {
assert!(t.value > 0.0);
}
}
#[test]
fn log_minor_ticks_basic() {
let minor = generate_log_minor_ticks(1.0, 100.0);
assert!(!minor.is_empty());
assert!(minor.contains(&2.0), "should include 2: {:?}", minor);
assert!(minor.contains(&5.0), "should include 5: {:?}", minor);
assert!(minor.contains(&9.0), "should include 9: {:?}", minor);
assert!(minor.contains(&20.0), "should include 20: {:?}", minor);
assert!(minor.contains(&50.0), "should include 50: {:?}", minor);
assert!(minor.contains(&90.0), "should include 90: {:?}", minor);
}
#[test]
fn log_minor_ticks_all_positive() {
let minor = generate_log_minor_ticks(0.01, 1000.0);
for &v in &minor {
assert!(v > 0.0, "minor tick must be positive, got {}", v);
}
}
#[test]
fn log_minor_ticks_sorted() {
let minor = generate_log_minor_ticks(1.0, 10000.0);
for w in minor.windows(2) {
assert!(w[1] >= w[0], "minor ticks not sorted: {} before {}", w[0], w[1]);
}
}
#[test]
fn symlog_ticks_include_zero_dedicated() {
let ticks = generate_ticks(-100.0, 100.0, 7, &Scale::SymLog { linthresh: 1.0 });
let pos = positions(&ticks);
assert!(pos.contains(&0.0), "symlog ticks should include zero: {:?}", pos);
}
#[test]
fn symlog_ticks_include_linthresh() {
let ticks = generate_ticks(-1000.0, 1000.0, 7, &Scale::SymLog { linthresh: 10.0 });
let pos = positions(&ticks);
assert!(pos.contains(&10.0), "should include +linthresh=10: {:?}", pos);
assert!(pos.contains(&-10.0), "should include -linthresh=-10: {:?}", pos);
}
#[test]
fn symlog_ticks_sorted_dedicated() {
let ticks = generate_ticks(-1000.0, 1000.0, 7, &Scale::SymLog { linthresh: 1.0 });
assert_nice(&ticks);
}
#[test]
fn symlog_ticks_positive_only() {
let ticks = generate_ticks(0.1, 10000.0, 7, &Scale::SymLog { linthresh: 1.0 });
assert!(!ticks.is_empty());
assert_nice(&ticks);
}
#[test]
fn symlog_ticks_negative_only() {
let ticks = generate_ticks(-10000.0, -0.1, 7, &Scale::SymLog { linthresh: 1.0 });
assert!(!ticks.is_empty());
assert_nice(&ticks);
}
#[test]
fn symlog_ticks_degenerate() {
let ticks = generate_ticks(-0.5, 0.5, 5, &Scale::SymLog { linthresh: 1.0 });
assert!(!ticks.is_empty());
}
}