use super::data::{BarDataset, DataPoint, Dataset, Series};
use super::encoding::Field;
use super::field_value::DataTable;
use super::mark::Mark;
use super::theme::{ChartConfig, GridStyle};
pub struct Missing;
pub struct Set<T>(pub T);
#[derive(Debug, Clone)]
pub enum ChartData {
TimeSeries(Dataset),
Categorical(BarDataset),
Table(DataTable),
}
impl ChartData {
pub fn as_dataset(&self) -> Option<&Dataset> {
match self {
Self::TimeSeries(ds) => Some(ds),
_ => None,
}
}
pub fn as_bar_dataset(&self) -> Option<&BarDataset> {
match self {
Self::Categorical(bd) => Some(bd),
_ => None,
}
}
pub fn as_table(&self) -> Option<&DataTable> {
match self {
Self::Table(t) => Some(t),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ChartSpec {
pub data: ChartData,
pub mark: Mark,
pub x: Field,
pub y: Option<Field>,
pub color: Option<Field>,
pub size: Option<Field>,
pub config: ChartConfig,
}
impl ChartSpec {
pub fn builder() -> ChartSpecBuilder<Missing, Missing, Missing> {
ChartSpecBuilder {
data: Missing,
mark: Missing,
x: Missing,
y: None,
color: None,
size: None,
config: ChartConfig::default(),
}
}
}
pub struct ChartSpecBuilder<D, M, X> {
data: D,
mark: M,
x: X,
y: Option<Field>,
color: Option<Field>,
size: Option<Field>,
config: ChartConfig,
}
impl<M, X> ChartSpecBuilder<Missing, M, X> {
pub fn data(self, dataset: Dataset) -> ChartSpecBuilder<Set<ChartData>, M, X> {
ChartSpecBuilder {
data: Set(ChartData::TimeSeries(dataset)),
mark: self.mark,
x: self.x,
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
pub fn data_points(self, points: Vec<DataPoint>) -> ChartSpecBuilder<Set<ChartData>, M, X> {
let series = Series::new("default", points);
let dataset = Dataset::from_series(series);
self.data(dataset)
}
pub fn bar_data(self, bar_dataset: BarDataset) -> ChartSpecBuilder<Set<ChartData>, M, X> {
ChartSpecBuilder {
data: Set(ChartData::Categorical(bar_dataset)),
mark: self.mark,
x: self.x,
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
pub fn from_table(self, table: DataTable) -> ChartSpecBuilder<Set<ChartData>, M, X> {
ChartSpecBuilder {
data: Set(ChartData::Table(table)),
mark: self.mark,
x: self.x,
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
}
impl<D, X> ChartSpecBuilder<D, Missing, X> {
pub fn mark(self, mark: Mark) -> ChartSpecBuilder<D, Set<Mark>, X> {
ChartSpecBuilder {
data: self.data,
mark: Set(mark),
x: self.x,
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
}
impl<D, M> ChartSpecBuilder<D, M, Missing> {
pub fn x(self, field: Field) -> ChartSpecBuilder<D, M, Set<Field>> {
ChartSpecBuilder {
data: self.data,
mark: self.mark,
x: Set(field),
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
}
impl<D, M, X> ChartSpecBuilder<D, M, X> {
pub fn y(mut self, field: Field) -> Self {
self.y = Some(field);
self
}
pub fn color(mut self, field: Field) -> Self {
self.color = Some(field);
self
}
pub fn size(mut self, field: Field) -> Self {
self.size = Some(field);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.config.title = Some(title.into());
self
}
pub fn grid(mut self, show: bool) -> Self {
self.config.grid = Some(GridStyle {
show_x: show,
show_y: show,
..GridStyle::default()
});
self
}
pub fn config(mut self, config: ChartConfig) -> Self {
self.config = config;
self
}
}
impl ChartSpecBuilder<Set<ChartData>, Set<Mark>, Set<Field>> {
pub fn build(self) -> ChartSpec {
ChartSpec {
data: self.data.0,
mark: self.mark.0,
x: self.x.0,
y: self.y,
color: self.color,
size: self.size,
config: self.config,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::data::DataType;
fn sample_points() -> Vec<DataPoint> {
vec![
DataPoint::new(0.0, 1.0),
DataPoint::new(1.0, 3.0),
DataPoint::new(2.0, 2.0),
]
}
#[test]
fn test_builder_all_required() {
let spec = ChartSpec::builder()
.data_points(sample_points())
.mark(Mark::Line)
.x(Field::temporal("time"))
.build();
assert_eq!(spec.mark, Mark::Line);
assert_eq!(spec.x.name, "time");
assert_eq!(spec.x.data_type, DataType::Temporal);
let ds = spec.data.as_dataset().expect("expected TimeSeries");
assert_eq!(ds.series.len(), 1);
assert_eq!(ds.series[0].data.len(), 3);
assert!(spec.y.is_none());
assert!(spec.color.is_none());
assert!(spec.size.is_none());
}
#[test]
fn test_builder_with_optionals() {
let spec = ChartSpec::builder()
.data_points(sample_points())
.mark(Mark::Point)
.x(Field::quantitative("x"))
.y(Field::quantitative("y"))
.color(Field::nominal("category"))
.size(Field::quantitative("magnitude"))
.title("My Chart")
.grid(true)
.build();
assert_eq!(spec.y.as_ref().unwrap().name, "y");
assert_eq!(spec.color.as_ref().unwrap().name, "category");
assert_eq!(spec.size.as_ref().unwrap().name, "magnitude");
assert_eq!(spec.config.title.as_deref(), Some("My Chart"));
assert!(spec.config.grid.as_ref().unwrap().show_x);
}
#[test]
fn test_builder_any_order() {
let spec1 = ChartSpec::builder()
.mark(Mark::Bar)
.x(Field::nominal("category"))
.data_points(sample_points())
.build();
let spec2 = ChartSpec::builder()
.x(Field::nominal("category"))
.data_points(sample_points())
.mark(Mark::Bar)
.build();
assert_eq!(spec1.mark, spec2.mark);
assert_eq!(spec1.x.name, spec2.x.name);
}
#[test]
fn test_data_points_convenience() {
let points = sample_points();
let spec = ChartSpec::builder()
.data_points(points.clone())
.mark(Mark::Line)
.x(Field::quantitative("x"))
.build();
let ds = spec.data.as_dataset().unwrap();
assert_eq!(ds.series.len(), 1);
assert_eq!(ds.series[0].name, "default");
assert_eq!(ds.series[0].data.len(), points.len());
}
#[test]
fn test_data_dataset_direct() {
let mut dataset = Dataset::new();
dataset.add_series(Series::new("s1", sample_points()));
dataset.add_series(Series::new("s2", sample_points()));
let spec = ChartSpec::builder()
.data(dataset)
.mark(Mark::Line)
.x(Field::temporal("time"))
.build();
assert_eq!(spec.data.as_dataset().unwrap().series.len(), 2);
}
#[test]
fn test_default_config() {
let spec = ChartSpec::builder()
.data_points(sample_points())
.mark(Mark::Line)
.x(Field::quantitative("x"))
.build();
assert!(spec.config.title.is_none());
assert!(spec.config.grid.is_none());
}
#[test]
fn test_from_table_stores_table() {
use crate::core::field_value::{DataTable, FieldValue};
use std::collections::HashMap;
let mut table = DataTable::default();
let mut row = HashMap::new();
row.insert("x".into(), FieldValue::Numeric(1.0));
row.insert("y".into(), FieldValue::Numeric(2.0));
table.push(row);
let spec = ChartSpec::builder()
.from_table(table)
.mark(Mark::Line)
.x(Field::temporal("x"))
.y(Field::quantitative("y"))
.build();
assert!(spec.data.as_table().is_some());
assert_eq!(spec.data.as_table().unwrap().len(), 1);
}
#[test]
fn test_bar_data_categorical() {
let mut bd = BarDataset::new(vec!["Q1".into(), "Q2".into()]);
bd.add_series("Revenue", vec![100.0, 150.0]);
let spec = ChartSpec::builder()
.bar_data(bd)
.mark(Mark::Bar)
.x(Field::nominal("period"))
.build();
assert!(spec.data.as_bar_dataset().is_some());
assert_eq!(spec.data.as_bar_dataset().unwrap().series.len(), 1);
}
}