use crate::axis::AxisConfig;
use crate::interaction::Pin;
use crate::series::Series;
use crate::style::Theme;
use crate::view::{Range, View, Viewport};
#[derive(Debug, Clone)]
pub struct Plot {
theme: Theme,
x_axis: AxisConfig,
y_axis: AxisConfig,
view: View,
viewport: Option<Viewport>,
series: Vec<Series>,
pins: Vec<Pin>,
}
impl Plot {
pub fn new() -> Self {
Self {
theme: Theme::default(),
x_axis: AxisConfig::default(),
y_axis: AxisConfig::default(),
view: View::default(),
viewport: None,
series: Vec::new(),
pins: Vec::new(),
}
}
pub fn builder() -> PlotBuilder {
PlotBuilder::default()
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn set_theme(&mut self, theme: Theme) {
self.theme = theme;
}
pub fn x_axis(&self) -> &AxisConfig {
&self.x_axis
}
pub fn y_axis(&self) -> &AxisConfig {
&self.y_axis
}
pub fn view(&self) -> View {
self.view
}
pub fn viewport(&self) -> Option<Viewport> {
self.viewport
}
pub fn series(&self) -> &[Series] {
&self.series
}
pub fn series_mut(&mut self) -> &mut Vec<Series> {
&mut self.series
}
pub fn add_series(&mut self, series: &Series) {
self.series.push(series.share());
}
pub fn pins(&self) -> &[Pin] {
&self.pins
}
pub fn pins_mut(&mut self) -> &mut Vec<Pin> {
&mut self.pins
}
pub fn data_bounds(&self) -> Option<Viewport> {
let mut x_range: Option<Range> = None;
let mut y_range: Option<Range> = None;
for series in &self.series {
if !series.is_visible() {
continue;
}
if let Some(bounds) = series.bounds() {
x_range = Some(match x_range {
None => bounds.x,
Some(existing) => Range::union(existing, bounds.x)?,
});
y_range = Some(match y_range {
None => bounds.y,
Some(existing) => Range::union(existing, bounds.y)?,
});
}
}
match (x_range, y_range) {
(Some(x), Some(y)) => Some(Viewport::new(x, y)),
_ => None,
}
}
pub fn set_manual_view(&mut self, viewport: Viewport) {
self.view = View::Manual;
self.viewport = Some(viewport);
}
pub fn reset_view(&mut self) {
self.view = View::default();
self.viewport = None;
}
pub fn refresh_viewport(&mut self, padding_frac: f64, min_padding: f64) -> Option<Viewport> {
let bounds = self.data_bounds()?;
match self.view {
View::AutoAll { auto_x, auto_y } => {
let mut next = bounds;
if let Some(current) = self.viewport {
if !auto_x {
next.x = current.x;
}
if !auto_y {
next.y = current.y;
}
}
self.viewport = Some(next.padded(padding_frac, min_padding));
}
View::Manual => {
if self.viewport.is_none() {
self.viewport = Some(bounds);
}
}
View::FollowLastN { points } => {
self.viewport = self.follow_last(points, false);
}
View::FollowLastNXY { points } => {
self.viewport = self.follow_last(points, true);
}
}
self.viewport
}
fn follow_last(&self, points: usize, follow_y: bool) -> Option<Viewport> {
let mut max_series: Option<&Series> = None;
let mut max_point: Option<crate::geom::Point> = None;
for series in &self.series {
if !series.is_visible() {
continue;
}
let last_point = series.with_store(|store| store.data().points().last().copied());
if let Some(point) = last_point
&& max_point.is_none_or(|max| point.x > max.x)
{
max_point = Some(point);
max_series = Some(series);
}
}
let max_series = max_series?;
let max_point = max_point?;
let (len, start_point) = max_series.with_store(|store| {
let data = store.data();
let len = data.len();
let start_index = len.saturating_sub(points);
(len, data.point(start_index))
});
if len == 0 {
return None;
}
let start_point = start_point?;
let x_range = Range::new(start_point.x, max_point.x);
let y_range = if follow_y {
let mut y_range: Option<Range> = None;
for series in &self.series {
if !series.is_visible() {
continue;
}
series.with_store(|store| {
let series_data = store.data();
let index_range = series_data.range_by_x(x_range);
for index in index_range {
if let Some(point) = series_data.point(index) {
y_range = Some(match y_range {
None => Range::new(point.y, point.y),
Some(mut existing) => {
existing.expand_to_include(point.y);
existing
}
});
}
}
});
}
y_range?
} else if let Some(current) = self.viewport {
current.y
} else {
self.data_bounds()?.y
};
Some(Viewport::new(x_range, y_range))
}
}
impl Default for Plot {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default)]
pub struct PlotBuilder {
theme: Theme,
x_axis: AxisConfig,
y_axis: AxisConfig,
view: View,
series: Vec<Series>,
}
impl PlotBuilder {
pub fn theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
pub fn x_axis(mut self, axis: AxisConfig) -> Self {
self.x_axis = axis;
self
}
pub fn y_axis(mut self, axis: AxisConfig) -> Self {
self.y_axis = axis;
self
}
pub fn view(mut self, view: View) -> Self {
self.view = view;
self
}
pub fn series(mut self, series: &Series) -> Self {
self.series.push(series.share());
self
}
pub fn build(self) -> Plot {
Plot {
theme: self.theme,
x_axis: self.x_axis,
y_axis: self.y_axis,
view: self.view,
viewport: None,
series: self.series,
pins: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::series::Series;
#[test]
fn add_series_uses_shared_data_stream() {
let mut source = Series::line("shared");
let _ = source.extend_y([1.0, 2.0]);
let mut plot = Plot::new();
plot.add_series(&source);
let initial_bounds = plot.data_bounds().expect("plot bounds");
assert_eq!(initial_bounds.y.min, 1.0);
assert_eq!(initial_bounds.y.max, 2.0);
let _ = source.push_y(3.0);
let next_bounds = plot.data_bounds().expect("plot bounds");
assert_eq!(next_bounds.y.max, 3.0);
}
#[test]
fn series_mut_can_remove_series() {
let mut first = Series::line("first");
let mut second = Series::line("second");
let _ = first.push_y(1.0);
let _ = second.push_y(9.0);
let mut plot = Plot::new();
plot.add_series(&first);
plot.add_series(&second);
let removed = plot.series_mut().remove(1);
assert_eq!(removed.name(), "second");
assert_eq!(plot.series().len(), 1);
assert_eq!(plot.series()[0].name(), "first");
}
}