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