Skip to main content

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