perspective_viewer/config/
column_config_schema.rs1use std::collections::HashSet;
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18use super::{CssKind, KeyValueOpts, NumberSeriesStyleDefaultConfig};
19
20#[derive(Clone, Debug, Default, Deserialize, Serialize)]
25pub struct ColumnConfigSchema {
26 pub fields: Vec<ControlSpec>,
27}
28
29impl ColumnConfigSchema {
30 pub fn active_keys(&self) -> HashSet<String> {
36 let mut out = HashSet::new();
37 for spec in &self.fields {
38 for k in spec.serialized_keys() {
39 out.insert(k.to_string());
40 }
41 }
42 out
43 }
44}
45
46#[derive(Clone, Debug, Deserialize, Serialize)]
52#[serde(tag = "kind")]
53pub enum ControlSpec {
54 Enum {
55 key: String,
56 variants: Vec<EnumVariant>,
57 default: String,
58 },
59 Bool {
60 key: String,
61 default: bool,
62 },
63 Number {
64 key: String,
65 default: f64,
66
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 include: Option<bool>,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 min: Option<f64>,
73
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 max: Option<f64>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 step: Option<f64>,
79 },
80 String {
81 key: String,
82 default: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 placeholder: Option<String>,
85 },
86 Color {
87 key: String,
88 default: String,
89 },
90 Palette {
91 key: String,
92 default: String,
93
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 max: Option<usize>,
96 },
97 GradientStops {
98 key: String,
99 default: String,
100
101 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
102 discrete: bool,
103 },
104 DatetimeFormat,
105 StringFormat,
106 NumberSeriesStyle {
107 default: NumberSeriesStyleDefaultConfig,
108 },
109 Symbols {
110 default: KeyValueOpts,
111 },
112 NumberFormat,
113 AggregateDepth,
114}
115
116#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
117pub struct EnumVariant {
118 pub value: String,
119 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub label: Option<String>,
121}
122
123#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126pub struct GradientStopSpec {
127 pub color: String,
128 pub offset: f64,
129}
130
131pub fn canonicalize_gradient_stops(mut stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
135 for stop in &mut stops {
136 stop.offset = (stop.offset.clamp(0.0, 1.0) * 1000.0).round() / 1000.0;
137 }
138
139 stops.sort_by(|a, b| {
140 a.offset
141 .partial_cmp(&b.offset)
142 .unwrap_or(std::cmp::Ordering::Equal)
143 });
144
145 stops
146}
147
148pub fn discrete_pair(stops: Vec<GradientStopSpec>) -> Vec<GradientStopSpec> {
151 let stops = canonicalize_gradient_stops(stops);
152 match (stops.first(), stops.last()) {
153 (Some(first), Some(last)) if stops.len() > 2 => vec![
154 GradientStopSpec {
155 color: first.color.clone(),
156 offset: 0.0,
157 },
158 GradientStopSpec {
159 color: last.color.clone(),
160 offset: 1.0,
161 },
162 ],
163 _ => stops,
164 }
165}
166
167impl ColumnConfigSchema {
168 pub fn canonicalize_defaults(mut self) -> Self {
171 self.fields.retain_mut(|spec| {
172 let (kind, key, default) = match spec {
173 ControlSpec::Color { key, default } => (CssKind::Color, key, default),
174 ControlSpec::Palette { key, default, .. } => (CssKind::Palette, key, default),
175 ControlSpec::GradientStops { key, default, .. } => {
176 (CssKind::Gradient, key, default)
177 },
178 _ => return true,
179 };
180
181 match kind.canonicalize(default) {
182 Ok(canonical) => {
183 *default = canonical;
184 true
185 },
186 Err(error) => {
187 tracing::error!("Dropping `{key}` — invalid schema default: {error}");
188 false
189 },
190 }
191 });
192
193 self
194 }
195
196 pub fn css_kind_of(&self, key: &str) -> Option<CssKind> {
198 self.fields.iter().find_map(|spec| match spec {
199 ControlSpec::Color { key: k, .. } if k == key => Some(CssKind::Color),
200 ControlSpec::Palette { key: k, .. } if k == key => Some(CssKind::Palette),
201 ControlSpec::GradientStops { key: k, .. } if k == key => Some(CssKind::Gradient),
202 _ => None,
203 })
204 }
205}
206
207impl ControlSpec {
208 pub fn serialized_keys(&self) -> Vec<&str> {
214 match self {
215 ControlSpec::DatetimeFormat => vec!["date_format"],
216 ControlSpec::StringFormat => vec!["format"],
217 ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
218 ControlSpec::Symbols { .. } => vec!["symbols"],
219 ControlSpec::NumberFormat => vec!["number_format"],
220 ControlSpec::AggregateDepth => vec!["aggregate_depth"],
221 ControlSpec::Enum { key, .. }
222 | ControlSpec::Bool { key, .. }
223 | ControlSpec::Number { key, .. }
224 | ControlSpec::String { key, .. }
225 | ControlSpec::Color { key, .. }
226 | ControlSpec::Palette { key, .. }
227 | ControlSpec::GradientStops { key, .. } => vec![key.as_str()],
228 }
229 }
230}
231
232#[derive(Clone, Debug, Deserialize, Serialize)]
237pub struct ColumnConfigFieldUpdate {
238 pub keys: Vec<String>,
239 pub value: serde_json::Map<String, Value>,
240}
241
242pub fn filter_to_schema(
246 config: &serde_json::Map<String, Value>,
247 active_keys: &HashSet<String>,
248) -> serde_json::Map<String, Value> {
249 config
250 .iter()
251 .filter(|(k, _)| active_keys.contains(k.as_str()))
252 .map(|(k, v)| (k.clone(), v.clone()))
253 .collect()
254}