#![warn(missing_docs)]
use crate::color::Color;
use crate::plot::scale::Scale;
use crate::plot::series::SeriesHandle;
use crate::scene::style::PointShape;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum PlotControls {
#[default]
ZoomDrag,
PanDrag,
}
pub const DEFAULT_LINE_WIDTH: f32 = 1.5;
pub const DEFAULT_MARKER_SIZE: f32 = 5.0;
#[derive(Clone, Debug)]
pub struct LineMark {
pub series: SeriesHandle,
pub color: Option<Color>,
pub width: f32,
pub label: Option<String>,
}
impl LineMark {
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = width;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Clone, Debug)]
pub struct ScatterMark {
pub series: SeriesHandle,
pub color: Option<Color>,
pub size: f32,
pub shape: PointShape,
pub label: Option<String>,
}
impl ScatterMark {
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn shape(mut self, shape: PointShape) -> Self {
self.shape = shape;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Clone, Debug)]
pub enum Mark {
Line(LineMark),
Scatter(ScatterMark),
}
impl Mark {
pub fn series(&self) -> &SeriesHandle {
match self {
Mark::Line(m) => &m.series,
Mark::Scatter(m) => &m.series,
}
}
pub fn explicit_color(&self) -> Option<Color> {
match self {
Mark::Line(m) => m.color,
Mark::Scatter(m) => m.color,
}
}
pub fn color_at(&self, i: usize) -> Color {
self.explicit_color()
.unwrap_or_else(|| crate::plot::palette::series_color(i))
}
pub fn display_label(&self, i: usize) -> String {
let explicit = match self {
Mark::Line(m) => m.label.as_deref(),
Mark::Scatter(m) => m.label.as_deref(),
};
explicit
.map(str::to_owned)
.unwrap_or_else(|| format!("Series {}", i + 1))
}
}
impl From<LineMark> for Mark {
fn from(m: LineMark) -> Self {
Mark::Line(m)
}
}
impl From<ScatterMark> for Mark {
fn from(m: ScatterMark) -> Self {
Mark::Scatter(m)
}
}
pub fn line(series: &SeriesHandle) -> LineMark {
LineMark {
series: series.clone(),
color: None,
width: DEFAULT_LINE_WIDTH,
label: None,
}
}
pub fn scatter(series: &SeriesHandle) -> ScatterMark {
ScatterMark {
series: series.clone(),
color: None,
size: DEFAULT_MARKER_SIZE,
shape: PointShape::Circle,
label: None,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum LegendPosition {
#[default]
TopRight,
TopLeft,
BottomRight,
BottomLeft,
}
#[derive(Clone, Debug)]
pub struct Axis {
pub scale: Scale,
pub title: Option<String>,
}
impl Axis {
pub fn new(scale: Scale) -> Self {
Self { scale, title: None }
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
}
impl Default for Axis {
fn default() -> Self {
Self::new(Scale::linear())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlotStyle {
pub background: Option<Color>,
pub grid: bool,
pub msaa_samples: u32,
}
impl Default for PlotStyle {
fn default() -> Self {
Self {
background: None,
grid: true,
msaa_samples: 4,
}
}
}
#[derive(Clone, Debug)]
pub struct PlotSpec {
pub marks: Vec<Mark>,
pub x: Axis,
pub y: Axis,
pub style: PlotStyle,
pub crosshair: bool,
pub y_autoscale: bool,
pub controls: PlotControls,
pub legend: Option<LegendPosition>,
pub downsample: Option<crate::plot::Decimation>,
}
impl Default for PlotSpec {
fn default() -> Self {
Self {
marks: Vec::new(),
x: Axis::default(),
y: Axis::default(),
style: PlotStyle::default(),
crosshair: false,
y_autoscale: true,
controls: PlotControls::default(),
legend: None,
downsample: None,
}
}
}
impl PlotSpec {
pub fn new() -> Self {
Self::default()
}
pub fn line(mut self, series: &SeriesHandle) -> Self {
self.marks.push(Mark::Line(line(series)));
self
}
pub fn scatter(mut self, series: &SeriesHandle) -> Self {
self.marks.push(Mark::Scatter(scatter(series)));
self
}
pub fn add_mark(mut self, mark: impl Into<Mark>) -> Self {
self.marks.push(mark.into());
self
}
pub fn x(mut self, scale: Scale) -> Self {
self.x.scale = scale;
self
}
pub fn y(mut self, scale: Scale) -> Self {
self.y.scale = scale;
self
}
pub fn x_axis(mut self, axis: Axis) -> Self {
self.x = axis;
self
}
pub fn y_axis(mut self, axis: Axis) -> Self {
self.y = axis;
self
}
pub fn crosshair(mut self, on: bool) -> Self {
self.crosshair = on;
self
}
pub fn y_autoscale(mut self, on: bool) -> Self {
self.y_autoscale = on;
self
}
pub fn controls(mut self, controls: PlotControls) -> Self {
self.controls = controls;
self
}
pub fn legend(mut self, position: LegendPosition) -> Self {
self.legend = Some(position);
self
}
pub fn grid(mut self, on: bool) -> Self {
self.style.grid = on;
self
}
pub fn downsample(mut self, decimation: crate::plot::Decimation) -> Self {
self.downsample = Some(decimation);
self
}
pub fn background(mut self, color: Color) -> Self {
self.style.background = Some(color);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn handle() -> SeriesHandle {
SeriesHandle::new(vec![crate::plot::series::Sample::new(0.0, 0.0)])
}
#[test]
fn builder_collects_marks_and_axes() {
let h = handle();
let spec = PlotSpec::new()
.x(Scale::time())
.y(Scale::linear())
.line(&h)
.scatter(&h)
.crosshair(true);
assert_eq!(spec.marks.len(), 2);
assert!(matches!(spec.marks[0], Mark::Line(_)));
assert!(matches!(spec.marks[1], Mark::Scatter(_)));
assert_eq!(spec.x.scale, Scale::time());
assert!(spec.crosshair);
assert!(spec.y_autoscale); }
#[test]
fn styled_marks_via_escape_hatch() {
let h = handle();
let red = Color::srgb_u8(220, 40, 40);
let spec = PlotSpec::new().add_mark(line(&h).color(red).width(3.0));
match &spec.marks[0] {
Mark::Line(m) => {
assert_eq!(m.color, Some(red));
assert_eq!(m.width, 3.0);
}
_ => panic!("expected a line mark"),
}
}
#[test]
fn default_line_is_auto_colored() {
let h = handle();
let m = line(&h);
assert_eq!(m.color, None);
assert_eq!(m.width, DEFAULT_LINE_WIDTH);
}
}