use crate::color::{lerp_color, ColorSpace};
use super::breaks::{
extended_breaks, linear_minor_breaks_between, log_minor_breaks, log_pretty_breaks, sqrt_breaks,
symlog_breaks, symlog_minor_breaks, temporal_breaks_date, temporal_breaks_datetime,
temporal_breaks_from_f64, temporal_breaks_time, temporal_minor_breaks_from_f64,
temporal_minor_breaks_from_f64_with_interval, TemporalInterval,
};
use super::direction::Direction;
use super::input::InputRange;
use super::output::OutputRange;
use super::transform::{Transform, TransformKind};
use super::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TemporalUnit {
Date,
DateTime,
Time,
Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ScaleTypeKind {
#[default]
Continuous,
Discrete,
Ordinal,
Binned,
Identity,
Temporal(TemporalUnit),
}
impl ScaleTypeKind {
pub fn name(self) -> &'static str {
match self {
ScaleTypeKind::Continuous => "continuous",
ScaleTypeKind::Discrete => "discrete",
ScaleTypeKind::Ordinal => "ordinal",
ScaleTypeKind::Binned => "binned",
ScaleTypeKind::Identity => "identity",
ScaleTypeKind::Temporal(_) => "temporal",
}
}
}
fn ordered(min: f64, max: f64) -> (f64, f64) {
if min <= max {
(min, max)
} else {
(max, min)
}
}
pub fn continuous_map(
input: &Value,
input_range: Option<&InputRange>,
output_range: Option<&OutputRange>,
transform: &Transform,
color_space: ColorSpace,
direction: Direction,
) -> Value {
let v = match input.as_number() {
Some(n) => n,
None => return Value::Null,
};
let (d_min, d_max) = match input_range {
Some(InputRange::Continuous { min, max }) => (*min, *max),
_ => return Value::Null,
};
let v_t = transform.forward(v);
let dmin_t = transform.forward(d_min);
let dmax_t = transform.forward(d_max);
let t = if dmax_t == dmin_t {
0.0
} else {
(v_t - dmin_t) / (dmax_t - dmin_t)
};
interpolate_range(direction.apply_fraction(t), output_range, color_space)
}
pub fn continuous_breaks(
input_range: Option<&InputRange>,
transform: &Transform,
n: usize,
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
transform_breaks(min, max, n, transform.kind)
.into_iter()
.map(Value::Number)
.collect()
}
pub fn continuous_minor_breaks(
input_range: Option<&InputRange>,
transform: &Transform,
majors: &[Value],
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
transform_minor_breaks(min, max, transform.kind, majors)
.into_iter()
.map(Value::Number)
.collect()
}
fn transform_breaks(min: f64, max: f64, n: usize, kind: TransformKind) -> Vec<f64> {
match kind {
TransformKind::Identity
| TransformKind::Square
| TransformKind::Exp10
| TransformKind::Exp2
| TransformKind::Exp => extended_breaks(min, max, n),
TransformKind::Log10 => log_pretty_breaks(min, max, n, 10.0),
TransformKind::Log2 => log_pretty_breaks(min, max, n, 2.0),
TransformKind::Log => log_pretty_breaks(min, max, n, std::f64::consts::E),
TransformKind::Sqrt => sqrt_breaks(min, max, n),
TransformKind::Asinh | TransformKind::PseudoLog => {
symlog_breaks(min, max, n, std::f64::consts::E)
}
TransformKind::PseudoLog2 => symlog_breaks(min, max, n, 2.0),
TransformKind::PseudoLog10 => symlog_breaks(min, max, n, 10.0),
}
}
fn transform_minor_breaks(min: f64, max: f64, kind: TransformKind, majors: &[Value]) -> Vec<f64> {
match kind {
TransformKind::Log10 => log_minor_breaks(min, max, 10.0),
TransformKind::Log2 => log_minor_breaks(min, max, 2.0),
TransformKind::Log => log_minor_breaks(min, max, std::f64::consts::E),
TransformKind::Asinh | TransformKind::PseudoLog => {
symlog_minor_breaks(min, max, std::f64::consts::E)
}
TransformKind::PseudoLog2 => symlog_minor_breaks(min, max, 2.0),
TransformKind::PseudoLog10 => symlog_minor_breaks(min, max, 10.0),
_ => {
let m: Vec<f64> = majors.iter().filter_map(|v| v.as_number()).collect();
linear_minor_breaks_between(&m, 1)
}
}
}
pub fn temporal_breaks(
input_range: Option<&InputRange>,
unit: TemporalUnit,
n: usize,
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
temporal_breaks_from_f64(min, max, unit, n)
.into_iter()
.map(|raw| wrap_temporal_value(raw, unit))
.collect()
}
pub fn temporal_breaks_with_interval(
input_range: Option<&InputRange>,
unit: TemporalUnit,
interval: TemporalInterval,
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
if !min.is_finite() || !max.is_finite() || min >= max {
return Vec::new();
}
let raws: Vec<f64> = match unit {
TemporalUnit::Date => temporal_breaks_date(min as i32, max as i32, interval)
.into_iter()
.filter(|d| (*d as f64) >= min && (*d as f64) <= max)
.map(|d| d as f64)
.collect(),
TemporalUnit::DateTime | TemporalUnit::Duration => {
temporal_breaks_datetime(min as i64, max as i64, interval)
.into_iter()
.filter(|us| (*us as f64) >= min && (*us as f64) <= max)
.map(|us| us as f64)
.collect()
}
TemporalUnit::Time => temporal_breaks_time(min as i64, max as i64, interval)
.into_iter()
.filter(|us| (*us as f64) >= min && (*us as f64) <= max)
.map(|us| us as f64)
.collect(),
};
raws.into_iter()
.map(|raw| wrap_temporal_value(raw, unit))
.collect()
}
pub fn temporal_minor_breaks(
input_range: Option<&InputRange>,
unit: TemporalUnit,
_majors: &[Value],
n: usize,
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
temporal_minor_breaks_from_f64(min, max, unit, n)
.into_iter()
.map(|raw| wrap_temporal_value(raw, unit))
.collect()
}
pub fn temporal_minor_breaks_with_interval(
input_range: Option<&InputRange>,
unit: TemporalUnit,
interval: TemporalInterval,
) -> Vec<Value> {
let (min, max) = match input_range {
Some(InputRange::Continuous { min, max }) => ordered(*min, *max),
_ => return Vec::new(),
};
temporal_minor_breaks_from_f64_with_interval(min, max, unit, interval)
.into_iter()
.map(|raw| wrap_temporal_value(raw, unit))
.collect()
}
pub fn wrap_temporal_value(raw: f64, unit: TemporalUnit) -> Value {
match unit {
TemporalUnit::Date => Value::Date(raw as i32),
TemporalUnit::DateTime => Value::DateTime(raw as i64),
TemporalUnit::Time => Value::Time(raw as i64),
TemporalUnit::Duration => Value::Duration(raw as i64),
}
}
pub fn discrete_map(
input: &Value,
input_range: Option<&InputRange>,
output_range: Option<&OutputRange>,
direction: Direction,
) -> Value {
let domain = match input_range {
Some(InputRange::Discrete(d)) => d,
_ => return Value::Null,
};
let n = domain.len();
let idx = match domain.iter().position(|d| d.key_eq(input)) {
Some(i) => i,
None => return Value::Null,
};
let idx = direction.apply_index(idx, n);
match output_range {
None => {
if n == 0 {
Value::Null
} else {
Value::Number((idx as f64 + 0.5) / n as f64)
}
}
Some(OutputRange::Numbers(vs)) => vs
.get(idx)
.copied()
.map(Value::Number)
.unwrap_or(Value::Null),
Some(OutputRange::Colors(vs)) => vs
.get(idx)
.copied()
.map(Value::Color)
.unwrap_or(Value::Null),
Some(OutputRange::Strings(vs)) => vs
.get(idx)
.cloned()
.map(Value::String)
.unwrap_or(Value::Null),
Some(OutputRange::Linetypes(vs)) => vs
.get(idx)
.cloned()
.map(Value::Linetype)
.unwrap_or(Value::Null),
}
}
pub fn ordinal_map(
input: &Value,
input_range: Option<&InputRange>,
output_range: Option<&OutputRange>,
color_space: ColorSpace,
direction: Direction,
) -> Value {
let domain = match input_range {
Some(InputRange::Discrete(d)) => d,
_ => return Value::Null,
};
let n = domain.len();
let idx = match domain.iter().position(|d| d.key_eq(input)) {
Some(i) => i,
None => return Value::Null,
};
if n == 0 {
return Value::Null;
}
let idx = direction.apply_index(idx, n);
match output_range {
None => Value::Number((idx as f64 + 0.5) / n as f64),
Some(range) => {
let t = if n > 1 {
idx as f64 / (n - 1) as f64
} else {
0.0
};
interpolate_range(t, Some(range), color_space)
}
}
}
pub fn discrete_breaks(input_range: Option<&InputRange>) -> Vec<Value> {
match input_range {
Some(InputRange::Discrete(d)) => d.clone(),
_ => Vec::new(),
}
}
pub fn discrete_band_width(input_range: Option<&InputRange>) -> f64 {
match input_range {
Some(InputRange::Discrete(d)) if !d.is_empty() => 1.0 / d.len() as f64,
_ => 0.0,
}
}
pub fn binned_map(
input: &Value,
input_range: Option<&InputRange>,
bins: Option<&[f64]>,
output_range: Option<&OutputRange>,
color_space: ColorSpace,
direction: Direction,
) -> Value {
let v = match input.as_number() {
Some(n) => n,
None => return Value::Null,
};
let (d_min, d_max) = match input_range {
Some(InputRange::Continuous { min, max }) => (*min, *max),
_ => return Value::Null,
};
let edges = match bins {
Some(es) if es.len() >= 2 => es,
_ => return Value::Null,
};
if !v.is_finite() || v < d_min || v > d_max {
return Value::Null;
}
let bin = find_bin(v, edges);
match output_range {
None => {
let span = d_max - d_min;
if span <= 0.0 {
return Value::Number(0.0);
}
let centre = (edges[bin] + edges[bin + 1]) * 0.5;
Value::Number(direction.apply_fraction((centre - d_min) / span))
}
Some(range) => {
let n_bins = edges.len() - 1;
let bin = direction.apply_index(bin, n_bins);
let t = if n_bins > 1 {
bin as f64 / (n_bins - 1) as f64
} else {
0.0
};
interpolate_range(t, Some(range), color_space)
}
}
}
pub fn binned_map_break(
input: &Value,
input_range: Option<&InputRange>,
direction: Direction,
) -> Value {
let v = match input.as_number() {
Some(n) => n,
None => return Value::Null,
};
let (d_min, d_max) = match input_range {
Some(InputRange::Continuous { min, max }) => (*min, *max),
_ => return Value::Null,
};
if !v.is_finite() || v < d_min || v > d_max {
return Value::Null;
}
let span = d_max - d_min;
if span <= 0.0 {
return Value::Number(0.0);
}
Value::Number(direction.apply_fraction((v - d_min) / span))
}
pub fn binned_breaks(bins: Option<&[f64]>) -> Vec<Value> {
match bins {
Some(es) => es.iter().copied().map(Value::Number).collect(),
None => Vec::new(),
}
}
pub fn binned_band_width(bins: Option<&[f64]>) -> f64 {
match bins {
Some(es) if es.len() >= 2 => 1.0 / (es.len() - 1) as f64,
_ => 0.0,
}
}
pub fn binned_band_width_at(
input: &Value,
input_range: Option<&InputRange>,
bins: Option<&[f64]>,
) -> f64 {
let v = match input.as_number() {
Some(n) => n,
None => return 0.0,
};
let (d_min, d_max) = match input_range {
Some(InputRange::Continuous { min, max }) => (*min, *max),
_ => return 0.0,
};
let edges = match bins {
Some(es) if es.len() >= 2 => es,
_ => return 0.0,
};
let span = d_max - d_min;
if span <= 0.0 {
return 0.0;
}
let bin = find_bin(v, edges);
(edges[bin + 1] - edges[bin]) / span
}
pub fn identity_map(input: &Value) -> Value {
input.clone()
}
fn interpolate_range(t: f64, range: Option<&OutputRange>, color_space: ColorSpace) -> Value {
match range {
None => Value::Number(t),
Some(OutputRange::Numbers(vs)) => match vs.len() {
0 => Value::Null,
1 => Value::Number(vs[0]),
n => {
let (lo, frac) = pick_segment(t, n);
Value::Number(lerp_f64(vs[lo], vs[lo + 1], frac))
}
},
Some(OutputRange::Colors(vs)) => match vs.len() {
0 => Value::Null,
1 => Value::Color(vs[0]),
n => {
let (lo, frac) = pick_segment(t, n);
Value::Color(lerp_color(vs[lo], vs[lo + 1], frac, color_space))
}
},
Some(OutputRange::Strings(vs)) => match vs.len() {
0 => Value::Null,
1 => Value::String(vs[0].clone()),
n => {
let idx = (t * (n - 1) as f64).round() as isize;
let clamped = idx.clamp(0, n as isize - 1) as usize;
Value::String(vs[clamped].clone())
}
},
Some(OutputRange::Linetypes(vs)) => match vs.len() {
0 => Value::Null,
1 => Value::Linetype(vs[0].clone()),
n => {
let idx = (t * (n - 1) as f64).round() as isize;
let idx = idx.clamp(0, n as isize - 1) as usize;
Value::Linetype(vs[idx].clone())
}
},
}
}
fn pick_segment(t: f64, n: usize) -> (usize, f64) {
debug_assert!(n >= 2, "pick_segment requires n >= 2");
let segments = (n - 1) as f64;
let scaled = t * segments;
let raw_lo = scaled.floor();
let lo = (raw_lo as isize).clamp(0, n as isize - 2) as usize;
let frac = scaled - lo as f64;
(lo, frac)
}
fn lerp_f64(a: f64, b: f64, t: f64) -> f64 {
a + t * (b - a)
}
fn find_bin(v: f64, edges: &[f64]) -> usize {
let n_bins = edges.len() - 1;
edges
.partition_point(|e| *e <= v)
.saturating_sub(1)
.min(n_bins - 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_bin_brackets_are_half_open_with_a_closed_top() {
let edges = [10.0, 20.0, 30.0];
assert_eq!(find_bin(10.0, &edges), 0);
assert_eq!(find_bin(15.0, &edges), 0);
assert_eq!(find_bin(20.0, &edges), 1);
assert_eq!(find_bin(25.0, &edges), 1);
assert_eq!(find_bin(30.0, &edges), 1);
}
#[test]
fn find_bin_clamps_outside_the_edge_list() {
let edges = [10.0, 20.0, 30.0];
assert_eq!(find_bin(5.0, &edges), 0);
assert_eq!(find_bin(-100.0, &edges), 0);
assert_eq!(find_bin(35.0, &edges), 1);
}
fn srgb() -> ColorSpace {
ColorSpace::Srgb
}
fn cat(s: &str) -> Value {
Value::String(std::sync::Arc::from(s))
}
fn strings(items: &[&str]) -> OutputRange {
OutputRange::Strings(items.iter().map(|s| std::sync::Arc::from(*s)).collect())
}
fn dash(len: f64) -> std::sync::Arc<[crate::scales::value::LinetypeStep]> {
std::sync::Arc::from(vec![crate::scales::value::LinetypeStep::Dash(len)])
}
#[test]
fn pick_segment_walks_one_segment_per_stop_pair() {
assert_eq!(pick_segment(0.0, 3), (0, 0.0));
assert_eq!(pick_segment(0.25, 3), (0, 0.5));
assert_eq!(pick_segment(0.5, 3), (1, 0.0));
assert_eq!(pick_segment(0.75, 3), (1, 0.5));
}
#[test]
fn pick_segment_holds_the_top_stop_pair_at_the_upper_end() {
let (lo, frac) = pick_segment(1.0, 3);
assert_eq!(lo, 1);
assert!((frac - 1.0).abs() < 1e-12, "{frac}");
}
#[test]
fn pick_segment_extrapolates_past_both_ends() {
let (lo, frac) = pick_segment(1.5, 3);
assert_eq!(lo, 1);
assert!((frac - 2.0).abs() < 1e-12, "{frac}");
let (lo, frac) = pick_segment(-0.5, 3);
assert_eq!(lo, 0);
assert!((frac + 1.0).abs() < 1e-12, "{frac}");
}
#[test]
fn interpolate_range_without_a_range_returns_the_fraction() {
let v = interpolate_range(0.42, None, srgb());
assert_eq!(v.as_number(), Some(0.42));
}
#[test]
fn interpolate_range_over_numbers_is_piecewise_linear() {
let range = OutputRange::Numbers(vec![0.0, 10.0, 100.0]);
let at = |t: f64| {
interpolate_range(t, Some(&range), srgb())
.as_number()
.unwrap()
};
assert!((at(0.0) - 0.0).abs() < 1e-12);
assert!((at(0.25) - 5.0).abs() < 1e-12);
assert!((at(0.5) - 10.0).abs() < 1e-12);
assert!((at(0.75) - 55.0).abs() < 1e-12);
assert!((at(1.0) - 100.0).abs() < 1e-12);
}
#[test]
fn interpolate_range_over_an_empty_or_single_stop_range() {
assert!(interpolate_range(0.5, Some(&OutputRange::Numbers(vec![])), srgb()).is_null());
let one = OutputRange::Numbers(vec![7.0]);
assert_eq!(
interpolate_range(0.9, Some(&one), srgb()).as_number(),
Some(7.0)
);
assert!(interpolate_range(0.5, Some(&OutputRange::Colors(vec![])), srgb()).is_null());
assert!(interpolate_range(0.5, Some(&strings(&[])), srgb()).is_null());
assert!(interpolate_range(0.5, Some(&OutputRange::Linetypes(vec![])), srgb()).is_null());
}
#[test]
fn interpolate_range_over_colors_blends_componentwise_in_the_named_space() {
let range = OutputRange::Colors(vec![
crate::color::rgb(0.0, 0.0, 0.0),
crate::color::rgb(1.0, 0.0, 0.0),
crate::color::rgb(1.0, 1.0, 1.0),
]);
let mid = match interpolate_range(0.25, Some(&range), ColorSpace::Srgb) {
Value::Color(c) => c,
other => panic!("expected a color, got {other:?}"),
};
let [r, g, b, a] = mid.components;
assert!((r - 0.5).abs() < 1e-6, "{r}");
assert!(g.abs() < 1e-6 && b.abs() < 1e-6);
assert!((a - 1.0).abs() < 1e-6);
}
#[test]
fn interpolate_range_over_strings_picks_the_nearest_entry() {
let range = strings(&["a", "b", "c"]);
let at = |t: f64| match interpolate_range(t, Some(&range), srgb()) {
Value::String(s) => s.to_string(),
other => panic!("expected a string, got {other:?}"),
};
assert_eq!(at(0.0), "a");
assert_eq!(at(0.4), "b");
assert_eq!(at(0.9), "c");
assert_eq!(at(-2.0), "a");
assert_eq!(at(3.0), "c");
}
#[test]
fn interpolate_range_over_linetypes_picks_the_nearest_entry() {
let range = OutputRange::Linetypes(vec![dash(1.0), dash(2.0), dash(3.0)]);
let at = |t: f64| match interpolate_range(t, Some(&range), srgb()) {
Value::Linetype(p) => p,
other => panic!("expected a linetype, got {other:?}"),
};
assert_eq!(&*at(0.1), &*dash(1.0));
assert_eq!(&*at(0.5), &*dash(2.0));
assert_eq!(&*at(1.0), &*dash(3.0));
assert_eq!(&*at(4.0), &*dash(3.0));
}
#[test]
fn ordinal_map_interpolates_a_palette_shorter_than_the_domain() {
let domain = InputRange::Discrete(vec![cat("a"), cat("b"), cat("c"), cat("d"), cat("e")]);
let range = OutputRange::Numbers(vec![0.0, 100.0]);
let at = |v: &str| {
ordinal_map(
&cat(v),
Some(&domain),
Some(&range),
srgb(),
Direction::Forward,
)
.as_number()
.unwrap()
};
assert!((at("a") - 0.0).abs() < 1e-12);
assert!((at("b") - 25.0).abs() < 1e-12);
assert!((at("c") - 50.0).abs() < 1e-12);
assert!((at("e") - 100.0).abs() < 1e-12);
}
#[test]
fn discrete_map_reversed_mirrors_band_and_palette_index() {
let domain =
InputRange::Discrete(vec![Value::from("a"), Value::from("b"), Value::from("c")]);
let band = |v: &str| {
discrete_map(&cat(v), Some(&domain), None, Direction::Reversed)
.as_number()
.unwrap()
};
assert!((band("a") - 2.5 / 3.0).abs() < 1e-12, "{}", band("a"));
assert!((band("c") - 0.5 / 3.0).abs() < 1e-12, "{}", band("c"));
let range = OutputRange::Numbers(vec![1.0, 2.0, 3.0]);
let pick = |v: &str| {
discrete_map(&cat(v), Some(&domain), Some(&range), Direction::Reversed)
.as_number()
.unwrap()
};
assert_eq!(pick("a"), 3.0);
assert_eq!(pick("c"), 1.0);
}
#[test]
fn ordinal_map_reversed_walks_the_gradient_from_the_far_end() {
let domain =
InputRange::Discrete(vec![Value::from("a"), Value::from("b"), Value::from("c")]);
let range = OutputRange::Numbers(vec![0.0, 10.0]);
let at = |v: &str| {
ordinal_map(
&cat(v),
Some(&domain),
Some(&range),
srgb(),
Direction::Reversed,
)
.as_number()
.unwrap()
};
assert!((at("a") - 10.0).abs() < 1e-12);
assert!((at("b") - 5.0).abs() < 1e-12);
assert!((at("c") - 0.0).abs() < 1e-12);
}
#[test]
fn ordinal_map_reversed_without_a_range_mirrors_the_band_centre() {
let domain = InputRange::Discrete(vec![cat("a"), cat("b")]);
let at = |v: &str| {
ordinal_map(&cat(v), Some(&domain), None, srgb(), Direction::Reversed)
.as_number()
.unwrap()
};
assert!((at("a") - 0.75).abs() < 1e-12);
assert!((at("b") - 0.25).abs() < 1e-12);
}
#[test]
fn binned_band_width_at_reports_the_containing_bins_own_slot() {
let domain = InputRange::Continuous {
min: 0.0,
max: 100.0,
};
let edges = [0.0, 10.0, 50.0, 100.0];
let at = |v: f64| binned_band_width_at(&Value::Number(v), Some(&domain), Some(&edges));
assert!((at(5.0) - 0.1).abs() < 1e-12, "{}", at(5.0));
assert!((at(20.0) - 0.4).abs() < 1e-12, "{}", at(20.0));
assert!((at(60.0) - 0.5).abs() < 1e-12, "{}", at(60.0));
assert!((binned_band_width(Some(&edges)) - 1.0 / 3.0).abs() < 1e-12);
}
#[test]
fn binned_band_width_at_is_zero_without_a_domain_or_bins() {
let domain = InputRange::Continuous {
min: 0.0,
max: 10.0,
};
let edges = [0.0, 5.0, 10.0];
assert_eq!(
binned_band_width_at(&cat("a"), Some(&domain), Some(&edges)),
0.0
);
assert_eq!(
binned_band_width_at(&Value::Number(1.0), None, Some(&edges)),
0.0
);
assert_eq!(
binned_band_width_at(&Value::Number(1.0), Some(&domain), Some(&[3.0])),
0.0
);
}
#[test]
fn wrap_temporal_value_restores_each_units_typed_variant() {
assert!(matches!(
wrap_temporal_value(19_723.0, TemporalUnit::Date),
Value::Date(19_723)
));
assert!(matches!(
wrap_temporal_value(1_704_067_200_000_000.0, TemporalUnit::DateTime),
Value::DateTime(1_704_067_200_000_000)
));
assert!(matches!(
wrap_temporal_value(3_600_000_000_000.0, TemporalUnit::Time),
Value::Time(3_600_000_000_000)
));
assert!(matches!(
wrap_temporal_value(-90_000_000.0, TemporalUnit::Duration),
Value::Duration(-90_000_000)
));
}
#[test]
fn binned_map_sends_below_edge_values_to_the_first_bin() {
let domain = InputRange::Continuous {
min: 0.0,
max: 100.0,
};
let edges = [10.0, 20.0, 30.0];
let low = binned_map(
&Value::Number(5.0),
Some(&domain),
Some(&edges),
None,
ColorSpace::default(),
Direction::Forward,
);
let high = binned_map(
&Value::Number(95.0),
Some(&domain),
Some(&edges),
None,
ColorSpace::default(),
Direction::Forward,
);
let (low, high) = (low.as_number().unwrap(), high.as_number().unwrap());
assert!(
low < high,
"below-range {low} should sit under above-range {high}"
);
assert!((low - 0.15).abs() < 1e-12, "{low}");
}
}