pub mod discrete;
pub mod linear;
pub mod log;
pub mod mapper;
pub mod temporal;
use self::discrete::DiscreteScale;
use self::linear::LinearScale;
use self::log::LogScale;
use self::mapper::VisualMapper;
use self::temporal::TemporalScale;
use crate::core::utils::IntoParallelizable;
use crate::error::ChartonError;
use std::sync::{Arc, RwLock};
use time::OffsetDateTime;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct Expansion {
pub mult: (f64, f64),
pub add: (f64, f64),
}
impl Default for Expansion {
fn default() -> Self {
Self {
mult: (0.05, 0.05),
add: (0.0, 0.0),
}
}
}
#[derive(Debug, Clone)]
pub struct Tick {
pub value: f64,
pub label: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ExplicitTick {
Continuous(f64),
Discrete(String),
Timestamp(i64),
Temporal(OffsetDateTime),
}
pub trait IntoExplicitTicks {
fn into_explicit_ticks(self) -> Vec<ExplicitTick>;
}
impl IntoExplicitTicks for Vec<f64> {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter().map(ExplicitTick::Continuous).collect()
}
}
impl<const N: usize> IntoExplicitTicks for [f64; N] {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter().map(ExplicitTick::Continuous).collect()
}
}
impl IntoExplicitTicks for Vec<&str> {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter()
.map(|s| ExplicitTick::Discrete(s.to_string()))
.collect()
}
}
impl IntoExplicitTicks for Vec<i64> {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter().map(ExplicitTick::Timestamp).collect()
}
}
impl<const N: usize> IntoExplicitTicks for [i64; N] {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter().map(ExplicitTick::Timestamp).collect()
}
}
impl IntoExplicitTicks for Vec<OffsetDateTime> {
fn into_explicit_ticks(self) -> Vec<ExplicitTick> {
self.into_iter().map(ExplicitTick::Temporal).collect()
}
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum Scale {
Linear,
Log,
Discrete,
Temporal,
}
impl Scale {
pub fn normalize_column(
&self,
scale_trait: &dyn ScaleTrait,
column: &crate::core::data::ColumnVector,
) -> Vec<Option<f64>> {
(0..column.len())
.maybe_into_par_iter()
.map(|i| {
match self {
Scale::Discrete => column.get_str(i).map(|s| scale_trait.normalize_string(&s)),
Scale::Linear | Scale::Log => {
column.get_f64(i).map(|v| scale_trait.normalize(v))
}
Scale::Temporal => column.get_f64(i).map(|v| scale_trait.normalize(v)),
}
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScaleDomain {
Continuous(f64, f64),
Discrete(Vec<String>),
Temporal(i64, i64), }
pub trait ScaleTrait: std::fmt::Debug + Send + Sync {
fn scale_type(&self) -> Scale;
fn normalize(&self, value: f64) -> f64;
fn normalize_string(&self, value: &str) -> f64;
fn domain(&self) -> (f64, f64);
fn logical_max(&self) -> f64;
fn mapper(&self) -> Option<&VisualMapper>;
fn suggest_ticks(&self, count: usize) -> Vec<Tick>;
fn create_explicit_ticks(&self, explicit: &[ExplicitTick]) -> Vec<Tick>;
fn get_domain_enum(&self) -> ScaleDomain;
fn sample_n(&self, n: usize) -> Vec<Tick>;
}
pub fn create_scale(
scale_type: &Scale,
domain_data: ScaleDomain,
expansion: Expansion,
mapper: Option<VisualMapper>, ) -> Result<Arc<dyn ScaleTrait>, ChartonError> {
let scale: Box<dyn ScaleTrait> = match scale_type {
Scale::Linear => {
if let ScaleDomain::Continuous(min, max) = domain_data {
let range = max - min;
let lower_padding = range * expansion.mult.0 + expansion.add.0;
let upper_padding = range * expansion.mult.1 + expansion.add.1;
Box::new(LinearScale::new(
(min - lower_padding, max + upper_padding),
mapper,
))
} else {
return Err(ChartonError::Scale(
"Linear scale requires Continuous domain".into(),
));
}
}
Scale::Log => {
if let ScaleDomain::Continuous(min, max) = domain_data {
let log_min = min.ln();
let log_max = max.ln();
let log_range = log_max - log_min;
let expanded_min = (log_min - log_range * expansion.mult.0).exp();
let expanded_max = (log_max + log_range * expansion.mult.1).exp();
Box::new(LogScale::new((expanded_min, expanded_max), 10.0, mapper)?)
} else {
return Err(ChartonError::Scale(
"Log scale requires Continuous domain".into(),
));
}
}
Scale::Discrete => {
if let ScaleDomain::Discrete(categories) = domain_data {
Box::new(DiscreteScale::new(categories, expansion, mapper))
} else {
return Err(ChartonError::Scale(
"Discrete scale requires Categorical domain".into(),
));
}
}
Scale::Temporal => {
if let ScaleDomain::Temporal(min_ns, max_ns) = domain_data {
let diff_ns = (max_ns - min_ns) as f64;
let lower_pad_ns = (diff_ns * expansion.mult.0 + expansion.add.0 * 1e9) as i64;
let upper_pad_ns = (diff_ns * expansion.mult.1 + expansion.add.1 * 1e9) as i64;
Box::new(TemporalScale::new(
(min_ns - lower_pad_ns, max_ns + upper_pad_ns),
mapper,
))
} else {
return Err(ChartonError::Scale(
"Time scale requires Temporal domain".into(),
));
}
}
};
Ok(Arc::from(scale))
}
pub fn get_normalized_value(
scale_trait: &dyn ScaleTrait,
scale_type: &Scale,
value: &ExplicitTick,
) -> f64 {
match scale_type {
Scale::Discrete => {
let label = match value {
ExplicitTick::Discrete(s) => s.clone(),
ExplicitTick::Continuous(v) => v.to_string(),
ExplicitTick::Timestamp(ts) => ts.to_string(),
ExplicitTick::Temporal(dt) => dt.to_string(),
};
scale_trait.normalize_string(&label)
}
_ => match value {
ExplicitTick::Continuous(v) => scale_trait.normalize(*v),
ExplicitTick::Timestamp(ns) => scale_trait.normalize(*ns as f64),
ExplicitTick::Temporal(dt) => scale_trait.normalize(dt.unix_timestamp_nanos() as f64),
ExplicitTick::Discrete(_) => {
unreachable!("Discrete values are blocked for cotinuous scales by validataion")
}
},
}
}
pub(crate) fn format_ticks(values: &[f64]) -> Vec<Tick> {
if values.is_empty() {
return vec![];
}
let use_sci = values.iter().any(|&v| {
let a = v.abs();
a != 0.0 && (a >= 10000.0 || a <= 0.001)
});
let step = if values.len() > 1 {
(values[1] - values[0]).abs()
} else {
values[0].abs()
};
let mut precision = if use_sci {
let max_val = values.iter().map(|v| v.abs()).fold(0.0, f64::max);
let magnitude = if max_val > 0.0 {
max_val.log10().floor()
} else {
0.0
};
let step_mag = if step > 0.0 {
step.log10().floor()
} else {
magnitude
};
((magnitude - step_mag).max(0.0) as usize).clamp(0, 6)
} else if step > 0.0 && step < 0.9999 {
((-step.log10()).ceil() as usize).clamp(0, 6)
} else {
0
};
let mut labels: Vec<String> = values
.iter()
.map(|&v| {
if use_sci {
format!("{:.*e}", precision, v).replace("e", "E")
} else {
format!("{:.*}", precision, v)
}
})
.collect();
if precision > 0 {
let all_redundant = labels.iter().all(|l| {
if let Some(dot_idx) = l.find('.') {
let end_idx = l.find('E').unwrap_or(l.len());
l[dot_idx + 1..end_idx].chars().all(|c| c == '0')
} else {
true
}
});
if all_redundant {
precision = 0;
labels = values
.iter()
.map(|&v| {
if use_sci {
format!("{:.*e}", precision, v).replace("e", "E")
} else {
format!("{:.*}", precision, v)
}
})
.collect();
}
}
values
.iter()
.zip(labels)
.map(|(&v, l)| Tick { value: v, label: l })
.collect()
}
#[derive(Debug)]
pub struct ResolvedScale(pub(crate) RwLock<Option<Arc<dyn ScaleTrait>>>);
impl ResolvedScale {
pub fn new(scale: Option<Arc<dyn ScaleTrait>>) -> Self {
Self(RwLock::new(scale))
}
pub fn none() -> Self {
Self::new(None)
}
}
impl Clone for ResolvedScale {
fn clone(&self) -> Self {
let guard = self.0.read().unwrap();
let inner_clone = guard.clone();
Self(RwLock::new(inner_clone))
}
}