use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{CssKind, KeyValueOpts, NumberSeriesStyleDefaultConfig};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ColumnConfigSchema {
pub fields: Vec<ControlSpec>,
}
impl ColumnConfigSchema {
pub fn active_keys(&self) -> HashSet<String> {
let mut out = HashSet::new();
for spec in &self.fields {
for k in spec.serialized_keys() {
out.insert(k.to_string());
}
}
out
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "kind")]
pub enum ControlSpec {
Enum {
key: String,
variants: Vec<EnumVariant>,
default: String,
},
Bool {
key: String,
default: bool,
},
Number {
key: String,
default: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
include: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
step: Option<f64>,
},
String {
key: String,
default: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
placeholder: Option<String>,
},
Color {
key: String,
default: String,
},
Palette {
key: String,
default: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
max: Option<usize>,
},
GradientStops {
key: String,
default: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
discrete: bool,
},
DatetimeFormat,
StringFormat,
NumberSeriesStyle {
default: NumberSeriesStyleDefaultConfig,
},
Symbols {
default: KeyValueOpts,
},
NumberFormat,
AggregateDepth,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct EnumVariant {
pub value: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct GradientStopSpec {
pub color: String,
pub offset: f64,
}
pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
for stop in &mut stops {
stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
}
stops.sort_by(|a, b| {
a.offset
.partial_cmp(&b.offset)
.unwrap_or(std::cmp::Ordering::Equal)
});
stops
}
pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
let stops = canonicalize_gradient_stops(stops);
match (stops.first(), stops.last()) {
(Some(first), Some(last)) if stops.len() > 2 => vec![
GradientStopSpec {
color: first.color.clone(),
offset: 0.0,
},
GradientStopSpec {
color: last.color.clone(),
offset: 1.0,
},
],
_ => stops,
}
}
impl ColumnConfigSchema {
pub fn canonicalize_defaults(mut self) -> Self {
self.fields.retain_mut(|spec| {
let (kind, key, default) = match spec {
ControlSpec::Color { key, default } => (CssKind::Color, key, default),
ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
ControlSpec::GradientStops { key, default, .. } => {
(CssKind::Gradient, key, default)
},
_ => return true,
};
match kind.canonicalize(default) {
Ok(canonical) => {
*default = canonical;
true
},
Err(error) => {
tracing::error!("Dropping `{key}` — invalid schema default: {error}");
false
},
}
});
self
}
pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
self.fields.iter().find_map(|spec| match spec {
ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
_ => None,
})
}
}
impl ControlSpec {
pub fn serialized_keys(&self) -> Vec<&str> {
match self {
ControlSpec::DatetimeFormat => vec!["date_format"],
ControlSpec::StringFormat => vec!["format"],
ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
ControlSpec::Symbols { .. } => vec!["symbols"],
ControlSpec::NumberFormat => vec!["number_format"],
ControlSpec::AggregateDepth => vec!["aggregate_depth"],
ControlSpec::Enum { key, .. }
| ControlSpec::Bool { key, .. }
| ControlSpec::Number { key, .. }
| ControlSpec::String { key, .. }
| ControlSpec::Color { key, .. }
| ControlSpec::Palette { key, .. }
| ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ColumnConfigFieldUpdate {
pub keys: Vec<String>,
pub value: serde_json::Map<String, Value>,
}
pub fn filter_to_schema(
config: &serde_json::Map<String, Value>,
active_keys: &HashSet<String>,
) -> serde_json::Map<String, Value> {
config
.iter()
.filter(|(k, _)| active_keys.contains(k.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}