use std::sync::Arc;
use crate::aes::Aesthetic;
use crate::data::Value;
use super::format::LabelFormatter;
use super::transform::ScaleTransform;
use super::Scale;
#[derive(Clone)]
pub struct ScaleLinewidthContinuous {
name: String,
min: f64,
max: f64,
trained: bool,
limits_fixed: bool,
range: (f64, f64),
scale_transform: ScaleTransform,
custom_breaks: Option<Vec<f64>>,
custom_labels: Option<Vec<String>>,
label_formatter: Option<LabelFormatter>,
locale: Option<crate::locale::Locale>,
}
impl ScaleLinewidthContinuous {
pub fn new() -> Self {
ScaleLinewidthContinuous {
name: String::new(),
min: f64::INFINITY,
max: f64::NEG_INFINITY,
trained: false,
limits_fixed: false,
range: (1.0, 6.0),
scale_transform: ScaleTransform::Identity,
custom_breaks: None,
custom_labels: None,
label_formatter: None,
locale: None,
}
}
pub fn with_range(mut self, min: f64, max: f64) -> Self {
self.range = (min, max);
self
}
pub fn with_limits(mut self, min: f64, max: f64) -> Self {
self.min = min;
self.max = max;
self.trained = true;
self.limits_fixed = true;
self
}
pub fn with_transform(mut self, transform: ScaleTransform) -> Self {
self.scale_transform = transform;
self
}
pub fn with_breaks(mut self, breaks: Vec<f64>) -> Self {
self.custom_breaks = Some(breaks);
self
}
pub fn with_labels(mut self, labels: Vec<String>) -> Self {
self.custom_labels = Some(labels);
self
}
pub fn with_label_formatter<F>(mut self, f: F) -> Self
where
F: Fn(f64) -> String + Send + Sync + 'static,
{
self.label_formatter = Some(Arc::new(f));
self
}
pub fn with_name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}
pub fn with_locale(mut self, locale: crate::locale::Locale) -> Self {
self.locale = Some(locale);
self
}
fn format_label(&self, v: f64) -> String {
if let Some(f) = &self.label_formatter {
f(v)
} else {
super::util::format_number_opt(v, self.locale.as_ref())
}
}
fn domain_transform_is_broken(&self) -> bool {
if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
return false;
}
let t_min = self.scale_transform.apply(self.min);
let t_max = self.scale_transform.apply(self.max);
!t_min.is_finite() || !t_max.is_finite()
}
fn fraction(&self, value: &Value) -> f64 {
let f = match value.as_f64() {
Some(f) => f,
None => return 0.0,
};
if !f.is_finite() {
return 0.0;
}
if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
return 0.5;
}
if self.domain_transform_is_broken() {
return 0.5;
}
let (dom_lo, dom_hi) = (self.min.min(self.max), self.min.max(self.max));
let clamped = f.clamp(dom_lo, dom_hi);
let t_min = self.scale_transform.apply(self.min);
let t_max = self.scale_transform.apply(self.max);
let t_v = self.scale_transform.apply(clamped);
let (lo, hi) = (t_min.min(t_max), t_min.max(t_max));
let range = hi - lo;
if !t_v.is_finite() || range.abs() < f64::EPSILON {
return 0.5;
}
((t_v - lo) / range).clamp(0.0, 1.0)
}
}
impl Default for ScaleLinewidthContinuous {
fn default() -> Self {
Self::new()
}
}
impl Scale for ScaleLinewidthContinuous {
fn set_default_locale(&mut self, locale: &crate::locale::Locale) {
if self.locale.is_none() {
self.locale = Some(locale.clone());
}
}
fn locale(&self) -> Option<&crate::locale::Locale> {
self.locale.as_ref()
}
fn format_value(&self, value: &Value) -> Option<String> {
let f = value.as_f64()?;
if !f.is_finite() {
return None;
}
Some(self.format_label(f))
}
fn aesthetic(&self) -> Aesthetic {
Aesthetic::Linewidth
}
fn train(&mut self, values: &[Value]) {
if self.limits_fixed {
self.trained = true;
return;
}
for v in values {
if let Some(f) = v.as_f64() {
if f.is_finite() {
if f < self.min {
self.min = f;
}
if f > self.max {
self.max = f;
}
}
}
}
self.trained = true;
}
fn map(&self, value: &Value) -> f64 {
self.fraction(value)
}
fn breaks(&self) -> Vec<(f64, String)> {
if !self.trained || self.min > self.max {
return vec![];
}
if let Some(ref custom) = self.custom_breaks {
return custom
.iter()
.enumerate()
.map(|(i, &v)| {
let pos = self.map(&Value::Float(v));
let label = self
.custom_labels
.as_ref()
.and_then(|labels| labels.get(i).cloned())
.unwrap_or_else(|| self.format_label(v));
(pos, label)
})
.collect();
}
if super::util::is_degenerate_range(self.min, self.max) {
return vec![(0.5, self.format_label(self.min))];
}
let n_breaks = 4;
let range = self.max - self.min;
let step = super::util::nice_step(range / n_breaks as f64);
let start = (self.min / step).ceil() * step;
let values = super::util::stepped_breaks(start, self.max, step);
values
.iter()
.map(|&v| (self.map(&Value::Float(v)), self.format_label(v)))
.collect()
}
fn name(&self) -> &str {
&self.name
}
fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
fn map_to_size(&self, value: &Value) -> Option<f64> {
let f = value.as_f64()?;
if !f.is_finite() {
return None;
}
if self.domain_transform_is_broken() {
return None;
}
let t = self.fraction(value);
let (lo, hi) = self.range;
Some(lo + t * (hi - lo))
}
fn map_fraction_to_size(&self, fraction: f64) -> Option<f64> {
let (lo, hi) = self.range;
Some(lo + fraction.clamp(0.0, 1.0) * (hi - lo))
}
fn domain(&self) -> Option<(f64, f64)> {
if self.trained {
Some((self.min, self.max))
} else {
None
}
}
fn clone_box(&self) -> Box<dyn Scale> {
Box::new(self.clone())
}
fn reset_training(&mut self) {
if !self.limits_fixed {
self.min = f64::INFINITY;
self.max = f64::NEG_INFINITY;
self.trained = false;
}
}
fn validate(&self, warnings: &mut Vec<String>) {
if self.domain_transform_is_broken() {
warnings.push(format!(
"scale_linewidth: {:?} is undefined at the domain limits ({}, {}); \
every row falls back to the geom's own linewidth instead of a mapped size \
(ggplot2's equivalent: NA plus a warning)",
self.scale_transform, self.min, self.max
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_value_falls_back_to_format_label_and_rejects_non_finite() {
let s = ScaleLinewidthContinuous::new().with_label_formatter(|v| format!("{v:.1}mm"));
assert_eq!(
s.format_value(&Value::Float(2.0)),
Some("2.0mm".to_string())
);
assert_eq!(s.format_value(&Value::Float(f64::NAN)), None);
}
#[test]
fn range_transform_and_limits_match_the_documented_formula() {
let vmax = 10.0;
let scale = ScaleLinewidthContinuous::new()
.with_range(0.75, 8.0)
.with_transform(ScaleTransform::Sqrt)
.with_limits(0.0, vmax);
for v in [0.0, 2.5, 5.0, 7.5, 10.0] {
let width = scale.map_to_size(&Value::Float(v)).unwrap();
let expected = 0.75 + 7.25 * (v / vmax).sqrt();
assert!(
(width - expected).abs() < 1e-9,
"v={v}: width={width}, expected={expected}"
);
}
}
#[test]
fn limits_squish_out_of_range_values() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_limits(0.0, 10.0);
assert_eq!(scale.map_to_size(&Value::Float(-5.0)), Some(1.0));
assert_eq!(scale.map_to_size(&Value::Float(50.0)), Some(6.0));
}
#[test]
fn limits_fixed_domain_is_not_widened_by_training() {
let mut scale = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
scale.train(&[Value::Float(-100.0), Value::Float(100.0)]);
assert_eq!(scale.domain(), Some((0.0, 10.0)));
}
#[test]
fn default_range_is_1_to_6() {
let mut scale = ScaleLinewidthContinuous::new();
scale.train(&[Value::Float(0.0), Value::Float(10.0)]);
assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
assert_eq!(scale.map_to_size(&Value::Float(10.0)), Some(6.0));
}
#[test]
fn log10_transform_with_a_nonzero_lower_limit() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Log10)
.with_limits(1.0, 100.0);
let cases = [(1.0, 1.0), (10.0, 3.5), (100.0, 6.0)];
for (v, expected) in cases {
let width = scale.map_to_size(&Value::Float(v)).unwrap();
assert!(
(width - expected).abs() < 1e-9,
"v={v}: width={width}, expected={expected}"
);
}
}
#[test]
fn reverse_transform_flips_which_end_gets_the_larger_width() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Reverse)
.with_limits(1.0, 100.0);
let min_width = scale.map_to_size(&Value::Float(1.0)).unwrap();
let max_width = scale.map_to_size(&Value::Float(100.0)).unwrap();
assert!((min_width - 6.0).abs() < 1e-9, "min_width={min_width}");
assert!((max_width - 1.0).abs() < 1e-9, "max_width={max_width}");
}
#[test]
fn sqrt_transform_with_a_nonzero_lower_limit() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Sqrt)
.with_limits(4.0, 100.0);
let cases = [(4.0, 1.0), (16.0, 2.25), (100.0, 6.0)];
for (v, expected) in cases {
let width = scale.map_to_size(&Value::Float(v)).unwrap();
assert!(
(width - expected).abs() < 1e-9,
"v={v}: width={width}, expected={expected}"
);
}
}
#[test]
fn na_or_non_finite_input_returns_none_not_the_minimum() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_limits(0.0, 10.0);
assert_eq!(scale.map_to_size(&Value::Na), None);
assert_eq!(scale.map_to_size(&Value::Str("x".into())), None);
assert_eq!(scale.map_to_size(&Value::Float(f64::NAN)), None);
assert_eq!(scale.map_to_size(&Value::Float(f64::INFINITY)), None);
}
#[test]
fn log10_with_a_zero_lower_limit_maps_every_value_to_none_and_warns() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Log10)
.with_limits(0.0, 100.0);
for v in [0.0, 1.0, 50.0, 100.0] {
assert_eq!(
scale.map_to_size(&Value::Float(v)),
None,
"v={v} should map to None, not the midpoint width"
);
}
let mut warnings = Vec::new();
scale.validate(&mut warnings);
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("Log10"));
}
#[test]
fn a_value_trained_to_zero_breaks_log10_the_same_way() {
let mut scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Log10);
scale.train(&[Value::Float(0.0), Value::Float(100.0)]);
assert_eq!(scale.map_to_size(&Value::Float(50.0)), None);
let mut warnings = Vec::new();
scale.validate(&mut warnings);
assert_eq!(warnings.len(), 1);
}
#[test]
fn out_of_limits_value_squishes_to_the_nearest_end_even_under_log10() {
let scale = ScaleLinewidthContinuous::new()
.with_range(1.0, 6.0)
.with_transform(ScaleTransform::Log10)
.with_limits(1.0, 100.0);
assert_eq!(scale.map_to_size(&Value::Float(0.0)), Some(1.0));
assert_eq!(scale.map_to_size(&Value::Float(-50.0)), Some(1.0));
assert_eq!(scale.map_to_size(&Value::Float(1000.0)), Some(6.0));
let mut warnings = Vec::new();
scale.validate(&mut warnings);
assert!(
warnings.is_empty(),
"a fine domain shouldn't warn: {warnings:?}"
);
}
#[test]
fn fraction_stays_finite_even_when_the_domain_transform_is_broken() {
let scale = ScaleLinewidthContinuous::new()
.with_transform(ScaleTransform::Log10)
.with_limits(0.0, 100.0);
let t = scale.map(&Value::Float(50.0));
assert!(t.is_finite());
}
#[test]
fn broken_domain_transform_warning_reaches_the_plot_warnings() {
use crate::prelude::*;
let (_svg, warnings) = GGPlot::new(vec![
("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
("xend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
("yend".into(), vec![Value::Float(0.5), Value::Float(1.5)]),
("size".into(), vec![Value::Float(0.0), Value::Float(100.0)]),
])
.aes(
Aes::new()
.x("x")
.y("y")
.xend("xend")
.yend("yend")
.linewidth("size"),
)
.geom_segment()
.scale_linewidth(ScaleLinewidthContinuous::new().with_transform(ScaleTransform::Log10))
.render_svg_native_with_warnings(200, 200)
.unwrap();
assert!(warnings.iter().any(|w| w.contains("Log10")), "{warnings:?}");
}
#[test]
fn with_labels_overrides_the_formatted_label_for_each_break() {
let scale = ScaleLinewidthContinuous::new()
.with_name("flow size")
.with_breaks(vec![1.0, 2.0])
.with_labels(vec!["small".into(), "big".into()])
.with_limits(0.0, 10.0);
assert_eq!(scale.name(), "flow size");
let breaks = scale.breaks();
let labels: Vec<&str> = breaks.iter().map(|(_, l)| l.as_str()).collect();
assert_eq!(labels, vec!["small", "big"]);
}
#[test]
fn with_labels_shorter_than_breaks_falls_back_to_the_formatted_number() {
let scale = ScaleLinewidthContinuous::new()
.with_breaks(vec![1.0, 2.5])
.with_labels(vec!["small".into()])
.with_limits(0.0, 10.0);
let breaks = scale.breaks();
assert_eq!(breaks[0].1, "small");
assert_eq!(breaks[1].1, "2.5");
}
#[test]
fn with_label_formatter_formats_every_break() {
let scale = ScaleLinewidthContinuous::new()
.with_breaks(vec![1.0, 2.0])
.with_label_formatter(|v| format!("{v:.1}mm"))
.with_limits(0.0, 10.0);
let labels: Vec<String> = scale.breaks().into_iter().map(|(_, l)| l).collect();
assert_eq!(labels, vec!["1.0mm", "2.0mm"]);
}
#[test]
fn with_locale_formats_default_break_labels() {
let mut scale = ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
scale.train(&[Value::Float(1000.0), Value::Float(2000.0)]);
let breaks = scale.breaks();
assert!(!breaks.is_empty());
assert!(breaks.iter().any(|(_, l)| l.contains('.')), "{breaks:?}");
}
#[test]
fn set_default_locale_only_applies_when_no_locale_was_set() {
let mut with_own =
ScaleLinewidthContinuous::new().with_locale(crate::locale::Locale::de_de());
with_own.set_default_locale(&crate::locale::Locale::en_us());
assert_eq!(
with_own.locale().unwrap().tag,
crate::locale::Locale::de_de().tag
);
let mut without_own = ScaleLinewidthContinuous::new();
assert!(without_own.locale().is_none());
without_own.set_default_locale(&crate::locale::Locale::en_us());
assert_eq!(
without_own.locale().unwrap().tag,
crate::locale::Locale::en_us().tag
);
}
#[test]
fn degenerate_range_gives_a_single_break_at_the_midpoint() {
let mut scale = ScaleLinewidthContinuous::new();
scale.train(&[Value::Float(5.0), Value::Float(5.0)]);
let breaks = scale.breaks();
assert_eq!(breaks.len(), 1);
assert_eq!(breaks[0].0, 0.5);
assert_eq!(breaks[0].1, "5");
}
#[test]
fn untrained_scale_has_no_breaks() {
let scale = ScaleLinewidthContinuous::new();
assert!(scale.breaks().is_empty());
}
#[test]
fn clone_box_preserves_configuration() {
let scale = ScaleLinewidthContinuous::new()
.with_range(2.0, 9.0)
.with_name("w")
.with_limits(0.0, 5.0);
let cloned = scale.clone_box();
assert_eq!(cloned.name(), "w");
assert_eq!(
cloned.map_to_size(&Value::Float(5.0)),
scale.map_to_size(&Value::Float(5.0))
);
}
#[test]
fn reset_training_clears_an_auto_trained_domain_but_not_fixed_limits() {
let mut auto = ScaleLinewidthContinuous::new();
auto.train(&[Value::Float(1.0), Value::Float(9.0)]);
assert_eq!(auto.domain(), Some((1.0, 9.0)));
auto.reset_training();
assert_eq!(auto.domain(), None);
let mut fixed = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
fixed.reset_training();
assert_eq!(fixed.domain(), Some((0.0, 10.0)));
}
#[test]
fn default_trait_impl_matches_new() {
let default_scale = ScaleLinewidthContinuous::default();
let new_scale = ScaleLinewidthContinuous::new();
assert_eq!(default_scale.name(), new_scale.name());
assert_eq!(default_scale.breaks(), new_scale.breaks());
}
#[test]
fn set_name_overrides_the_axis_legend_title() {
let mut scale = ScaleLinewidthContinuous::new();
scale.set_name("custom title");
assert_eq!(scale.name(), "custom title");
}
#[test]
fn map_handles_na_non_finite_and_untrained_without_panicking() {
let untrained = ScaleLinewidthContinuous::new();
assert_eq!(untrained.map(&Value::Float(5.0)), 0.5);
let trained = ScaleLinewidthContinuous::new().with_limits(0.0, 10.0);
assert_eq!(trained.map(&Value::Na), 0.0);
assert_eq!(trained.map(&Value::Str("x".into())), 0.0);
assert_eq!(trained.map(&Value::Float(f64::NAN)), 0.0);
}
}