use crate::core::Result;
use crate::plots::traits::{
ComputedSeries, ComputedStyle, LegendKey, PlotArea, PlotCompute, PlotConfig, PlotData,
PlotPrimitive, PlotRender, draw_primitives,
};
use crate::render::skia::SkiaRenderer;
use crate::render::{Color, LineStyle, Theme};
#[derive(Debug, Clone)]
pub struct AreaConfig {
pub color: Option<Color>,
pub alpha: f32,
pub line_color: Option<Color>,
pub line_width: f32,
pub baseline: f64,
pub interpolation: AreaInterpolation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AreaInterpolation {
#[default]
Linear,
Step,
Smooth,
}
impl Default for AreaConfig {
fn default() -> Self {
Self {
color: None,
alpha: 0.5,
line_color: None,
line_width: 1.5,
baseline: 0.0,
interpolation: AreaInterpolation::Linear,
}
}
}
impl AreaConfig {
pub fn new() -> Self {
Self::default()
}
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn line_color(mut self, color: Color) -> Self {
self.line_color = Some(color);
self
}
pub fn line_width(mut self, width: f32) -> Self {
self.line_width = width.max(0.0);
self
}
pub fn baseline(mut self, baseline: f64) -> Self {
self.baseline = baseline;
self
}
pub fn interpolation(mut self, interp: AreaInterpolation) -> Self {
self.interpolation = interp;
self
}
}
pub fn area_polygon(x: &[f64], y: &[f64], baseline: f64) -> Vec<(f64, f64)> {
if x.is_empty() || y.is_empty() {
return vec![];
}
let n = x.len().min(y.len());
let mut polygon = Vec::with_capacity(n * 2 + 2);
for i in 0..n {
polygon.push((x[i], y[i]));
}
polygon.push((x[n - 1], baseline));
polygon.push((x[0], baseline));
polygon
}
pub fn fill_between_polygon(x: &[f64], y1: &[f64], y2: &[f64]) -> Vec<(f64, f64)> {
if x.is_empty() || y1.is_empty() || y2.is_empty() {
return vec![];
}
let n = x.len().min(y1.len()).min(y2.len());
let mut polygon = Vec::with_capacity(n * 2);
for i in 0..n {
polygon.push((x[i], y2[i]));
}
for i in (0..n).rev() {
polygon.push((x[i], y1[i]));
}
polygon
}
pub fn fill_between_where(
x: &[f64],
y1: &[f64],
y2: &[f64],
where_mask: &[bool],
) -> Vec<Vec<(f64, f64)>> {
if x.is_empty() || y1.is_empty() || y2.is_empty() || where_mask.is_empty() {
return vec![];
}
let n = x.len().min(y1.len()).min(y2.len()).min(where_mask.len());
let mut segments = Vec::new();
let mut current_segment: Option<(usize, usize)> = None;
for (i, &mask_val) in where_mask.iter().enumerate().take(n) {
if mask_val {
match current_segment {
None => current_segment = Some((i, i)),
Some((start, _)) => current_segment = Some((start, i)),
}
} else if let Some((start, end)) = current_segment {
let segment_x: Vec<f64> = x[start..=end].to_vec();
let segment_y1: Vec<f64> = y1[start..=end].to_vec();
let segment_y2: Vec<f64> = y2[start..=end].to_vec();
segments.push(fill_between_polygon(&segment_x, &segment_y1, &segment_y2));
current_segment = None;
}
}
if let Some((start, end)) = current_segment {
let segment_x: Vec<f64> = x[start..=end].to_vec();
let segment_y1: Vec<f64> = y1[start..=end].to_vec();
let segment_y2: Vec<f64> = y2[start..=end].to_vec();
segments.push(fill_between_polygon(&segment_x, &segment_y1, &segment_y2));
}
segments
}
#[derive(Debug, Clone)]
pub struct StackPlotConfig {
pub colors: Option<Vec<Color>>,
pub alpha: f32,
pub labels: Vec<String>,
pub baseline: StackBaseline,
pub show_lines: bool,
pub line_color: Color,
pub line_width: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StackBaseline {
#[default]
Zero,
Symmetric,
Wiggle,
}
impl Default for StackPlotConfig {
fn default() -> Self {
Self {
colors: None,
alpha: 0.8,
labels: vec![],
baseline: StackBaseline::Zero,
show_lines: false,
line_color: Color::from_rgb(255, 255, 255),
line_width: 0.5,
}
}
}
impl StackPlotConfig {
pub fn new() -> Self {
Self::default()
}
pub fn colors(mut self, colors: Vec<Color>) -> Self {
self.colors = Some(colors);
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.alpha = alpha.clamp(0.0, 1.0);
self
}
pub fn labels(mut self, labels: Vec<String>) -> Self {
self.labels = labels;
self
}
pub fn baseline(mut self, baseline: StackBaseline) -> Self {
self.baseline = baseline;
self
}
pub fn lines(mut self, show: bool) -> Self {
self.show_lines = show;
self
}
}
impl PlotConfig for AreaConfig {}
impl PlotConfig for StackPlotConfig {}
pub struct Area;
pub struct StackedArea;
pub fn compute_stack(
x: &[f64],
ys: &[Vec<f64>],
baseline: StackBaseline,
) -> Vec<(Vec<f64>, Vec<f64>)> {
if x.is_empty() || ys.is_empty() {
return vec![];
}
let n = x.len();
let num_series = ys.len();
let mut cumulative: Vec<Vec<f64>> = vec![vec![0.0; n]; num_series + 1];
for (i, y) in ys.iter().enumerate() {
for j in 0..n.min(y.len()) {
cumulative[i + 1][j] = cumulative[i][j] + y[j];
}
}
let offset: Vec<f64> = match baseline {
StackBaseline::Zero => vec![0.0; n],
StackBaseline::Symmetric => {
let total = &cumulative[num_series];
total.iter().map(|t| -t / 2.0).collect()
}
StackBaseline::Wiggle => {
let total = &cumulative[num_series];
total.iter().map(|t| -t / 2.0).collect()
}
};
let mut result = Vec::with_capacity(num_series);
for i in 0..num_series {
let lower: Vec<f64> = cumulative[i]
.iter()
.zip(offset.iter())
.map(|(c, o)| c + o)
.collect();
let upper: Vec<f64> = cumulative[i + 1]
.iter()
.zip(offset.iter())
.map(|(c, o)| c + o)
.collect();
result.push((lower, upper));
}
result
}
#[derive(Debug, Clone)]
pub struct AreaData {
pub polygon: Vec<(f64, f64)>,
pub x: Vec<f64>,
pub y: Vec<f64>,
pub bounds: ((f64, f64), (f64, f64)),
pub(crate) config: AreaConfig,
}
pub struct AreaInput<'a> {
pub x: &'a [f64],
pub y: &'a [f64],
}
impl<'a> AreaInput<'a> {
pub fn new(x: &'a [f64], y: &'a [f64]) -> Self {
Self { x, y }
}
}
impl PlotCompute for Area {
type Input<'a> = AreaInput<'a>;
type Config = AreaConfig;
type Output = AreaData;
fn compute(input: Self::Input<'_>, config: &Self::Config) -> Result<Self::Output> {
if input.x.is_empty() || input.y.is_empty() {
return Err(crate::core::PlottingError::EmptyDataSet);
}
let polygon = area_polygon(input.x, input.y, config.baseline);
let x_min = input.x.iter().cloned().fold(f64::INFINITY, f64::min);
let x_max = input.x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let y_min = input
.y
.iter()
.cloned()
.fold(f64::INFINITY, f64::min)
.min(config.baseline);
let y_max = input
.y
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max)
.max(config.baseline);
Ok(AreaData {
polygon,
x: input.x.to_vec(),
y: input.y.to_vec(),
bounds: ((x_min, x_max), (y_min, y_max)),
config: config.clone(),
})
}
}
impl PlotData for AreaData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
self.bounds
}
fn is_empty(&self) -> bool {
self.polygon.is_empty()
}
}
impl PlotRender for AreaData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
_theme: &Theme,
color: Color,
) -> Result<()> {
if self.polygon.is_empty() {
return Ok(());
}
let config = &self.config;
let fill_color = config.color.unwrap_or(color).with_alpha(config.alpha);
let screen_polygon: Vec<(f32, f32)> = self
.polygon
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
renderer.draw_filled_polygon(&screen_polygon, fill_color)?;
if let Some(line_color) = config.line_color {
let n = self.x.len();
let line_points: Vec<(f32, f32)> = (0..n)
.map(|i| area.data_to_screen(self.x[i], self.y[i]))
.collect();
renderer.draw_polyline(
&line_points,
line_color,
config.line_width,
LineStyle::Solid,
)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct StackedAreaData {
pub stacks: Vec<(Vec<f64>, Vec<f64>)>,
pub x: Vec<f64>,
pub bounds: ((f64, f64), (f64, f64)),
pub(crate) config: StackPlotConfig,
}
pub struct StackedAreaInput<'a> {
pub x: &'a [f64],
pub ys: &'a [Vec<f64>],
}
impl<'a> StackedAreaInput<'a> {
pub fn new(x: &'a [f64], ys: &'a [Vec<f64>]) -> Self {
Self { x, ys }
}
}
impl PlotCompute for StackedArea {
type Input<'a> = StackedAreaInput<'a>;
type Config = StackPlotConfig;
type Output = StackedAreaData;
fn compute(input: Self::Input<'_>, config: &Self::Config) -> Result<Self::Output> {
if input.x.is_empty() || input.ys.is_empty() {
return Err(crate::core::PlottingError::EmptyDataSet);
}
let stacks = compute_stack(input.x, input.ys, config.baseline);
if stacks.is_empty() {
return Err(crate::core::PlottingError::EmptyDataSet);
}
let x_min = input.x.iter().cloned().fold(f64::INFINITY, f64::min);
let x_max = input.x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for (lower, upper) in &stacks {
y_min = y_min.min(lower.iter().cloned().fold(f64::INFINITY, f64::min));
y_max = y_max.max(upper.iter().cloned().fold(f64::NEG_INFINITY, f64::max));
}
Ok(StackedAreaData {
stacks,
x: input.x.to_vec(),
bounds: ((x_min, x_max), (y_min, y_max)),
config: config.clone(),
})
}
}
impl PlotData for StackedAreaData {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
self.bounds
}
fn is_empty(&self) -> bool {
self.stacks.is_empty()
}
}
impl PlotRender for StackedAreaData {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
theme: &Theme,
_color: Color,
) -> Result<()> {
if self.stacks.is_empty() {
return Ok(());
}
let config = &self.config;
for (i, (lower, upper)) in self.stacks.iter().enumerate() {
let polygon = fill_between_polygon(&self.x, lower, upper);
let fill_color = config
.colors
.as_ref()
.and_then(|c| c.get(i).copied())
.unwrap_or_else(|| theme.get_color(i))
.with_alpha(config.alpha);
let screen_polygon: Vec<(f32, f32)> = polygon
.iter()
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
renderer.draw_filled_polygon(&screen_polygon, fill_color)?;
if config.show_lines && i < self.stacks.len() - 1 {
let upper_line: Vec<(f32, f32)> = self
.x
.iter()
.zip(upper.iter())
.map(|(x, y)| area.data_to_screen(*x, *y))
.collect();
renderer.draw_polyline(
&upper_line,
config.line_color,
config.line_width,
LineStyle::Solid,
)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct StackedAreaBand {
pub x: Vec<f64>,
pub lower: Vec<f64>,
pub upper: Vec<f64>,
pub(crate) alpha: f32,
pub(crate) show_line: bool,
pub(crate) line_color: Color,
pub(crate) line_width: f32,
}
pub fn stacked_area_bands(
x: &[f64],
names: &[String],
ys: &[Vec<f64>],
config: &StackPlotConfig,
) -> Vec<(String, StackedAreaBand)> {
let stacks = compute_stack(x, ys, config.baseline);
let last = stacks.len().saturating_sub(1);
stacks
.into_iter()
.enumerate()
.map(|(index, (lower, upper))| {
(
names.get(index).cloned().unwrap_or_default(),
StackedAreaBand {
x: x.to_vec(),
lower,
upper,
alpha: config.alpha,
show_line: config.show_lines && index < last,
line_color: config.line_color,
line_width: config.line_width,
},
)
})
.collect()
}
impl PlotData for StackedAreaBand {
fn data_bounds(&self) -> ((f64, f64), (f64, f64)) {
let mut x_min = f64::INFINITY;
let mut x_max = f64::NEG_INFINITY;
for &value in &self.x {
if value.is_finite() {
x_min = x_min.min(value);
x_max = x_max.max(value);
}
}
let mut y_min = f64::INFINITY;
let mut y_max = f64::NEG_INFINITY;
for &value in self.lower.iter().chain(self.upper.iter()) {
if value.is_finite() {
y_min = y_min.min(value);
y_max = y_max.max(value);
}
}
if !x_min.is_finite() || !y_min.is_finite() {
return ((0.0, 1.0), (0.0, 1.0));
}
((x_min, x_max), (y_min, y_max))
}
fn is_empty(&self) -> bool {
self.x.is_empty() || self.upper.is_empty()
}
}
impl ComputedSeries for StackedAreaBand {
fn kind(&self) -> &'static str {
"stacked_area"
}
fn point_count(&self) -> usize {
self.x.len()
}
fn legend_key(&self) -> LegendKey {
LegendKey::Patch
}
fn primitives(&self, area: &PlotArea, style: &ComputedStyle) -> Vec<PlotPrimitive> {
let fill = style.tinted(style.color.with_alpha(self.alpha));
let points = area.project_points(fill_between_polygon(&self.x, &self.lower, &self.upper));
let mut primitives = Vec::new();
if points.len() >= 3 {
primitives.push(PlotPrimitive::Polygon {
points,
fill: Some(fill),
edge: None,
});
}
if self.show_line {
let width_px = style.stroke_px(self.line_width);
let color = style.tinted(self.line_color);
for run in area.project_subpaths(self.x.iter().copied().zip(self.upper.iter().copied()))
{
for pair in run.windows(2) {
primitives.push(PlotPrimitive::Line {
from: pair[0],
to: pair[1],
color,
width_px,
style: LineStyle::Solid,
});
}
}
}
primitives
}
}
impl PlotRender for StackedAreaBand {
fn render(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
_theme: &Theme,
color: Color,
) -> Result<()> {
let style = ComputedStyle::opaque(renderer.render_scale(), color);
draw_primitives(renderer, &self.primitives(area, &style))
}
fn render_styled(
&self,
renderer: &mut SkiaRenderer,
area: &PlotArea,
_theme: &Theme,
color: Color,
alpha: f32,
_line_width: Option<f32>,
) -> Result<()> {
let style = ComputedStyle {
scale: renderer.render_scale(),
color,
alpha,
line_width: None,
};
draw_primitives(renderer, &self.primitives(area, &style))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_area_polygon() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![1.0, 2.0, 1.0];
let polygon = area_polygon(&x, &y, 0.0);
assert_eq!(polygon.len(), 5);
assert_eq!(polygon[0], (0.0, 1.0));
assert_eq!(polygon[3], (2.0, 0.0));
assert_eq!(polygon[4], (0.0, 0.0));
}
#[test]
fn test_fill_between_polygon() {
let x = vec![0.0, 1.0, 2.0];
let y1 = vec![0.0, 0.0, 0.0];
let y2 = vec![1.0, 2.0, 1.0];
let polygon = fill_between_polygon(&x, &y1, &y2);
assert_eq!(polygon.len(), 6);
}
#[test]
fn test_fill_between_where() {
let x = vec![0.0, 1.0, 2.0, 3.0, 4.0];
let y1 = vec![0.0; 5];
let y2 = vec![1.0; 5];
let mask = vec![true, true, false, true, true];
let segments = fill_between_where(&x, &y1, &y2, &mask);
assert_eq!(segments.len(), 2); }
#[test]
fn test_compute_stack_zero() {
let x = vec![0.0, 1.0, 2.0];
let ys = vec![vec![1.0, 2.0, 1.0], vec![2.0, 1.0, 2.0]];
let stack = compute_stack(&x, &ys, StackBaseline::Zero);
assert_eq!(stack.len(), 2);
assert!((stack[0].0[0] - 0.0).abs() < 1e-10);
assert!((stack[0].1[0] - 1.0).abs() < 1e-10);
assert!((stack[1].0[0] - 1.0).abs() < 1e-10);
assert!((stack[1].1[0] - 3.0).abs() < 1e-10);
}
#[test]
fn test_compute_stack_symmetric() {
let x = vec![0.0, 1.0];
let ys = vec![vec![2.0, 2.0]];
let stack = compute_stack(&x, &ys, StackBaseline::Symmetric);
assert_eq!(stack.len(), 1);
assert!((stack[0].0[0] - (-1.0)).abs() < 1e-10);
assert!((stack[0].1[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_area_config_implements_plot_config() {
fn assert_plot_config<T: PlotConfig>() {}
assert_plot_config::<AreaConfig>();
}
#[test]
fn test_stack_plot_config_implements_plot_config() {
fn assert_plot_config<T: PlotConfig>() {}
assert_plot_config::<StackPlotConfig>();
}
#[test]
fn test_area_plot_compute_trait() {
use crate::plots::traits::PlotCompute;
let x = vec![0.0, 1.0, 2.0];
let y = vec![1.0, 2.0, 1.0];
let config = AreaConfig::default();
let input = AreaInput::new(&x, &y);
let result = Area::compute(input, &config);
assert!(result.is_ok());
let area_data = result.unwrap();
assert!(!area_data.polygon.is_empty());
}
#[test]
fn test_area_plot_compute_empty() {
use crate::plots::traits::PlotCompute;
let x: Vec<f64> = vec![];
let y: Vec<f64> = vec![];
let config = AreaConfig::default();
let input = AreaInput::new(&x, &y);
let result = Area::compute(input, &config);
assert!(result.is_err());
}
#[test]
fn test_area_plot_data_trait() {
use crate::plots::traits::{PlotCompute, PlotData};
let x = vec![0.0, 1.0, 2.0];
let y = vec![1.0, 2.0, 1.0];
let config = AreaConfig::default();
let input = AreaInput::new(&x, &y);
let area_data = Area::compute(input, &config).unwrap();
let ((x_min, x_max), (y_min, y_max)) = area_data.data_bounds();
assert!((x_min - 0.0).abs() < 1e-10);
assert!((x_max - 2.0).abs() < 1e-10);
assert!(y_min <= y_max);
assert!(!area_data.is_empty());
}
#[test]
fn test_stacked_area_plot_compute_trait() {
use crate::plots::traits::PlotCompute;
let x = vec![0.0, 1.0, 2.0];
let ys = vec![vec![1.0, 2.0, 1.0], vec![2.0, 1.0, 2.0]];
let config = StackPlotConfig::default();
let input = StackedAreaInput::new(&x, &ys);
let result = StackedArea::compute(input, &config);
assert!(result.is_ok());
let stack_data = result.unwrap();
assert_eq!(stack_data.stacks.len(), 2);
}
#[test]
fn test_stacked_area_plot_data_trait() {
use crate::plots::traits::{PlotCompute, PlotData};
let x = vec![0.0, 1.0, 2.0];
let ys = vec![vec![1.0, 2.0, 1.0], vec![2.0, 1.0, 2.0]];
let config = StackPlotConfig::default();
let input = StackedAreaInput::new(&x, &ys);
let stack_data = StackedArea::compute(input, &config).unwrap();
let ((x_min, x_max), (y_min, y_max)) = stack_data.data_bounds();
assert!((x_min - 0.0).abs() < 1e-10);
assert!((x_max - 2.0).abs() < 1e-10);
assert!(y_min <= y_max);
assert!(!stack_data.is_empty());
}
fn stack_input() -> (Vec<f64>, Vec<String>, Vec<Vec<f64>>) {
(
vec![0.0, 1.0, 2.0],
vec!["a".to_string(), "b".to_string()],
vec![vec![1.0, 2.0, 1.0], vec![2.0, 1.0, 2.0]],
)
}
#[test]
fn a_stacked_area_splits_into_one_band_per_named_column() {
let (x, names, ys) = stack_input();
let bands = stacked_area_bands(&x, &names, &ys, &StackPlotConfig::default());
assert_eq!(bands.len(), 2, "one series per named value column");
assert_eq!(bands[0].0, "a");
assert_eq!(bands[1].0, "b");
assert_eq!(bands[0].1.lower, vec![0.0, 0.0, 0.0]);
assert_eq!(bands[0].1.upper, vec![1.0, 2.0, 1.0]);
assert_eq!(bands[1].1.lower, vec![1.0, 2.0, 1.0]);
assert_eq!(bands[1].1.upper, vec![3.0, 3.0, 3.0]);
}
#[test]
fn bands_report_cumulative_bounds() {
let (x, names, ys) = stack_input();
let bands = stacked_area_bands(&x, &names, &ys, &StackPlotConfig::default());
assert_eq!(bands[0].1.data_bounds(), ((0.0, 2.0), (0.0, 2.0)));
assert_eq!(bands[1].1.data_bounds(), ((0.0, 2.0), (1.0, 3.0)));
let top = bands
.iter()
.map(|(_, b)| b.data_bounds().1.1)
.fold(f64::NEG_INFINITY, f64::max);
let whole =
StackedArea::compute(StackedAreaInput::new(&x, &ys), &StackPlotConfig::default())
.unwrap();
assert_eq!(top, whole.data_bounds().1.1);
}
#[test]
fn only_the_bands_underneath_carry_a_separator() {
let (x, names, ys) = stack_input();
let config = StackPlotConfig::default().lines(true);
let bands = stacked_area_bands(&x, &names, &ys, &config);
assert!(bands[0].1.show_line);
assert!(!bands[1].1.show_line);
}
#[test]
fn a_band_draws_one_filled_polygon() {
let (x, names, ys) = stack_input();
let bands = stacked_area_bands(&x, &names, &ys, &StackPlotConfig::default());
let band = &bands[0].1;
let ((x_min, x_max), (y_min, y_max)) = band.data_bounds();
let area = PlotArea::new(0.0, 0.0, 200.0, 100.0, x_min, x_max, y_min, y_max);
let style = ComputedStyle::opaque(
crate::core::units::RenderScale::new(96.0),
Color::from_rgb(10, 20, 30),
);
let primitives = band.primitives(&area, &style);
assert_eq!(primitives.len(), 1);
assert!(matches!(
&primitives[0],
PlotPrimitive::Polygon {
points,
fill: Some(_),
edge: None,
} if points.len() == 6
));
}
}