vegafusion_core/spec/transform/
timeunit.rs1use 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, #[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 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 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 let mut output_local_datetime_columns = Vec::from(input_local_datetime_columns);
129
130 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}