mod architecture;
mod boxplot;
mod calendar;
mod categorical;
mod classes;
mod dataset;
mod deployment;
mod flow;
mod network;
mod parliament;
mod rangebar;
mod sankey;
mod scatter;
mod sequence;
mod state;
mod stripes;
mod survival;
mod timechart;
mod timeline;
mod tree;
mod treemap;
pub(crate) mod untagged;
mod validate;
mod view;
mod waffle;
mod waterfall;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{
error::{ChartError, ChartWarning},
time::TimeValue,
};
pub use architecture::{
ArchitectureSpec, BoundarySpec, CategorySpec, ComponentKind, ComponentSpec,
ConnectionDirection, ConnectionSpec, InterfaceSpec, PerspectiveSpec,
};
pub(crate) use boxplot::BoxSummary;
pub use boxplot::{BoxDisplay, BoxSpec, ViolinScale};
pub(crate) use calendar::calendar_date;
pub use calendar::{CalendarDay, CalendarLayout, CalendarSpec};
pub use categorical::{DataPoint, SeriesSpec};
pub use classes::{ClassKind, ClassSpec, ClassesSpec, PackageSpec, RelationKind, RelationSpec};
pub use flow::{
DiagramRole, DiagramSpacing, FlowEdgeSpec, FlowNodeSpec, FlowSpec, GroupSpec, LaneSpec,
MainPath, NodeKind, NoteSpec, PartSpec, PortSide,
};
pub use network::{NetworkEdgeSpec, NetworkLayout, NetworkNodeSpec, NetworkOrder, NetworkSpec};
pub use parliament::{ParliamentSpec, PartySpec};
pub use rangebar::RangeSpec;
pub(crate) use sankey::SankeyGraph;
pub use sankey::{SankeyLinkSpec, SankeyNodeSpec, SankeyOrder, SankeySpec};
pub(crate) use scatter::Quadrant;
pub use scatter::{
ScatterAxis, ScatterLineSpec, ScatterPointSpec, ScatterQuadrantLabels, ScatterQuadrantsSpec,
ScatterSpec, ScatterTickSpec,
};
pub use sequence::{
BranchSpec, DiagramOrientation, FragmentKind, FragmentSpec, MessageKind, MessageSpec,
ParticipantKind, ParticipantSpec, SequenceSpec,
};
pub use state::{StateKind, StateNodeSpec, StateSpec, TransitionSpec};
pub(crate) use stripes::Diverging;
pub use stripes::StripesSpec;
pub(crate) use survival::{KmCurve, KmStep};
pub use survival::{SurvivalGroupSpec, SurvivalObservationSpec, SurvivalSpec};
pub use timechart::{
Curve, Dash, Gaps, LayerSpec, Mark, OhlcPoint, PaneSpec, Shape, Stroke, TimeAxisKind,
TimeAxisSpec, TimePoint, TimePrecision, Tooltips,
};
pub(crate) use timechart::{LayerContext, MAX_TIME_POINTS_PER_LAYER, validate_layer_name};
pub(crate) use timeline::Span;
pub use timeline::{MarkerSpec, TimelineItemSpec, TimelineSpec};
pub use tree::{DistancesSpec, Linkage, TreeLayout, TreeNodeKind, TreeNodeSpec, TreeSpec};
pub(crate) use treemap::{TmNode, TmTree};
pub use treemap::{TreemapItemSpec, TreemapSpec};
pub(crate) use validate::validate_number;
use validate::{json_pointer, validate_optional_text, validate_text};
pub use view::{InstanceSpec, ViewSpec, ViewStepSpec};
pub use waffle::{WafflePartSpec, WaffleSpec};
pub(crate) use waterfall::WaterfallBar;
pub use waterfall::{StepKind, StepSpec, WaterfallSpec};
const MAX_DATA_POINTS: usize = 100;
pub(crate) const MAX_SERIES: usize = 4;
pub(crate) const MAX_AXIS_TICKS: f64 = 50.0;
pub(crate) const MAX_REFERENCES: usize = 4;
pub(crate) const MAX_DECIMALS: u8 = 6;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[allow(
clippy::struct_excessive_bools,
reason = "every bool is a switch of the JSON specification"
)]
pub struct ChartSpec {
pub schema_version: u8,
#[serde(rename = "type")]
pub chart_type: ChartType,
#[serde(default)]
pub orientation: Orientation,
#[serde(default, skip_serializing_if = "BoxDisplay::is_box")]
pub box_display: BoxDisplay,
#[serde(default, skip_serializing_if = "ViolinScale::is_width")]
pub violin_scale: ViolinScale,
#[serde(default, skip_serializing_if = "is_false")]
pub show_mean: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub patterns: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stack: Option<Stack>,
pub title: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub category_axis: CategoryAxisSpec,
#[serde(default)]
pub value_axis: ValueAxisSpec,
#[serde(default = "default_width")]
pub width: u32,
#[serde(default = "default_height")]
pub height: u32,
#[serde(default = "default_show_values")]
pub show_values: bool,
#[serde(default = "default_true", skip_serializing_if = "is_true")]
pub show_title: bool,
#[serde(default)]
pub data: Vec<DataPoint>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub categories: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub series: Vec<SeriesSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub zoom_steps: Vec<ZoomStep>,
#[serde(default, skip_serializing_if = "Theme::is_light")]
pub theme: Theme,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub tokens: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "TimeAxisSpec::is_default")]
pub time_axis: TimeAxisSpec,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub panes: Vec<PaneSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stripes: Option<StripesSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub calendar: Option<CalendarSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sequence: Option<SequenceSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flow: Option<FlowSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<StateSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub architecture: Option<ArchitectureSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tree: Option<TreeSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub waterfall: Option<WaterfallSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub waffle: Option<WaffleSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parliament: Option<ParliamentSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub treemap: Option<TreemapSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sankey: Option<SankeySpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub survival: Option<SurvivalSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scatter: Option<ScatterSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub classes: Option<ClassesSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network: Option<NetworkSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeline: Option<TimelineSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub boxes: Vec<BoxSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ranges: Vec<RangeSpec>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub references: Vec<ReferenceSpec>,
#[serde(default, skip_serializing_if = "is_false")]
pub sparkline: bool,
#[serde(default, skip_serializing_if = "Tooltips::is_observations")]
pub tooltips: Tooltips,
#[serde(default, skip_serializing_if = "LegendPlacement::is_top")]
pub legend: LegendPlacement,
#[serde(default, skip_serializing_if = "is_false")]
pub independent_axes: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub columns: Option<u32>,
#[serde(default, skip_serializing_if = "Locale::is_en")]
pub locale: Locale,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mobile: Option<MobileSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MobileSpec {
pub width: u32,
#[serde(default = "default_mobile_height")]
pub height: u32,
#[serde(default = "default_breakpoint")]
pub breakpoint: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub columns: Option<u32>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Theme {
#[default]
Light,
Dark,
Blueprint,
}
impl Theme {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_light(&self) -> bool {
matches!(self, Self::Light)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Locale {
#[default]
En,
De,
}
impl Locale {
pub(crate) const fn code(self) -> &'static str {
match self {
Self::En => "en",
Self::De => "de",
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_en(&self) -> bool {
matches!(self, Self::En)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct NumberStyle {
pub format: ValueFormat,
pub decimals: Option<u8>,
pub locale: Locale,
pub thousands: bool,
}
impl From<ValueFormat> for NumberStyle {
fn from(format: ValueFormat) -> Self {
Self {
format,
..Self::default()
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ChartType {
Bar,
Line,
Time,
Stripes,
Calendar,
Rangebar,
Multiples,
Sequence,
Flow,
State,
Architecture,
Tree,
Boxplot,
Timeline,
Waterfall,
Waffle,
Parliament,
Treemap,
Sankey,
Survival,
Scatter,
Classes,
Network,
}
impl ChartType {
pub const ALL: [Self; 23] = [
Self::Bar,
Self::Line,
Self::Time,
Self::Stripes,
Self::Calendar,
Self::Rangebar,
Self::Multiples,
Self::Sequence,
Self::Flow,
Self::State,
Self::Architecture,
Self::Tree,
Self::Boxplot,
Self::Timeline,
Self::Waterfall,
Self::Waffle,
Self::Parliament,
Self::Treemap,
Self::Sankey,
Self::Survival,
Self::Scatter,
Self::Classes,
Self::Network,
];
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL
.into_iter()
.find(|chart_type| type_name(*chart_type) == name)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LegendPlacement {
#[default]
Top,
End,
}
impl LegendPlacement {
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_top(&self) -> bool {
matches!(self, Self::Top)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Stack {
Normal,
Percent,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Orientation {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CategoryAxisSpec {
#[serde(default)]
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ValueAxisSpec {
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub format: ValueFormat,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub decimals: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub thousands_separator: bool,
#[serde(default, skip_serializing_if = "AxisScale::is_linear")]
pub scale: AxisScale,
#[serde(default, skip_serializing_if = "is_false")]
pub reverse: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step: Option<f64>,
#[serde(default, skip_serializing_if = "is_false")]
pub exact: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
}
impl ChartSpec {
pub(crate) fn axis_unit(&self, pane: usize) -> Option<&str> {
match self.chart_type {
ChartType::Time => self.panes.get(pane)?.value_axis.unit.as_deref(),
ChartType::Multiples if self.is_bar_multiples() => {
self.panes.get(pane)?.value_axis.unit.as_deref()
}
_ => self.value_axis.unit.as_deref(),
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AxisScale {
#[default]
Linear,
Log,
}
impl AxisScale {
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) const fn is_linear(&self) -> bool {
matches!(self, Self::Linear)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ReferenceSpec {
pub value: f64,
pub label: String,
}
impl ValueAxisSpec {
pub(crate) const fn bounds(&self) -> (Option<f64>, Option<f64>) {
(self.min, self.max)
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ValueFormat {
#[default]
Number,
Percent,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ZoomStep {
pub label: String,
pub from: ZoomBound,
pub to: ZoomBound,
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ZoomBound {
Index(usize),
Time(TimeValue),
}
impl<'de> Deserialize<'de> for ZoomBound {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
untagged::deserialize(deserializer, "ZoomBound", |shape| {
let index = match shape {
untagged::Shape::Unsigned(number) => usize::try_from(number).ok(),
untagged::Shape::Signed(number) => usize::try_from(number).ok(),
_ => None,
};
match index {
Some(index) => Some(Self::Index(index)),
None => TimeValue::from_shape(shape).map(Self::Time),
}
})
}
}
impl ZoomBound {
pub(crate) const fn index(&self) -> Option<usize> {
match self {
Self::Index(index) => Some(*index),
Self::Time(_) => None,
}
}
pub(crate) fn resolve(&self, zone: crate::time::TimeZone) -> Result<i64, &'static str> {
match self {
Self::Index(seconds) => {
#[allow(clippy::cast_precision_loss)]
let seconds = *seconds as f64;
TimeValue::Number(seconds).resolve(zone)
}
Self::Time(time) => time.resolve(zone),
}
}
}
#[derive(Clone, Copy)]
pub(crate) struct LayerRef<'a> {
pub global: usize,
pub pane: usize,
pub local: usize,
pub layer: &'a LayerSpec,
}
pub(crate) struct Dataset {
pub categories: Vec<String>,
pub series: Vec<Series>,
}
pub(crate) struct Series {
pub name: Option<String>,
pub values: Vec<Option<f64>>,
pub style: Option<NumberStyle>,
}
impl Dataset {
pub(crate) fn values(&self) -> impl Iterator<Item = f64> + '_ {
self.series
.iter()
.flat_map(|series| series.values.iter().flatten().copied())
}
pub(crate) fn category_path(&self, index: usize) -> String {
if self.series[0].name.is_none() {
format!("/data/{index}/label")
} else {
format!("/categories/{index}")
}
}
}
impl ChartSpec {
pub fn from_json(input: &str) -> Result<Self, ChartError> {
serde_json::from_str(input).map_err(|error: serde_json::Error| {
let (code, (path, message)) = match error.classify() {
serde_json::error::Category::Syntax | serde_json::error::Category::Eof => {
("invalid_json", ("/".to_owned(), error.to_string()))
}
serde_json::error::Category::Data | serde_json::error::Category::Io => {
("invalid_spec", json_pointer(input, &error))
}
};
ChartError::new(code, path, message)
})
}
pub(crate) fn validate(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.validate_sized(true)
}
pub(crate) fn mobile_variant(&self) -> Option<ChartSpec> {
let mobile = self.mobile.as_ref()?;
let mut spec = self.clone();
spec.width = mobile.width;
spec.height = mobile.height;
if mobile.columns.is_some() {
spec.columns = mobile.columns;
}
spec.mobile = None;
Some(spec)
}
pub(crate) fn validate_mobile_variant(&self) -> Result<Vec<ChartWarning>, ChartError> {
self.validate_sized(false)
}
fn validate_sized(&self, check_size: bool) -> Result<Vec<ChartWarning>, ChartError> {
self.validate_metadata(check_size)?;
self.reject_foreign_blocks()?;
self.validate_theme()?;
self.validate_value_axes()?;
let mut warnings = match self.chart_type {
ChartType::Time => self.validate_time(),
ChartType::Multiples => self.validate_multiples(),
ChartType::Stripes => self.validate_stripes(),
ChartType::Calendar => self.validate_calendar(),
ChartType::Rangebar => self.validate_rangebar(),
ChartType::Sequence => self.validate_sequence(),
ChartType::Flow => self.validate_flow(),
ChartType::State => self.validate_state(),
ChartType::Architecture => self.validate_architecture(),
ChartType::Tree => self.validate_tree(),
ChartType::Boxplot => self.validate_boxplot(),
ChartType::Timeline => self.validate_timeline(),
ChartType::Classes => self.validate_classes(),
ChartType::Scatter => self.validate_scatter(),
ChartType::Survival => self.validate_survival(),
ChartType::Sankey => self.validate_sankey(),
ChartType::Treemap => self.validate_treemap(),
ChartType::Parliament => self.validate_parliament(),
ChartType::Waffle => self.validate_waffle(),
ChartType::Waterfall => self.validate_waterfall(),
ChartType::Network => self.validate_network(),
ChartType::Bar | ChartType::Line => self.validate_bar_and_line(),
}?;
warnings.extend(crate::contrast::warnings(self));
Ok(warnings)
}
}
pub(crate) const fn type_name(chart_type: ChartType) -> &'static str {
match chart_type {
ChartType::Bar => "bar",
ChartType::Line => "line",
ChartType::Time => "time",
ChartType::Stripes => "stripes",
ChartType::Calendar => "calendar",
ChartType::Rangebar => "rangebar",
ChartType::Multiples => "multiples",
ChartType::Sequence => "sequence",
ChartType::Flow => "flow",
ChartType::State => "state",
ChartType::Architecture => "architecture",
ChartType::Tree => "tree",
ChartType::Boxplot => "boxplot",
ChartType::Timeline => "timeline",
ChartType::Classes => "classes",
ChartType::Scatter => "scatter",
ChartType::Survival => "survival",
ChartType::Sankey => "sankey",
ChartType::Treemap => "treemap",
ChartType::Parliament => "parliament",
ChartType::Waffle => "waffle",
ChartType::Waterfall => "waterfall",
ChartType::Network => "network",
}
}
const fn default_width() -> u32 {
800
}
const fn default_height() -> u32 {
450
}
const fn default_mobile_height() -> u32 {
360
}
const fn default_breakpoint() -> u32 {
640
}
const fn default_show_values() -> bool {
true
}
const fn default_true() -> bool {
true
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(value: &bool) -> bool {
!*value
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_true(value: &bool) -> bool {
*value
}