use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use super::{ChartSpec, MAX_SERIES, validate_number, validate_text};
use crate::error::{ChartError, ChartWarning};
pub(crate) const MAX_COLUMNS: usize = 12;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MosaicSpec {
pub columns: Vec<MosaicColumnSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MosaicColumnSpec {
pub label: String,
pub segments: Vec<MosaicSegmentSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct MosaicSegmentSpec {
pub label: String,
pub value: f64,
}
impl MosaicSpec {
pub(crate) fn segments(&self) -> Vec<&str> {
let mut labels: Vec<&str> = Vec::new();
for segment in self.columns.iter().flat_map(|column| &column.segments) {
if !labels.contains(&segment.label.as_str()) {
labels.push(&segment.label);
}
}
labels
}
pub(crate) fn column_total(&self, column: usize) -> f64 {
self.columns[column]
.segments
.iter()
.map(|segment| segment.value)
.sum()
}
pub(crate) fn total(&self) -> f64 {
(0..self.columns.len())
.map(|column| self.column_total(column))
.sum()
}
}
impl ChartSpec {
pub(super) fn validate_mosaic(&self) -> Result<Vec<ChartWarning>, ChartError> {
for (field, present) in [
("/data", !self.data.is_empty()),
("/categories", !self.categories.is_empty()),
("/series", !self.series.is_empty()),
("/zoomSteps", !self.zoom_steps.is_empty()),
("/panes", !self.panes.is_empty()),
("/references", !self.references.is_empty()),
("/timeAxis", !self.time_axis.is_default()),
(
"/valueAxis",
self.value_axis != super::ValueAxisSpec::default(),
),
] {
if present {
return Err(ChartError::new(
"option_not_supported",
field,
"a mosaic chart is drawn from columns and segments; remove this field",
));
}
}
let Some(mosaic) = &self.mosaic else {
return Err(ChartError::new(
"missing_mosaic",
"/mosaic",
"a mosaic chart requires a mosaic block",
));
};
if mosaic.columns.is_empty() {
return Err(ChartError::new(
"empty_data",
"/mosaic/columns",
"provide at least one column",
));
}
if mosaic.columns.len() > MAX_COLUMNS {
return Err(ChartError::new(
"too_many_data_points",
"/mosaic/columns",
format!("at most {MAX_COLUMNS} columns are supported"),
));
}
let mut columns = BTreeSet::new();
for (index, column) in mosaic.columns.iter().enumerate() {
let path = format!("/mosaic/columns/{index}");
validate_text(&column.label, &format!("{path}/label"), 100)?;
if !columns.insert(column.label.as_str()) {
return Err(ChartError::new(
"duplicate_label",
format!("{path}/label"),
"column labels must be unique",
));
}
if column.segments.is_empty() {
return Err(ChartError::new(
"empty_data",
format!("{path}/segments"),
"provide at least one segment",
));
}
let mut segments = BTreeSet::new();
for (position, segment) in column.segments.iter().enumerate() {
let path = format!("{path}/segments/{position}");
validate_text(&segment.label, &format!("{path}/label"), 100)?;
if !segments.insert(segment.label.as_str()) {
return Err(ChartError::new(
"duplicate_label",
format!("{path}/label"),
"segment labels must be unique within a column",
));
}
validate_number(segment.value, &format!("{path}/value"))?;
if segment.value <= 0.0 {
return Err(ChartError::new(
"invalid_value",
format!("{path}/value"),
"a segment has a value above zero; leave a segment out of a column to give it none",
));
}
}
}
if mosaic.segments().len() > MAX_SERIES {
return Err(ChartError::new(
"too_many_series",
"/mosaic/columns",
format!(
"at most {MAX_SERIES} different segment labels are supported, one per palette color"
),
));
}
Ok(Vec::new())
}
}