use chrono::{Datelike, TimeZone, Timelike};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EpochUnit {
Seconds,
Milliseconds,
Microseconds,
Nanoseconds,
}
impl EpochUnit {
pub fn units_per_second(&self) -> f64 {
match self {
EpochUnit::Seconds => 1.0,
EpochUnit::Milliseconds => 1_000.0,
EpochUnit::Microseconds => 1_000_000.0,
EpochUnit::Nanoseconds => 1_000_000_000.0,
}
}
pub fn to_seconds(&self, value: f64) -> f64 {
value / self.units_per_second()
}
}
impl Default for EpochUnit {
fn default() -> Self {
EpochUnit::Seconds
}
}
impl std::fmt::Display for EpochUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EpochUnit::Seconds => write!(f, "s"),
EpochUnit::Milliseconds => write!(f, "ms"),
EpochUnit::Microseconds => write!(f, "µs"),
EpochUnit::Nanoseconds => write!(f, "ns"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TimeResolution {
Seconds,
Milliseconds,
Microseconds,
Nanoseconds,
}
impl std::fmt::Display for TimeResolution {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeResolution::Seconds => write!(f, "Seconds"),
TimeResolution::Milliseconds => write!(f, "Milliseconds"),
TimeResolution::Microseconds => write!(f, "Microseconds"),
TimeResolution::Nanoseconds => write!(f, "Nanoseconds"),
}
}
}
#[derive(Debug, Clone)]
pub struct TimeFormatter {
pub epoch_unit: EpochUnit,
pub force_date_visible: bool,
pub force_show_year: bool,
pub milliseconds_threshold: f64,
pub microseconds_threshold: f64,
pub nanoseconds_threshold: f64,
pub min_resolution: TimeResolution,
pub max_resolution: TimeResolution,
pub available_width_pixels: Option<f32>,
}
impl Default for TimeFormatter {
fn default() -> Self {
Self {
epoch_unit: EpochUnit::Seconds,
force_date_visible: false,
force_show_year: false,
milliseconds_threshold: 3_600.0,
microseconds_threshold: 60.0,
nanoseconds_threshold: 1.0,
min_resolution: TimeResolution::Seconds,
max_resolution: TimeResolution::Nanoseconds,
available_width_pixels: None,
}
}
}
impl TimeFormatter {
pub fn for_epoch_unit(epoch_unit: EpochUnit) -> Self {
Self {
epoch_unit,
..Self::default()
}
}
pub fn format(&self, value_raw: f64, x_range_raw: (f64, f64)) -> String {
let ups = self.epoch_unit.units_per_second();
let value_secs = value_raw / ups;
let range_start_secs = x_range_raw.0 / ups;
let range_end_secs = x_range_raw.1 / ups;
let (range_lo_secs, range_hi_secs) = if range_start_secs <= range_end_secs {
(range_start_secs, range_end_secs)
} else {
(range_end_secs, range_start_secs)
};
let range_span_secs = range_hi_secs - range_lo_secs;
let start_dt = secs_to_local(range_lo_secs);
let end_dt = secs_to_local(range_hi_secs);
let value_dt = secs_to_local(value_secs);
let date_changes = start_dt.date_naive() != end_dt.date_naive();
let year_changes = start_dt.year() != end_dt.year();
let show_date = date_changes || self.force_date_visible;
let show_year = show_date && (year_changes || self.force_show_year);
let resolution = self.determine_resolution(range_span_secs);
let base = if show_date {
if show_year {
value_dt.format("%Y-%m-%d %H:%M:%S").to_string()
} else {
value_dt.format("%m-%d %H:%M:%S").to_string()
}
} else {
value_dt.format("%H:%M:%S").to_string()
};
match resolution {
TimeResolution::Seconds => base,
TimeResolution::Milliseconds => {
let ms = value_dt.nanosecond() / 1_000_000;
format!("{}.{:03}", base, ms)
}
TimeResolution::Microseconds => {
let us = value_dt.nanosecond() / 1_000;
format!("{}.{:06}", base, us)
}
TimeResolution::Nanoseconds => {
let ns = value_dt.nanosecond();
format!("{}.{:09}", base, ns)
}
}
}
pub fn determine_resolution(&self, range_span_secs: f64) -> TimeResolution {
let mut res = self.min_resolution;
if TimeResolution::Milliseconds > self.min_resolution
&& TimeResolution::Milliseconds <= self.max_resolution
&& range_span_secs < self.milliseconds_threshold
{
res = TimeResolution::Milliseconds;
}
if TimeResolution::Microseconds <= self.max_resolution
&& range_span_secs < self.microseconds_threshold
&& !self.too_narrow_for_sub_millisecond()
{
res = TimeResolution::Microseconds;
}
if TimeResolution::Nanoseconds <= self.max_resolution
&& range_span_secs < self.nanoseconds_threshold
&& !self.too_narrow_for_sub_millisecond()
{
res = TimeResolution::Nanoseconds;
}
res.max(self.min_resolution).min(self.max_resolution)
}
fn too_narrow_for_sub_millisecond(&self) -> bool {
self.available_width_pixels
.map(|w| w < 120.0)
.unwrap_or(false)
}
}
fn secs_to_local(secs: f64) -> chrono::DateTime<chrono::Local> {
if !secs.is_finite() {
return chrono::Local
.timestamp_opt(0, 0)
.single()
.unwrap_or_default();
}
let s = secs.floor() as i64;
let ns_frac = ((secs - s as f64) * 1e9).round() as u32;
let ns_frac = ns_frac.min(999_999_999);
chrono::DateTime::from_timestamp(s, ns_frac)
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap())
.with_timezone(&chrono::Local)
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecimalFormatter {
pub decimal_places: Option<usize>,
pub unit: Option<String>,
}
impl Default for DecimalFormatter {
fn default() -> Self {
Self {
decimal_places: None,
unit: None,
}
}
}
impl DecimalFormatter {
pub fn format(&self, value: f64, dec_pl: usize) -> String {
let places = self.decimal_places.unwrap_or(dec_pl);
let s = format!("{:.*}", places, value);
match &self.unit {
Some(u) => format!("{} {}", s, u),
None => s,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScientificFormatter {
pub significant_digits: Option<usize>,
pub unit: Option<String>,
}
impl Default for ScientificFormatter {
fn default() -> Self {
Self {
significant_digits: None,
unit: None,
}
}
}
impl ScientificFormatter {
pub fn format(&self, value: f64, dec_pl: usize) -> String {
let digits = self.significant_digits.unwrap_or(dec_pl);
let formatted = format_scientific(value, digits);
match &self.unit {
Some(u) => format!("{} {}", formatted, u),
None => formatted,
}
}
}
fn format_scientific(value: f64, digits: usize) -> String {
if value == 0.0 {
return format!("{:.*}", digits, 0.0_f64);
}
if !value.is_finite() {
return format!("{}", value);
}
let sign = if value < 0.0 { -1.0 } else { 1.0 };
let abs_val = value.abs();
let exp = abs_val.log10().floor() as i32;
let mantissa = sign * abs_val / 10f64.powi(exp);
if exp == 0 {
format!("{:.*}", digits, mantissa)
} else {
format!("{:.*}e{}", digits, mantissa, exp)
}
}
#[derive(Debug, Clone)]
pub enum XFormatter {
Auto,
Decimal(DecimalFormatter),
Scientific(ScientificFormatter),
Time(Box<TimeFormatter>),
}
impl Default for XFormatter {
fn default() -> Self {
XFormatter::Auto
}
}
impl XFormatter {
pub fn time(tf: TimeFormatter) -> Self {
XFormatter::Time(Box::new(tf))
}
pub fn auto() -> Self {
XFormatter::Auto
}
pub fn is_auto(&self) -> bool {
matches!(self, XFormatter::Auto)
}
pub fn format_time_value(
&self,
value: f64,
x_bounds: (f64, f64),
dec_pl: usize,
_step: f64,
) -> String {
match self {
XFormatter::Auto | XFormatter::Time(_) => {
let tf = match self {
XFormatter::Time(tf) => tf.as_ref(),
_ => return Self::default_time_format(value, x_bounds),
};
tf.format(value, x_bounds)
}
XFormatter::Decimal(df) => df.format(value, dec_pl),
XFormatter::Scientific(sf) => sf.format(value, dec_pl),
}
}
pub fn format_numeric_value(&self, value: f64, dec_pl: usize, step: f64) -> String {
match self {
XFormatter::Auto | XFormatter::Decimal(_) => {
let df = match self {
XFormatter::Decimal(df) => return df.format(value, dec_pl),
_ => &DecimalFormatter::default(),
};
format_adaptive_numeric(value, dec_pl, step, df.unit.as_deref())
}
XFormatter::Scientific(sf) => sf.format(value, dec_pl),
XFormatter::Time(tf) => {
tf.format(value, (value, value))
}
}
}
fn default_time_format(value: f64, x_bounds: (f64, f64)) -> String {
let tf = TimeFormatter::default();
tf.format(value, x_bounds)
}
}
fn format_adaptive_numeric(v: f64, dec_pl: usize, step: f64, unit: Option<&str>) -> String {
let sci = if step.is_finite() && step != 0.0 {
let exp = step.abs().log10().floor() as i32;
exp < -(dec_pl as i32) || exp >= dec_pl as i32
} else {
false
};
let formatted = if sci {
format_scientific(v, dec_pl)
} else {
format!("{:.*}", dec_pl, v)
};
match unit {
Some(u) => format!("{} {}", formatted, u),
None => formatted,
}
}