Skip to main content

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}