perspective_viewer/config/column_config_schema.rs
1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors. ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::collections::HashSet;
14
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18use super::{KeyValueOpts, NumberSeriesStyleDefaultConfig};
19
20/// The full schema for one column at one point in time. Plugins may return
21/// different schemas for the same column based on the column's current
22/// stored value (e.g. to hide dependent fields), so this is re-queried on
23/// every field update.
24///
25/// Each entry is a [`ControlSpec`]. Primitive variants carry their own
26/// `key` (JSON storage key) inline; the sidebar UI label is supplied by
27/// CSS via `--psp-label--<key>--content`. Composite variants render a
28/// self-contained Yew component that supplies its own labels and owns a
29/// fixed key namespace via [`ControlSpec::serialized_keys`].
30#[derive(Clone, Debug, Default, Deserialize, Serialize)]
31pub struct ColumnConfigSchema {
32 pub fields: Vec<ControlSpec>,
33}
34
35impl ColumnConfigSchema {
36 /// Union of every JSON key any control in this schema knows how to
37 /// read or write. Used to build the schema-filtered view of
38 /// `columns_config` passed to `plugin.restore()` — keys not in this
39 /// set are "ghost" state from a different plugin and stay invisible
40 /// to the active one.
41 pub fn active_keys(&self) -> HashSet<String> {
42 let mut out = HashSet::new();
43 for spec in &self.fields {
44 for k in spec.serialized_keys() {
45 out.insert(k.to_string());
46 }
47 }
48 out
49 }
50}
51
52/// Discriminated union of widget kinds the viewer can render. Composite
53/// variants wrap an existing rich Yew component and carry only the
54/// component's `*DefaultConfig`. Primitive variants render generic scalar
55/// widgets and carry their own `key` inline; the visible label is
56/// resolved at CSS time via `--psp-label--<key>--content`.
57#[derive(Clone, Debug, Deserialize, Serialize)]
58#[serde(tag = "kind")]
59pub enum ControlSpec {
60 Enum {
61 key: String,
62 variants: Vec<EnumVariant>,
63 default: String,
64 },
65 Bool {
66 key: String,
67 default: bool,
68 },
69 Number {
70 key: String,
71 default: f64,
72
73 /// If `true`, always serialize this values even if it is the default.
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 include: Option<bool>,
76
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 min: Option<f64>,
79
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 max: Option<f64>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 step: Option<f64>,
85 },
86 String {
87 key: String,
88 default: String,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 placeholder: Option<String>,
91 },
92 Color {
93 key: String,
94 default: String,
95 },
96 /// Paired pos/neg color picker rendered as a single horizontal
97 /// gradient/range bar. Used to expose the existing
98 /// [`crate::components::form::color_range_selector::ColorRangeSelector`]
99 /// widget at primitive granularity. Owns two top-level keys
100 /// (`key_pos` + `key_neg`); the visible label is derived from
101 /// `key_pos`.
102 ColorRange {
103 key_pos: String,
104 key_neg: String,
105 default_pos: String,
106 default_neg: String,
107 /// When `true`, the bar renders as a continuous gradient
108 /// (e.g. for `gradient` color modes); when `false`, the bar
109 /// renders as a hard pos/neg split. Currently only changes the
110 /// visual; pos/neg semantics are unchanged.
111 #[serde(default)]
112 is_gradient: bool,
113 },
114
115 /// Residual format-only widget for datetime columns — owns
116 /// `date_format` only. Pair with primitive `Enum` and `Color` fields
117 /// for `datetime_color_mode` + `color` to fully decompose datetime
118 /// styling.
119 DatetimeFormat,
120 /// Residual format-only widget for string columns — owns `format`
121 /// only. Pair with primitive `Enum` and `Color` fields for
122 /// `string_color_mode` + `color` to fully decompose string styling.
123 StringFormat,
124 NumberSeriesStyle {
125 default: NumberSeriesStyleDefaultConfig,
126 },
127 Symbols {
128 default: KeyValueOpts,
129 },
130 NumberFormat,
131 AggregateDepth,
132}
133
134#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
135pub struct EnumVariant {
136 pub value: String,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub label: Option<String>,
139}
140
141impl ControlSpec {
142 /// Top-level JSON keys this control owns when its value is serialized
143 /// into a column's config map. For primitives this is just `[key]`;
144 /// for composites it's the set of fields the wrapped sub-struct
145 /// flattens. Used by [`ColumnConfigSchema::active_keys`] to filter the
146 /// `columns_config` blob passed to `plugin.restore()`.
147 pub fn serialized_keys(&self) -> Vec<&str> {
148 match self {
149 ControlSpec::DatetimeFormat => vec!["date_format"],
150 ControlSpec::StringFormat => vec!["format"],
151 ControlSpec::NumberSeriesStyle { .. } => vec!["chart_type", "stack"],
152 ControlSpec::Symbols { .. } => vec!["symbols"],
153 ControlSpec::NumberFormat => vec!["number_format"],
154 ControlSpec::AggregateDepth => vec!["aggregate_depth"],
155 ControlSpec::ColorRange {
156 key_pos, key_neg, ..
157 } => vec![key_pos.as_str(), key_neg.as_str()],
158 ControlSpec::Enum { key, .. }
159 | ControlSpec::Bool { key, .. }
160 | ControlSpec::Number { key, .. }
161 | ControlSpec::String { key, .. }
162 | ControlSpec::Color { key, .. } => vec![key.as_str()],
163 }
164 }
165}
166
167/// One UI-emitted change to a single schema field. The emitting widget
168/// declares which top-level keys the update is allowed to write
169/// (`keys` — equivalent to the field's [`ControlSpec::serialized_keys`])
170/// and a partial new sub-state (`value`).
171///
172/// Apply semantics: keys in `keys` are *cleared* from the column's config
173/// map, then keys present in `value` are *inserted*. Defaults are
174/// pre-stripped by the caller (typically via `skip_serializing_if`), so
175/// "no value set for key K" means the schema default applies and K is
176/// not serialized.
177#[derive(Clone, Debug, Deserialize, Serialize)]
178pub struct ColumnConfigFieldUpdate {
179 pub keys: Vec<String>,
180 pub value: serde_json::Map<String, Value>,
181}
182
183/// Filter a per-column config map to only the keys advertised by the
184/// active plugin's schema. Foreign keys (left over from a previous plugin)
185/// stay in the unfiltered presentation state but never reach `restore()`.
186pub fn filter_to_schema(
187 config: &serde_json::Map<String, Value>,
188 active_keys: &HashSet<String>,
189) -> serde_json::Map<String, Value> {
190 config
191 .iter()
192 .filter(|(k, _)| active_keys.contains(k.as_str()))
193 .map(|(k, v)| (k.clone(), v.clone()))
194 .collect()
195}