use crate::data::IntoSeries;
use crate::mark::{Area, Bars, Cells, Line, Points, Range};
use crate::plot::Plot;
pub fn line<'a>(values: impl IntoSeries<'a>) -> Plot<'a> {
Plot::new().layer(Line::y(values))
}
pub fn scatter<'a>(x: impl IntoSeries<'a>, y: impl IntoSeries<'a>) -> Plot<'a> {
Plot::new().layer(Points::xy(x, y))
}
pub fn bar<'a>(
categories: impl IntoIterator<Item = impl Into<String>>,
values: impl IntoSeries<'a>,
) -> Plot<'a> {
Plot::new().layer(Bars::new(categories, values))
}
pub fn hist<'a>(values: impl IntoSeries<'a>) -> Plot<'a> {
let series = values.into_series();
match crate::stat::Bins::auto(series.as_slice(), 60) {
Some(bins) => {
let counts: Vec<f64> = bins.counts().iter().map(|&count| count as f64).collect();
Plot::new().layer(Bars::spans(bins.start(), bins.width(), counts))
}
None => Plot::new(),
}
}
pub fn stairs<'a>(values: impl IntoSeries<'a>) -> Plot<'a> {
let series = values.into_series();
let mut x = Vec::with_capacity(series.len() * 2);
let mut y = Vec::with_capacity(series.len() * 2);
for (index, value) in series.iter().enumerate() {
if index > 0 {
x.push(index as f64);
y.push(y.last().copied().unwrap_or(value));
}
x.push(index as f64);
y.push(value);
}
Plot::new().layer(Line::xy(x, y))
}
pub fn ecdf<'a>(values: impl IntoSeries<'a>) -> Plot<'a> {
let series = values.into_series();
let (sorted, fractions) = crate::stat::ecdf(series.as_slice());
let mut x = Vec::with_capacity(sorted.len() * 2);
let mut y = Vec::with_capacity(sorted.len() * 2);
let mut previous = 0.0f64;
for (value, fraction) in sorted.into_iter().zip(fractions) {
x.push(value);
y.push(previous);
x.push(value);
y.push(fraction);
previous = fraction;
}
Plot::new().layer(Line::xy(x, y))
}
pub fn heatmap<'a>(columns: usize, values: impl IntoSeries<'a>) -> Plot<'a> {
Plot::new().layer(Cells::matrix(columns, values))
}
pub fn hist2d<'a>(x: impl IntoSeries<'a>, y: impl IntoSeries<'a>) -> Plot<'a> {
let xs = x.into_series();
let ys = y.into_series();
match crate::stat::bins2(xs.as_slice(), ys.as_slice(), 48, 32) {
Some(grid) => {
let counts: Vec<f64> = grid
.counts
.into_iter()
.map(|count| if count == 0.0 { f64::NAN } else { count })
.collect();
Plot::new().layer(Cells::matrix(grid.columns, counts).extents(grid.x, grid.y))
}
None => Plot::new(),
}
}
pub fn box_plot<'a>(
categories: impl IntoIterator<Item = impl Into<String>>,
groups: impl IntoIterator<Item = impl IntoSeries<'a>>,
) -> Plot<'a> {
let categories: Vec<String> = categories.into_iter().map(Into::into).collect();
let stats: Vec<Option<crate::stat::BoxStats>> = groups
.into_iter()
.map(|group| crate::stat::BoxStats::of(group.into_series().as_slice()))
.collect();
assert_eq!(
categories.len(),
stats.len(),
"box_plot requires one category per group"
);
let pick = |f: &dyn Fn(&crate::stat::BoxStats) -> f64| -> Vec<f64> {
stats
.iter()
.map(|s| s.as_ref().map_or(f64::NAN, f))
.collect()
};
let mut outlier_x = Vec::new();
let mut outlier_y = Vec::new();
for (index, stat) in stats.iter().enumerate() {
if let Some(stat) = stat {
for &outlier in &stat.outliers {
outlier_x.push(index as f64);
outlier_y.push(outlier);
}
}
}
let plot = Plot::new().layer(
Range::over(
categories,
pick(&|s| s.whisker_low),
pick(&|s| s.whisker_high),
)
.body(pick(&|s| s.q1), pick(&|s| s.q3))
.marker(pick(&|s| s.median)),
);
if outlier_x.is_empty() {
plot
} else {
plot.layer(Points::xy(outlier_x, outlier_y))
}
}
pub fn error_bars<'a>(
x: impl IntoSeries<'a>,
y: impl IntoSeries<'a>,
error: impl IntoSeries<'a>,
) -> Plot<'a> {
let x = x.into_series();
let y = y.into_series();
let error = error.into_series();
assert!(
x.len() == y.len() && y.len() == error.len(),
"error_bars requires series of equal length"
);
let low: Vec<f64> = y.iter().zip(error.iter()).map(|(y, e)| y - e).collect();
let high: Vec<f64> = y.iter().zip(error.iter()).map(|(y, e)| y + e).collect();
let xs = x.as_slice().to_vec();
Plot::new()
.layer(Range::xy(xs.clone(), low, high))
.layer(Points::xy(xs, y.as_slice().to_vec()))
}
pub fn density<'a>(values: impl IntoSeries<'a>) -> Plot<'a> {
let series = values.into_series();
match crate::stat::kde(series.as_slice(), 256) {
Some((positions, densities)) => Plot::new().layer(Line::xy(positions, densities)),
None => Plot::new(),
}
}
pub fn violin<'a>(
categories: impl IntoIterator<Item = impl Into<String>>,
groups: impl IntoIterator<Item = impl IntoSeries<'a>>,
) -> Plot<'a> {
let categories: Vec<String> = categories.into_iter().map(Into::into).collect();
let densities: Vec<Option<(Vec<f64>, Vec<f64>)>> = groups
.into_iter()
.map(|group| crate::stat::kde(group.into_series().as_slice(), 128))
.collect();
assert_eq!(
categories.len(),
densities.len(),
"violin requires one category per group"
);
let mut plot = Plot::new().x_scale(crate::scale::Scale::bands(categories));
for (index, density) in densities.into_iter().enumerate() {
let Some((positions, values)) = density else {
continue;
};
let peak = values.iter().copied().fold(f64::MIN_POSITIVE, f64::max);
let center = index as f64;
let half: Vec<f64> = values.iter().map(|v| v / peak * 0.35).collect();
let left: Vec<f64> = half.iter().map(|w| center - w).collect();
let right: Vec<f64> = half.iter().map(|w| center + w).collect();
plot = plot.layer(Area::horizontal(positions, left, right));
}
plot
}