vegafusion_core/spec/transform/
timeunit.rs

1use crate::expression::column_usage::{ColumnUsage, DatasetsColumnUsage, VlSelectionFields};
2use crate::spec::transform::{TransformColumns, TransformSpecTrait};
3use crate::task_graph::graph::ScopedVariable;
4use crate::task_graph::scope::TaskScope;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8use vegafusion_common::escape::unescape_field;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct TimeUnitTransformSpec {
12    pub field: String, // TODO: support field object
13
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub units: Option<Vec<TimeUnitUnitSpec>>,
16
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub step: Option<f64>,
19
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub timezone: Option<TimeUnitTimeZoneSpec>,
22
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub interval: Option<bool>,
25
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub extent: Option<(String, String)>,
28
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub maxbins: Option<f64>,
31
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub signal: Option<String>,
34
35    #[serde(rename = "as", skip_serializing_if = "Option::is_none")]
36    pub as_: Option<Vec<String>>,
37
38    #[serde(flatten)]
39    pub extra: HashMap<String, Value>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum TimeUnitTimeZoneSpec {
45    Local,
46    Utc,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum TimeUnitUnitSpec {
52    Year,
53    Quarter,
54    Month,
55    Date,
56    Week,
57    Day,
58    DayOfYear,
59    Hours,
60    Minutes,
61    Seconds,
62    Milliseconds,
63}
64
65impl TimeUnitTransformSpec {
66    pub fn normalize_as(&self) -> (String, String) {
67        let as0 = self
68            .as_
69            .clone()
70            .and_then(|as_| as_.first().cloned())
71            .unwrap_or_else(|| "unit0".to_string());
72        let as1 = self
73            .as_
74            .clone()
75            .and_then(|as_| as_.get(1).cloned())
76            .unwrap_or_else(|| "unit1".to_string());
77        (as0, as1)
78    }
79}
80
81impl TransformSpecTrait for TimeUnitTransformSpec {
82    fn supported(&self) -> bool {
83        let unsupported = self.units.is_none()
84            || self.step.is_some()
85            || self.extent.is_some()
86            || self.maxbins.is_some()
87            || self.signal.is_some();
88        !unsupported
89    }
90
91    fn output_signals(&self) -> Vec<String> {
92        self.signal.clone().into_iter().collect()
93    }
94
95    fn transform_columns(
96        &self,
97        datum_var: &Option<ScopedVariable>,
98        _usage_scope: &[u32],
99        _task_scope: &TaskScope,
100        _vl_selection_fields: &VlSelectionFields,
101    ) -> TransformColumns {
102        if let Some(datum_var) = datum_var {
103            // Compute produced columns
104            let (bin_start, bin_end) = self.normalize_as();
105            let mut produced_cols = vec![bin_start];
106
107            if self.interval.unwrap_or(true) {
108                produced_cols.push(bin_end)
109            }
110
111            let produced = ColumnUsage::from(produced_cols.as_slice());
112
113            // Compute used columns
114            let col_usage = ColumnUsage::empty().with_column(&unescape_field(&self.field));
115            let usage = DatasetsColumnUsage::empty().with_column_usage(datum_var, col_usage);
116
117            TransformColumns::PassThrough { usage, produced }
118        } else {
119            TransformColumns::Unknown
120        }
121    }
122
123    fn local_datetime_columns_produced(
124        &self,
125        input_local_datetime_columns: &[String],
126    ) -> Vec<String> {
127        // Keep input local datetime columns as timeunit passes through all input columns
128        let mut output_local_datetime_columns = Vec::from(input_local_datetime_columns);
129
130        // Determine whether timeunit will create local datetime columns
131        if matches!(self.timezone, None | Some(TimeUnitTimeZoneSpec::Local)) {
132            let (bin_start, bin_end) = self.normalize_as();
133            output_local_datetime_columns.push(bin_start);
134            output_local_datetime_columns.push(bin_end);
135        }
136
137        output_local_datetime_columns
138    }
139}