use super::*;
pub(crate) fn tick_decimals(value: f64) -> usize {
if value == 0.0 {
return 0;
}
let magnitude: i32 = value.abs().log10().floor() as i32;
match magnitude {
m if m >= 6 => 0,
m if m >= 3 => 1,
m if m >= 0 => 2,
_ => 4,
}
}
pub(crate) fn format_tick(value: f64, interval: f64) -> String {
let decimals: usize = tick_decimals(value).min(tick_decimals(interval) + 2);
let rounded: f64 = (value * 10f64.powi(decimals as i32)).round() / 10f64.powi(decimals as i32);
let mut text = format!("{:.*}", decimals, rounded);
if text.contains('.') {
while text.ends_with('0') {
text.pop();
}
if text.ends_with('.') {
text.pop();
}
}
text
}
pub(crate) fn parse_colors(colors: &[String]) -> Vec<Color> {
if colors.is_empty() {
return DEFAULT_VISUAL_MAP_COLORS
.iter()
.filter_map(|color: &&str| Color::parse(color))
.collect();
}
colors
.iter()
.filter_map(|color: &String| Color::parse(color))
.collect()
}
pub(crate) fn parse_pieces(pieces: &[DataValue]) -> Vec<VisualPiece> {
let mut result: Vec<VisualPiece> = Vec::new();
for piece in pieces {
let min: Option<f64> = piece
.get(PIECE_FIELD_MIN)
.and_then(|value: &DataValue| value.to_number());
let max: Option<f64> = piece
.get(PIECE_FIELD_MAX)
.and_then(|value: &DataValue| value.to_number());
let color: Option<Color> = piece
.get(PIECE_FIELD_COLOR)
.and_then(|value: &DataValue| value.as_text())
.and_then(Color::parse);
let (Some(min), Some(max), Some(color)) = (min, max, color) else {
continue;
};
result.push(VisualPiece { min, max, color });
}
result
}
pub(crate) fn resolve_edge(position: Option<&Position>, default: f64, total: f64) -> f64 {
match position {
None => default,
Some(Position::Offset(distance)) => *distance,
Some(Position::Middle) | Some(Position::Inside) => total * 0.5,
Some(Position::Left) | Some(Position::Top) => 0.0,
Some(Position::Right) | Some(Position::Bottom) => total,
}
}
pub(crate) fn font_string(size: f64) -> String {
format!("{}px sans-serif", size)
}
pub(crate) fn collect_categories(series: &[SeriesOption]) -> Vec<String> {
for item in series {
if !is_cartesian(item) {
continue;
}
if !item
.data
.iter()
.any(|value: &DataValue| !point_label(value).is_empty())
{
continue;
}
return item.data.iter().map(point_label).collect();
}
Vec::new()
}
fn point_label(value: &DataValue) -> String {
match value {
DataValue::List(items) => items
.first()
.and_then(|item: &DataValue| item.as_text())
.map(|text: &str| text.to_string())
.unwrap_or_default(),
DataValue::Object(_) => value
.get(DATA_FIELD_NAME)
.and_then(|item: &DataValue| item.as_text())
.unwrap_or_default()
.to_string(),
other => other.as_text().unwrap_or_default().to_string(),
}
}
fn is_cartesian(series: &SeriesOption) -> bool {
matches!(
series.kind,
SeriesType::Line
| SeriesType::Bar
| SeriesType::Scatter
| SeriesType::EffectScatter
| SeriesType::Candlestick
| SeriesType::Boxplot
| SeriesType::Heatmap
| SeriesType::PictorialBar
| SeriesType::Custom
)
}
pub(crate) fn category_count(series: &[SeriesOption]) -> usize {
series
.iter()
.filter(|item: &&SeriesOption| is_cartesian(item))
.map(|item: &SeriesOption| item.data.len())
.max()
.unwrap_or(0)
}
pub(crate) fn value_extent(series: &[SeriesOption], axis: Option<&AxisOption>) -> (f64, f64) {
let mut min: f64 = f64::INFINITY;
let mut max: f64 = f64::NEG_INFINITY;
for item in series {
if !matches!(
item.kind,
SeriesType::Line
| SeriesType::Bar
| SeriesType::Scatter
| SeriesType::EffectScatter
| SeriesType::Candlestick
| SeriesType::Boxplot
| SeriesType::Heatmap
) {
continue;
}
for value in &item.data {
let selected: Option<f64> = match value {
DataValue::List(items) => match axis {
Some(AxisOption {
axis_type: AxisType::Category,
..
}) => items.iter().find_map(DataValue::to_number),
_ => items.iter().rev().find_map(DataValue::to_number),
},
DataValue::Object(_) => value.get(DATA_FIELD_VALUE).and_then(DataValue::to_number),
other => other.to_number(),
};
if let Some(number) = selected {
min = min.min(number);
max = max.max(number);
}
}
}
if min.is_finite() && max.is_finite() {
(min, max)
} else {
(0.0, 1.0)
}
}
pub(crate) fn nice_extent(min: f64, max: f64, split: usize) -> (f64, f64) {
let (min, max, _) = nice_scale(min, max, split);
(min, max)
}
pub(crate) fn nice_scale(min: f64, max: f64, split: usize) -> (f64, f64, f64) {
if !min.is_finite() || !max.is_finite() {
return (0.0, 1.0, 1.0);
}
if min == max {
let padding: f64 = if min.abs() < f64::EPSILON {
1.0
} else {
min.abs() * 0.1
};
return (min - padding, max + padding, padding * 2.0);
}
let (min, max) = if min < max { (min, max) } else { (max, min) };
let split: usize = split.max(2);
let range: f64 = nice_number(max - min, false);
let interval: f64 = nice_number(range / (split - 1) as f64, true);
if interval <= 0.0 {
return (min, max, 1.0);
}
let nice_min: f64 = (min / interval).floor() * interval;
let nice_max: f64 = (max / interval).ceil() * interval;
(
round_to(nice_min, interval),
round_to(nice_max, interval),
interval,
)
}
pub(crate) fn nice_number(value: f64, round: bool) -> f64 {
if value <= 0.0 {
return 0.0;
}
let exponent: f64 = value.log10().floor();
let power: f64 = 10f64.powf(exponent);
let fraction: f64 = value / power;
let nice: f64 = if round {
if fraction < 1.5 {
1.0
} else if fraction < 3.0 {
2.0
} else if fraction < 7.0 {
5.0
} else {
10.0
}
} else if fraction <= 1.0 {
1.0
} else if fraction <= 2.0 {
2.0
} else if fraction <= 5.0 {
5.0
} else {
10.0
};
nice * power
}
fn round_to(value: f64, interval: f64) -> f64 {
if interval <= 0.0 {
return value;
}
let decimals: i32 = -(interval.log10().floor()).max(0.0) as i32 + 2;
let factor: f64 = 10f64.powi(decimals);
(value * factor).round() / factor
}
pub(crate) fn value_ticks(domain_min: f64, domain_max: f64, split: usize) -> Vec<AxisTick> {
let (min, max, interval) = nice_scale(domain_min, domain_max, split);
ticks_at_interval(min, max, interval)
}
pub(crate) fn place_ticks(scale: &Scale, ticks: Vec<AxisTick>) -> Vec<AxisTick> {
ticks
.into_iter()
.map(|mut tick: AxisTick| {
tick.position = scale.map(tick.value);
tick
})
.collect()
}
pub(crate) fn ticks_at_interval(min: f64, max: f64, interval: f64) -> Vec<AxisTick> {
if interval <= 0.0 {
return vec![AxisTick {
value: min,
label: format_tick(min, interval),
position: 0.0,
}];
}
let mut ticks: Vec<AxisTick> = Vec::new();
let mut value: f64 = min;
let mut guard: i32 = 0;
while value <= max + interval * 0.5 && guard < 1000 {
ticks.push(AxisTick {
value,
label: format_tick(value, interval),
position: 0.0,
});
value += interval;
guard += 1;
}
ticks
}
pub(crate) fn log_ticks(
domain_min: f64,
domain_max: f64,
base: f64,
range_min: f64,
range_max: f64,
) -> Vec<AxisTick> {
let mut ticks: Vec<AxisTick> = Vec::new();
let mut exponent: i32 = domain_min.max(f64::MIN_POSITIVE).log(base).floor() as i32;
let last: i32 = domain_max.max(f64::MIN_POSITIVE).log(base).ceil() as i32;
let scale: Scale = Scale::log(
domain_min.max(f64::MIN_POSITIVE),
domain_max.max(f64::MIN_POSITIVE),
range_min,
range_max,
base,
);
while exponent <= last {
let value: f64 = base.powi(exponent);
ticks.push(AxisTick {
value,
label: format_number(value),
position: scale.map(value),
});
exponent += 1;
}
ticks
}
pub(crate) fn category_ticks(scale: &Scale) -> Vec<AxisTick> {
let count: usize = if scale.categories.is_empty() {
scale.count
} else {
scale.categories.len()
};
if count == 0 {
return Vec::new();
}
let sampled: usize = count.min(MAX_CATEGORY_TICKS);
let step: usize = ((count as f64 / sampled as f64).ceil() as usize).max(1);
let mut result: Vec<AxisTick> = Vec::with_capacity(sampled);
let mut index: usize = 0usize;
while index < count && result.len() < MAX_CATEGORY_TICKS {
let label: String = match scale.categories.get(index) {
Some(name) => name.clone(),
None => index.to_string(),
};
result.push(AxisTick {
value: index as f64,
label,
position: scale.map(index as f64),
});
index += step;
}
result
}
pub(crate) fn stack_position(option: &ChartOption, index: usize) -> (usize, usize) {
let Some(stack) = option.series[index].stack.clone() else {
return (0, 1);
};
let mut stack_index: usize = 0;
let mut stack_size: usize = 0;
for (position, series) in option.series.iter().enumerate() {
if series.stack.as_deref() == Some(stack.as_str()) {
if position == index {
stack_index = stack_size;
}
stack_size += 1;
}
}
(stack_index, stack_size.max(1))
}