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