use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, TimeDelta, Timelike};
use crate::chart::chart_data::{XAxisTemporalKind, x_datetime, x_time};
const MOST_TICKS: f64 = 4096.0;
pub fn nice_steps(lo: f64, hi: f64, finest: f64, whole: bool) -> Vec<f64> {
let span = hi - lo;
if !(span > 0.0 && span.is_finite()) {
return Vec::new();
}
let finest = finest.max(span / MOST_TICKS);
let mut exp = finest.log10().floor() as i32;
let mut steps = Vec::new();
loop {
for m in [1.0, 2.0, 2.5, 5.0] {
let step = m * 10f64.powi(exp);
if step < finest * (1.0 - 1e-9) || (whole && step < 1.0) || (m == 2.5 && exp < 1) {
continue;
}
steps.push(step);
if step >= span {
return steps;
}
}
exp += 1;
}
}
fn clean(v: f64, step: f64) -> f64 {
let places = (-(step.log10() + 1e-9).floor()).max(0.0) as i32 + 1;
let scale = 10f64.powi(places.min(15));
let rounded = (v * scale).round() / scale;
if rounded == 0.0 { 0.0 } else { rounded }
}
pub fn multiples(lo: f64, hi: f64, step: f64) -> Vec<f64> {
if step.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) || (hi - lo) / step > MOST_TICKS
{
return Vec::new();
}
let first = (lo / step - 1e-9).ceil() as i64;
let last = (hi / step + 1e-9).floor() as i64;
(first..=last)
.map(|k| clean(k as f64 * step, step))
.collect()
}
pub fn widen(lo: f64, hi: f64, step: f64) -> [f64; 2] {
let wlo = clean((lo / step + 1e-9).floor() * step, step);
let mut whi = clean((hi / step - 1e-9).ceil() * step, step);
if whi <= wlo {
whi = clean(wlo + step, step);
}
[wlo, whi]
}
pub fn widen_snug(lo: f64, hi: f64, step: f64, whole: bool) -> [f64; 2] {
let [wlo, whi] = widen(lo, hi, step);
let Some(&minor) = minor_steps(step, whole).last() else {
return [wlo, whi];
};
let [mlo, mhi] = widen(lo, hi, minor);
let most = step * 0.75;
[
if lo - wlo > most { mlo } else { wlo },
if whi - hi > most { mhi } else { whi },
]
}
pub fn minor_steps(step: f64, whole: bool) -> Vec<f64> {
let mantissa = step / 10f64.powf((step.log10() + 1e-9).floor());
let divisors: &[f64] = if (mantissa - 1.0).abs() < 1e-6 {
&[5.0, 2.0]
} else if (mantissa - 2.0).abs() < 1e-6 {
&[4.0, 2.0]
} else {
&[5.0]
};
divisors
.iter()
.map(|d| step / d)
.filter(|s| !whole || *s >= 1.0 - 1e-9)
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Unit {
Second,
Minute,
Hour,
Day,
Month,
Year,
}
impl Unit {
fn seconds(self) -> f64 {
match self {
Unit::Second => 1.0,
Unit::Minute => 60.0,
Unit::Hour => 3600.0,
Unit::Day => 86_400.0,
Unit::Month => 2_629_746.0,
Unit::Year => 31_556_952.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CalendarStep {
pub unit: Unit,
pub n: u32,
}
const CALENDAR_STEPS: &[(Unit, &[u32])] = &[
(Unit::Second, &[1, 2, 5, 10, 15, 30]),
(Unit::Minute, &[1, 2, 5, 10, 15, 30]),
(Unit::Hour, &[1, 2, 3, 6, 12]),
(Unit::Day, &[1, 2, 7, 14]),
(Unit::Month, &[1, 2, 3, 6]),
(Unit::Year, &[1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]),
];
pub fn calendar_steps(kind: XAxisTemporalKind) -> impl Iterator<Item = CalendarStep> {
CALENDAR_STEPS
.iter()
.flat_map(|(unit, ns)| ns.iter().map(move |&n| CalendarStep { unit: *unit, n }))
.filter(move |s| match kind {
XAxisTemporalKind::Date => s.unit >= Unit::Day,
XAxisTemporalKind::Time => s.unit <= Unit::Hour,
_ => true,
})
}
pub fn to_datetime(v: f64, kind: XAxisTemporalKind) -> Option<NaiveDateTime> {
match kind {
XAxisTemporalKind::Numeric => None,
XAxisTemporalKind::Time => Some(epoch_day().and_time(x_time(v)?)),
_ => x_datetime(v, kind),
}
}
fn epoch_day() -> NaiveDate {
NaiveDate::from_ymd_opt(1970, 1, 1).unwrap_or_default()
}
pub fn from_datetime(at: NaiveDateTime, kind: XAxisTemporalKind) -> Option<f64> {
let utc = at.and_utc();
Some(match kind {
XAxisTemporalKind::Numeric => return None,
XAxisTemporalKind::Date => (at.date() - epoch_day()).num_days() as f64,
XAxisTemporalKind::DatetimeUs => utc.timestamp_micros() as f64,
XAxisTemporalKind::DatetimeMs => utc.timestamp_millis() as f64,
XAxisTemporalKind::DatetimeNs => utc.timestamp_nanos_opt()? as f64,
XAxisTemporalKind::Time => {
let t = at.time();
f64::from(t.num_seconds_from_midnight()) * 1e9 + f64::from(t.nanosecond())
}
})
}
pub fn calendar_ticks(
lo: NaiveDateTime,
hi: NaiveDateTime,
step: CalendarStep,
) -> Vec<NaiveDateTime> {
let span = (hi - lo).as_seconds_f64();
let n = step.n.max(1);
if span < 0.0 || span / (step.unit.seconds() * f64::from(n)) > MOST_TICKS {
return Vec::new();
}
let midnight = |d: NaiveDate| d.and_time(NaiveTime::MIN);
let mut ticks = Vec::new();
let mut push = |at: NaiveDateTime| {
if at >= lo && at <= hi {
ticks.push(at);
}
at <= hi
};
match step.unit {
Unit::Year => {
let n = n as i32;
let mut year = lo.year().div_euclid(n) * n;
while let Some(d) = NaiveDate::from_ymd_opt(year, 1, 1) {
if !push(midnight(d)) {
break;
}
year += n;
}
}
Unit::Month => {
let n = n as i32;
let first = lo.year() * 12 + lo.month0() as i32;
let mut month = first - first.rem_euclid(n);
while let Some(d) =
NaiveDate::from_ymd_opt(month.div_euclid(12), month.rem_euclid(12) as u32 + 1, 1)
{
if !push(midnight(d)) {
break;
}
month += n;
}
}
Unit::Day if n == 1 => {
let mut day = lo.date();
while push(midnight(day)) {
let Some(next) = day.succ_opt() else { break };
day = next;
}
}
Unit::Day => {
let last = if n == 2 { 29 } else { 28 };
let (mut year, mut month) = (lo.year(), lo.month());
'months: while let Some(first) = NaiveDate::from_ymd_opt(year, month, 1) {
for day in (1..=last).step_by(n as usize) {
if let Some(d) = first.with_day(day)
&& !push(midnight(d))
{
break 'months;
}
}
(year, month) = if month == 12 {
(year + 1, 1)
} else {
(year, month + 1)
};
}
}
Unit::Hour | Unit::Minute | Unit::Second => {
let every = step.unit.seconds() as i64 * i64::from(n);
let day = midnight(lo.date());
let into = (lo - day).num_seconds();
let mut at = day + TimeDelta::seconds(into.div_euclid(every) * every);
while push(at) {
at += TimeDelta::seconds(every);
}
}
}
ticks
}
pub fn calendar_labels(
ticks: &[NaiveDateTime],
unit: Unit,
kind: XAxisTemporalKind,
context: bool,
) -> Vec<String> {
ticks
.iter()
.enumerate()
.map(|(i, at)| {
let pattern = if kind == XAxisTemporalKind::Time {
if unit == Unit::Second {
"%H:%M:%S"
} else {
"%H:%M"
}
} else {
calendar_pattern(at, unit, context && i == 0)
};
at.format(pattern).to_string()
})
.collect()
}
fn calendar_pattern(at: &NaiveDateTime, unit: Unit, first: bool) -> &'static str {
let day_start = at.time() == NaiveTime::MIN;
let month_start = day_start && at.day() == 1;
let year_start = month_start && at.month() == 1;
if year_start || unit == Unit::Year {
return "%Y";
}
match unit {
Unit::Month if first => "%b %Y",
Unit::Month => "%b",
Unit::Day if first => "%b %-d %Y",
_ if month_start && !first => "%b",
Unit::Day => "%-d",
_ if day_start && first => "%b %-d %Y",
_ if day_start => "%b %-d",
Unit::Second if first => "%b %-d %Y %H:%M:%S",
Unit::Second => "%H:%M:%S",
_ if first => "%b %-d %Y %H:%M",
_ => "%H:%M",
}
}
#[cfg(test)]
mod tests;