vegafusion_core/planning/
plan.rs

1use crate::error::Result;
2use crate::planning::extract::extract_server_data;
3use crate::planning::fuse::fuse_datasets;
4use crate::planning::lift_facet_aggregations::lift_facet_aggregations;
5use crate::planning::optimize_server::split_data_url_nodes;
6use crate::planning::projection_pushdown::projection_pushdown;
7use crate::planning::split_domain_data::split_domain_data;
8use crate::planning::stitch::{stitch_specs, CommPlan};
9use crate::planning::stringify_local_datetimes::stringify_local_datetimes;
10use crate::planning::strip_encodings::strip_encodings;
11use crate::planning::unsupported_data_warning::add_unsupported_data_warnings;
12use crate::proto::gen::pretransform::{
13    pre_transform_spec_warning::WarningType, PlannerWarning, PreTransformSpecWarning,
14};
15use crate::spec::chart::ChartSpec;
16use crate::task_graph::graph::ScopedVariable;
17use serde::{Deserialize, Serialize};
18
19#[derive(Clone, Serialize, Deserialize)]
20pub struct PreTransformSpecWarningSpec {
21    #[serde(rename = "type")]
22    pub typ: String,
23    pub message: String,
24}
25
26impl From<&PreTransformSpecWarning> for PreTransformSpecWarningSpec {
27    fn from(warning: &PreTransformSpecWarning) -> Self {
28        match warning.warning_type.as_ref().unwrap() {
29            WarningType::RowLimit(_) => {
30                PreTransformSpecWarningSpec {
31                    typ: "RowLimitExceeded".to_string(),
32                    message: "Some datasets in resulting Vega specification have been truncated to the provided row limit".to_string()
33                }
34            }
35            WarningType::BrokenInteractivity(_) => {
36                PreTransformSpecWarningSpec {
37                    typ: "BrokenInteractivity".to_string(),
38                    message: "Some interactive features may have been broken in the resulting Vega specification".to_string()
39                }
40            }
41            WarningType::Unsupported(_) => {
42                PreTransformSpecWarningSpec {
43                    typ: "Unsupported".to_string(),
44                    message: "Unable to pre-transform any datasets in the Vega specification".to_string()
45                }
46            }
47            WarningType::Planner(warning) => {
48                PreTransformSpecWarningSpec {
49                    typ: "Planner".to_string(),
50                    message: warning.message.clone()
51                }
52            }
53        }
54    }
55}
56
57impl From<&PlannerWarning> for PreTransformSpecWarning {
58    fn from(value: &PlannerWarning) -> Self {
59        PreTransformSpecWarning {
60            warning_type: Some(WarningType::Planner(PlannerWarning {
61                message: value.message.clone(),
62            })),
63        }
64    }
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68pub enum PlannerWarnings {
69    StringifyDatetimeMixedUsage(String),
70    UnsupportedTransforms(String),
71}
72
73impl PlannerWarnings {
74    pub fn message(&self) -> String {
75        match &self {
76            PlannerWarnings::StringifyDatetimeMixedUsage(message) => message.clone(),
77            PlannerWarnings::UnsupportedTransforms(message) => message.clone(),
78        }
79    }
80}
81
82#[derive(Debug, Clone)]
83pub struct PlannerConfig {
84    pub split_domain_data: bool,
85    pub split_url_data_nodes: bool,
86    pub stringify_local_datetimes: bool,
87    pub projection_pushdown: bool,
88    pub extract_inline_data: bool,
89    pub extract_server_data: bool,
90    pub allow_client_to_server_comms: bool,
91    pub fuse_datasets: bool,
92    pub lift_facet_aggregations: bool,
93    pub client_only_vars: Vec<ScopedVariable>,
94    pub keep_variables: Vec<ScopedVariable>,
95    pub strip_description_encoding: bool,
96    pub strip_aria_encoding: bool,
97    pub strip_tooltip_encoding: bool,
98}
99
100impl Default for PlannerConfig {
101    fn default() -> Self {
102        Self {
103            split_domain_data: true,
104            split_url_data_nodes: true,
105            stringify_local_datetimes: false,
106            projection_pushdown: true,
107            extract_inline_data: false,
108            extract_server_data: true,
109            allow_client_to_server_comms: true,
110            fuse_datasets: true,
111            lift_facet_aggregations: true,
112            client_only_vars: Default::default(),
113            keep_variables: Default::default(),
114            strip_description_encoding: true,
115            strip_aria_encoding: true,
116            strip_tooltip_encoding: false,
117        }
118    }
119}
120
121impl PlannerConfig {
122    pub fn pre_transformed_spec_config(
123        preserve_interactivity: bool,
124        keep_variables: Vec<ScopedVariable>,
125    ) -> PlannerConfig {
126        PlannerConfig {
127            stringify_local_datetimes: true,
128            extract_inline_data: true,
129            allow_client_to_server_comms: !preserve_interactivity,
130            keep_variables,
131            ..Default::default()
132        }
133    }
134}
135
136#[derive(Debug, Clone)]
137pub struct SpecPlan {
138    pub server_spec: ChartSpec,
139    pub client_spec: ChartSpec,
140    pub comm_plan: CommPlan,
141    pub warnings: Vec<PlannerWarnings>,
142}
143
144impl SpecPlan {
145    pub fn try_new(full_spec: &ChartSpec, config: &PlannerConfig) -> Result<Self> {
146        let mut warnings: Vec<PlannerWarnings> = Vec::new();
147
148        // Collect warnings associated with unsupported datasets
149        add_unsupported_data_warnings(full_spec, config, &mut warnings)?;
150
151        let mut client_spec = full_spec.clone();
152
153        // Push domain data calculations to the server
154        let domain_dataset_fields = if config.split_domain_data {
155            split_domain_data(&mut client_spec)?
156        } else {
157            Default::default()
158        };
159
160        // Lift aggregation transforms out of facets so they can be evaluated on the server
161        if config.lift_facet_aggregations {
162            lift_facet_aggregations(&mut client_spec, config)?;
163        }
164
165        // Remove unused encodings
166        strip_encodings(&mut client_spec, config)?;
167
168        // Attempt to limit the columns produced by each dataset to only include those
169        // that are actually used downstream
170        if config.projection_pushdown {
171            projection_pushdown(&mut client_spec)?;
172        }
173
174        if !config.extract_server_data {
175            // Return empty server spec and empty comm plan
176            Ok(Self {
177                server_spec: Default::default(),
178                client_spec,
179                comm_plan: Default::default(),
180                warnings,
181            })
182        } else {
183            let mut task_scope = client_spec.to_task_scope()?;
184            let input_client_spec = client_spec.clone();
185            let mut server_spec = extract_server_data(&mut client_spec, &mut task_scope, config)?;
186            let mut comm_plan = stitch_specs(
187                &task_scope,
188                &mut server_spec,
189                &mut client_spec,
190                config.keep_variables.as_slice(),
191            )?;
192
193            if !config.allow_client_to_server_comms && !comm_plan.client_to_server.is_empty() {
194                // Client to server comms are not allowed and the initial planning
195                // pass included them. re-plan without
196                let mut config = config.clone();
197                config
198                    .client_only_vars
199                    .extend(comm_plan.client_to_server.clone());
200
201                client_spec = input_client_spec;
202                server_spec = extract_server_data(&mut client_spec, &mut task_scope, &config)?;
203                comm_plan = stitch_specs(
204                    &task_scope,
205                    &mut server_spec,
206                    &mut client_spec,
207                    config.keep_variables.as_slice(),
208                )?;
209            }
210
211            if config.fuse_datasets {
212                let mut do_not_fuse = config.keep_variables.clone();
213                do_not_fuse.extend(comm_plan.server_to_client.clone());
214                fuse_datasets(&mut server_spec, do_not_fuse.as_slice())?;
215            }
216
217            if config.split_url_data_nodes {
218                split_data_url_nodes(&mut server_spec)?;
219            }
220
221            if config.stringify_local_datetimes {
222                stringify_local_datetimes(
223                    &mut server_spec,
224                    &mut client_spec,
225                    &comm_plan,
226                    &domain_dataset_fields,
227                )?;
228            }
229
230            Ok(Self {
231                server_spec,
232                client_spec,
233                comm_plan,
234                warnings,
235            })
236        }
237    }
238}