perspective_client/config/
view_config.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::HashMap;
14
15use serde::{Deserialize, Serialize};
16use ts_rs::TS;
17
18use super::aggregates::*;
19use super::expressions::*;
20use super::filters::*;
21use super::sort::*;
22use crate::proto;
23use crate::proto::columns_update;
24
25#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)]
26#[serde(deny_unknown_fields)]
27pub struct ViewConfig {
28    #[serde(default)]
29    pub group_by: Vec<String>,
30
31    #[serde(default)]
32    pub split_by: Vec<String>,
33
34    #[serde(default)]
35    pub sort: Vec<Sort>,
36
37    #[serde(default)]
38    pub filter: Vec<Filter>,
39
40    #[serde(skip_serializing_if = "is_default_value")]
41    #[serde(default)]
42    pub filter_op: FilterReducer,
43
44    #[serde(default)]
45    pub expressions: Expressions,
46
47    #[serde(default)]
48    pub columns: Vec<Option<String>>,
49
50    #[serde(default)]
51    pub aggregates: HashMap<String, Aggregate>,
52
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[serde(default)]
55    pub group_by_depth: Option<u32>,
56}
57
58fn is_default_value<A: Default + PartialEq>(value: &A) -> bool {
59    value == &A::default()
60}
61
62#[derive(Clone, Debug, Deserialize, Default, Serialize, TS)]
63#[serde(deny_unknown_fields)]
64pub struct ViewConfigUpdate {
65    /// A group by _groups_ the dataset by the unique values of each column used
66    /// as a group by - a close analogue in SQL to the `GROUP BY` statement.
67    /// The underlying dataset is aggregated to show the values belonging to
68    /// each group, and a total row is calculated for each group, showing
69    /// the currently selected aggregated value (e.g. `sum`) of the column.
70    /// Group by are useful for hierarchies, categorizing data and
71    /// attributing values, i.e. showing the number of units sold based on
72    /// State and City. In Perspective, group by are represented as an array
73    /// of string column names to pivot, are applied in the order provided;
74    /// For example, a group by of `["State", "City", "Postal Code"]` shows
75    /// the values for each Postal Code, which are grouped by City,
76    /// which are in turn grouped by State.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    #[serde(default)]
79    #[ts(optional)]
80    pub group_by: Option<Vec<String>>,
81
82    /// A split by _splits_ the dataset by the unique values of each column used
83    /// as a split by. The underlying dataset is not aggregated, and a new
84    /// column is created for each unique value of the split by. Each newly
85    /// created column contains the parts of the dataset that correspond to
86    /// the column header, i.e. a `View` that has `["State"]` as its split
87    /// by will have a new column for each state. In Perspective, Split By
88    /// are represented as an array of string column names to pivot.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    #[serde(default)]
91    #[ts(optional)]
92    pub split_by: Option<Vec<String>>,
93
94    /// The `columns` property specifies which columns should be included in the
95    /// [`crate::View`]'s output. This allows users to show or hide a specific
96    /// subset of columns, as well as control the order in which columns
97    /// appear to the user. This is represented in Perspective as an array
98    /// of string column names.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    #[serde(default)]
101    #[ts(optional)]
102    pub columns: Option<Vec<Option<String>>>,
103
104    /// The `filter` property specifies columns on which the query can be
105    /// filtered, returning rows that pass the specified filter condition.
106    /// This is analogous to the `WHERE` clause in SQL. There is no limit on
107    /// the number of columns where `filter` is applied, but the resulting
108    /// dataset is one that passes all the filter conditions, i.e. the
109    /// filters are joined with an `AND` condition.
110    ///
111    /// Perspective represents `filter` as an array of arrays, with the values
112    /// of each inner array being a string column name, a string filter
113    /// operator, and a filter operand in the type of the column.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    #[serde(default)]
116    #[ts(optional)]
117    pub filter: Option<Vec<Filter>>,
118
119    /// The `sort` property specifies columns on which the query should be
120    /// sorted, analogous to `ORDER BY` in SQL. A column can be sorted
121    /// regardless of its data type, and sorts can be applied in ascending
122    /// or descending order. Perspective represents `sort` as an array of
123    /// arrays, with the values of each inner array being a string column
124    /// name and a string sort direction. When `column-pivots` are applied,
125    /// the additional sort directions `"col asc"` and `"col desc"` will
126    /// determine the order of pivot columns groups.
127    #[serde(skip_serializing_if = "Option::is_none")]
128    #[serde(default)]
129    #[ts(optional)]
130    pub sort: Option<Vec<Sort>>,
131
132    /// The `expressions` property specifies _new_ columns in Perspective that
133    /// are created using existing column values or arbitary scalar values
134    /// defined within the expression. In `<perspective-viewer>`,
135    /// expressions are added using the "New Column" button in the side
136    /// panel.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    #[serde(default)]
139    #[ts(optional)]
140    pub expressions: Option<Expressions>,
141
142    /// Aggregates perform a calculation over an entire column, and are
143    /// displayed when one or more [Group By](#group-by) are applied to the
144    /// `View`. Aggregates can be specified by the user, or Perspective will
145    /// use the following sensible default aggregates based on column type:
146    ///
147    /// - "sum" for `integer` and `float` columns
148    /// - "count" for all other columns
149    ///
150    /// Perspective provides a selection of aggregate functions that can be
151    /// applied to columns in the `View` constructor using a dictionary of
152    /// column name to aggregate function name.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    #[serde(default)]
155    #[ts(optional)]
156    pub aggregates: Option<HashMap<String, Aggregate>>,
157
158    #[serde(skip_serializing)]
159    #[serde(default)]
160    #[ts(optional)]
161    pub group_by_depth: Option<u32>,
162
163    #[serde(skip_serializing_if = "Option::is_none")]
164    #[serde(default)]
165    #[ts(optional)]
166    pub filter_op: Option<FilterReducer>,
167}
168
169impl From<ViewConfigUpdate> for proto::ViewConfig {
170    fn from(value: ViewConfigUpdate) -> Self {
171        proto::ViewConfig {
172            group_by: value.group_by.unwrap_or_default(),
173            split_by: value.split_by.unwrap_or_default(),
174            columns: value.columns.map(|x| proto::ColumnsUpdate {
175                opt_columns: Some(columns_update::OptColumns::Columns(
176                    proto::columns_update::Columns {
177                        columns: x.into_iter().flatten().collect(),
178                    },
179                )),
180            }),
181            filter: value
182                .filter
183                .unwrap_or_default()
184                .into_iter()
185                .map(|x| x.into())
186                .collect(),
187            filter_op: value
188                .filter_op
189                .map(proto::view_config::FilterReducer::from)
190                .unwrap_or_default() as i32,
191            sort: value
192                .sort
193                .unwrap_or_default()
194                .into_iter()
195                .map(|x| x.into())
196                .collect(),
197            expressions: value.expressions.unwrap_or_default().0,
198            aggregates: value
199                .aggregates
200                .unwrap_or_default()
201                .into_iter()
202                .map(|(x, y)| (x, y.into()))
203                .collect(),
204            group_by_depth: value.group_by_depth,
205        }
206    }
207}
208
209impl From<FilterReducer> for proto::view_config::FilterReducer {
210    fn from(value: FilterReducer) -> Self {
211        match value {
212            FilterReducer::And => proto::view_config::FilterReducer::And,
213            FilterReducer::Or => proto::view_config::FilterReducer::Or,
214        }
215    }
216}
217
218impl From<proto::view_config::FilterReducer> for FilterReducer {
219    fn from(value: proto::view_config::FilterReducer) -> Self {
220        match value {
221            proto::view_config::FilterReducer::And => FilterReducer::And,
222            proto::view_config::FilterReducer::Or => FilterReducer::Or,
223        }
224    }
225}
226
227impl From<ViewConfig> for ViewConfigUpdate {
228    fn from(value: ViewConfig) -> Self {
229        ViewConfigUpdate {
230            group_by: Some(value.group_by),
231            split_by: Some(value.split_by),
232            columns: Some(value.columns),
233            filter: Some(value.filter),
234            filter_op: Some(value.filter_op),
235            sort: Some(value.sort),
236            expressions: Some(value.expressions),
237            aggregates: Some(value.aggregates),
238            group_by_depth: value.group_by_depth,
239        }
240    }
241}
242
243impl From<proto::ViewConfig> for ViewConfig {
244    fn from(value: proto::ViewConfig) -> Self {
245        ViewConfig {
246            group_by: value.group_by,
247            split_by: value.split_by,
248            columns: match value.columns.unwrap_or_default().opt_columns {
249                Some(columns_update::OptColumns::Columns(x)) => {
250                    x.columns.into_iter().map(Some).collect()
251                },
252                _ => {
253                    vec![]
254                },
255            },
256            filter: value.filter.into_iter().map(|x| x.into()).collect(),
257            filter_op: proto::view_config::FilterReducer::try_from(value.filter_op)
258                .unwrap_or_default()
259                .into(),
260            sort: value.sort.into_iter().map(|x| x.into()).collect(),
261            expressions: Expressions(value.expressions),
262            aggregates: value
263                .aggregates
264                .into_iter()
265                .map(|(x, y)| (x, y.into()))
266                .collect(),
267            group_by_depth: value.group_by_depth,
268        }
269    }
270}
271
272impl From<ViewConfigUpdate> for ViewConfig {
273    fn from(value: ViewConfigUpdate) -> Self {
274        ViewConfig {
275            group_by: value.group_by.unwrap_or_default(),
276            split_by: value.split_by.unwrap_or_default(),
277            columns: value.columns.unwrap_or_default(),
278            filter: value.filter.unwrap_or_default(),
279            filter_op: value.filter_op.unwrap_or_default(),
280            sort: value.sort.unwrap_or_default(),
281            expressions: value.expressions.unwrap_or_default(),
282            aggregates: value.aggregates.unwrap_or_default(),
283            group_by_depth: value.group_by_depth,
284        }
285    }
286}
287
288impl From<proto::ViewConfig> for ViewConfigUpdate {
289    fn from(value: proto::ViewConfig) -> Self {
290        ViewConfigUpdate {
291            group_by: Some(value.group_by),
292            split_by: Some(value.split_by),
293            columns: match value.columns.unwrap_or_default().opt_columns {
294                Some(columns_update::OptColumns::Columns(x)) => {
295                    Some(x.columns.into_iter().map(Some).collect())
296                },
297                _ => None,
298            },
299            filter: Some(value.filter.into_iter().map(|x| x.into()).collect()),
300            filter_op: Some(
301                proto::view_config::FilterReducer::try_from(value.filter_op)
302                    .unwrap_or_default()
303                    .into(),
304            ),
305            sort: Some(value.sort.into_iter().map(|x| x.into()).collect()),
306            expressions: Some(Expressions(value.expressions)),
307            aggregates: Some(
308                value
309                    .aggregates
310                    .into_iter()
311                    .map(|(x, y)| (x, y.into()))
312                    .collect(),
313            ),
314            group_by_depth: value.group_by_depth,
315        }
316    }
317}
318
319impl ViewConfig {
320    fn _apply<T>(field: &mut T, update: Option<T>) -> bool {
321        match update {
322            None => false,
323            Some(update) => {
324                *field = update;
325                true
326            },
327        }
328    }
329
330    pub fn reset(&mut self, reset_expressions: bool) {
331        let mut config = Self::default();
332        if !reset_expressions {
333            config.expressions = self.expressions.clone();
334        }
335        std::mem::swap(self, &mut config);
336    }
337
338    /// Apply `ViewConfigUpdate` to a `ViewConfig`, ignoring any fields in
339    /// `update` which were unset.
340    pub fn apply_update(&mut self, update: ViewConfigUpdate) -> bool {
341        let mut changed = false;
342        changed = Self::_apply(&mut self.group_by, update.group_by) || changed;
343        changed = Self::_apply(&mut self.split_by, update.split_by) || changed;
344        changed = Self::_apply(&mut self.columns, update.columns) || changed;
345        changed = Self::_apply(&mut self.filter, update.filter) || changed;
346        changed = Self::_apply(&mut self.sort, update.sort) || changed;
347        changed = Self::_apply(&mut self.aggregates, update.aggregates) || changed;
348        changed = Self::_apply(&mut self.expressions, update.expressions) || changed;
349        changed
350    }
351
352    pub fn is_aggregated(&self) -> bool {
353        !self.group_by.is_empty()
354    }
355
356    pub fn is_column_expression_in_use(&self, name: &str) -> bool {
357        let name = name.to_owned();
358        self.group_by.contains(&name)
359            || self.split_by.contains(&name)
360            || self.sort.iter().any(|x| x.0 == name)
361            || self.filter.iter().any(|x| x.column() == name)
362            || self.columns.contains(&Some(name))
363    }
364
365    /// `ViewConfig` carries additional metadata in the form of `None` columns
366    /// which are filtered befor ebeing passed to the engine, but whose position
367    /// is a placeholder for Viewer functionality. `is_equivalent` tests
368    /// equivalency from the perspective of the engine.
369    pub fn is_equivalent(&self, other: &Self) -> bool {
370        let _self = self.clone();
371        let _self = ViewConfig {
372            columns: _self.columns.into_iter().filter(|x| x.is_some()).collect(),
373            .._self
374        };
375
376        let _other = other.clone();
377        let _other = ViewConfig {
378            columns: _other.columns.into_iter().filter(|x| x.is_some()).collect(),
379            ..other.clone()
380        };
381
382        _self == _other
383    }
384}