From 86ce9cafb961d34b638394a1f4adf5dfc20235bd Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 2 Nov 2023 12:16:31 -0400 Subject: [PATCH] Lift aggregate transforms out of facets --- vegafusion-core/src/patch.rs | 2 +- .../src/planning/dependency_graph.rs | 118 +++++- vegafusion-core/src/planning/fuse.rs | 2 +- .../src/planning/lift_facet_aggregations.rs | 391 ++++++++++++++++++ vegafusion-core/src/planning/mod.rs | 1 + vegafusion-core/src/planning/plan.rs | 8 + .../src/planning/projection_pushdown.rs | 2 +- .../src/planning/split_domain_data.rs | 2 +- vegafusion-core/src/spec/mark.rs | 24 +- vegafusion-core/src/spec/values.rs | 6 + ...grouped_bar_with_error_bars.comm_plan.json | 40 ++ .../facet_grouped_bar_with_error_bars.vg.json | 306 ++++++++++++++ ...r_with_error_bars_with_sort.comm_plan.json | 40 ++ ...uped_bar_with_error_bars_with_sort.vg.json | 324 +++++++++++++++ .../tests/specs/custom/gh_391.comm_plan.json | 2 +- ...ependent_scale_layer_broken.comm_plan.json | 2 +- .../vegalite/trellis_barley.comm_plan.json | 2 +- .../trellis_barley_independent.comm_plan.json | 2 +- .../tests/test_image_comparison.rs | 4 +- 19 files changed, 1250 insertions(+), 28 deletions(-) create mode 100644 vegafusion-core/src/planning/lift_facet_aggregations.rs create mode 100644 vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.comm_plan.json create mode 100644 vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.vg.json create mode 100644 vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.comm_plan.json create mode 100644 vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.vg.json diff --git a/vegafusion-core/src/patch.rs b/vegafusion-core/src/patch.rs index 7b5df2443..552e0c607 100644 --- a/vegafusion-core/src/patch.rs +++ b/vegafusion-core/src/patch.rs @@ -99,7 +99,7 @@ fn arrayify_int_key_objects(obj: &Value) -> Value { } } Value::Array(arr) => { - Value::Array(arr.iter().map(|v| arrayify_int_key_objects(v)).collect()) + Value::Array(arr.iter().map(arrayify_int_key_objects).collect()) } _ => obj.clone(), } diff --git a/vegafusion-core/src/planning/dependency_graph.rs b/vegafusion-core/src/planning/dependency_graph.rs index 9005544f1..87a085453 100644 --- a/vegafusion-core/src/planning/dependency_graph.rs +++ b/vegafusion-core/src/planning/dependency_graph.rs @@ -16,9 +16,9 @@ use petgraph::prelude::{DiGraph, EdgeRef, NodeIndex}; use petgraph::Incoming; use std::collections::{HashMap, HashSet}; -pub fn toposort_dependency_graph( - data_graph: &DiGraph<(ScopedVariable, DependencyNodeSupported), ()>, -) -> Result> { +pub type DependencyGraph = DiGraph<(ScopedVariable, DependencyNodeSupported), ()>; + +pub fn toposort_dependency_graph(data_graph: &DependencyGraph) -> Result> { Ok(match toposort(&data_graph, None) { Ok(v) => v, Err(err) => { @@ -35,7 +35,7 @@ pub fn get_supported_data_variables( chart_spec: &ChartSpec, config: &PlannerConfig, ) -> Result> { - let data_graph = build_dependency_graph(chart_spec, config)?; + let (data_graph, _) = build_dependency_graph(chart_spec, config)?; // Sort dataset nodes topologically let nodes: Vec = toposort_dependency_graph(&data_graph)?; @@ -129,7 +129,7 @@ pub fn get_supported_data_variables( pub fn build_dependency_graph( chart_spec: &ChartSpec, config: &PlannerConfig, -) -> Result> { +) -> Result<(DependencyGraph, HashMap)> { let task_scope = chart_spec.to_task_scope()?; // Initialize graph with nodes @@ -144,16 +144,17 @@ pub fn build_dependency_graph( ); chart_spec.walk(&mut edges_visitor)?; - Ok(nodes_visitor.dependency_graph) + Ok((nodes_visitor.dependency_graph, nodes_visitor.node_indexes)) } /// Visitor to initialize directed graph with nodes for each dataset (no edges yet) #[derive(Debug)] pub struct AddDependencyNodesVisitor<'a> { - pub dependency_graph: DiGraph<(ScopedVariable, DependencyNodeSupported), ()>, + pub dependency_graph: DependencyGraph, pub node_indexes: HashMap, planner_config: &'a PlannerConfig, task_scope: &'a TaskScope, + mark_index: u32, } impl<'a> AddDependencyNodesVisitor<'a> { @@ -174,6 +175,7 @@ impl<'a> AddDependencyNodesVisitor<'a> { node_indexes, planner_config, task_scope, + mark_index: 0, } } } @@ -223,14 +225,19 @@ impl<'a> ChartVisitor for AddDependencyNodesVisitor<'a> { } fn visit_non_group_mark(&mut self, mark: &MarkSpec, scope: &[u32]) -> Result<()> { - // Named non-group marks can serve as datasets - if let Some(name) = &mark.name { - let scoped_var = (Variable::new_data(name), Vec::from(scope)); - let node_index = self - .dependency_graph - .add_node((scoped_var.clone(), DependencyNodeSupported::Unsupported)); - self.node_indexes.insert(scoped_var, node_index); - } + // non-group marks can serve as datasets + let name = mark + .name + .clone() + .unwrap_or_else(|| format!("unnamed_mark_{}", self.mark_index)); + self.mark_index += 1; + + let scoped_var = (Variable::new_data(&name), Vec::from(scope)); + let node_index = self + .dependency_graph + .add_node((scoped_var.clone(), DependencyNodeSupported::Unsupported)); + self.node_indexes.insert(scoped_var, node_index); + Ok(()) } @@ -247,7 +254,7 @@ impl<'a> ChartVisitor for AddDependencyNodesVisitor<'a> { // Named group marks can serve as datasets if let Some(name) = &mark.name { - let parent_scope = Vec::from(&scope[..scope.len()]); + let parent_scope = Vec::from(&scope[..(scope.len() - 1)]); let scoped_var = (Variable::new_data(name), parent_scope); let node_index = self .dependency_graph @@ -262,14 +269,15 @@ impl<'a> ChartVisitor for AddDependencyNodesVisitor<'a> { /// Visitor to add directed edges to graph with data nodes #[derive(Debug)] pub struct AddDependencyEdgesVisitor<'a> { - pub dependency_graph: &'a mut DiGraph<(ScopedVariable, DependencyNodeSupported), ()>, + pub dependency_graph: &'a mut DependencyGraph, node_indexes: &'a HashMap, task_scope: &'a TaskScope, + mark_index: u32, } impl<'a> AddDependencyEdgesVisitor<'a> { pub fn new( - dependency_graph: &'a mut DiGraph<(ScopedVariable, DependencyNodeSupported), ()>, + dependency_graph: &'a mut DependencyGraph, node_indexes: &'a HashMap, task_scope: &'a TaskScope, ) -> Self { @@ -277,6 +285,7 @@ impl<'a> AddDependencyEdgesVisitor<'a> { dependency_graph, node_indexes, task_scope, + mark_index: 0, } } } @@ -338,6 +347,79 @@ impl<'a> ChartVisitor for AddDependencyEdgesVisitor<'a> { Ok(()) } + fn visit_group_mark(&mut self, mark: &MarkSpec, scope: &[u32]) -> Result<()> { + // Facet datasets have parents + if let Some(from) = &mark.from { + if let Some(facet) = &from.facet { + // Scoped var for this facet dataset + let scoped_var = (Variable::new_data(&facet.name), Vec::from(scope)); + + let node_index = self + .node_indexes + .get(&scoped_var) + .with_context(|| format!("Missing data node: {scoped_var:?}"))?; + + // Build scoped var for parent node + let source = &facet.data; + let source_var = Variable::new_data(source); + // Resolve scope up one level because source dataset must be defined in parent, + // not as a dataset within this group mark + let resolved = self + .task_scope + .resolve_scope(&source_var, &scope[..(scope.len() - 1)])?; + let scoped_source_var = (resolved.var, resolved.scope); + + let source_node_index = self + .node_indexes + .get(&scoped_source_var) + .with_context(|| format!("Missing data node: {scoped_source_var:?}"))?; + + // Add directed edge + self.dependency_graph + .add_edge(*source_node_index, *node_index, ()); + } + } + + Ok(()) + } + + fn visit_non_group_mark(&mut self, mark: &MarkSpec, scope: &[u32]) -> Result<()> { + // non-group marks can serve as datasets + let name = mark + .name + .clone() + .unwrap_or_else(|| format!("unnamed_mark_{}", self.mark_index)); + self.mark_index += 1; + + let Some(from) = &mark.from else { return Ok(()) }; + let Some(source) = &from.data else { return Ok(()) }; + + // Scoped var for this facet dataset + let scoped_var = (Variable::new_data(&name), Vec::from(scope)); + + let node_index = self + .node_indexes + .get(&scoped_var) + .with_context(|| format!("Missing data node: {scoped_var:?}"))?; + + let source_var = Variable::new_data(source); + // Resolve scope up one level because source dataset must be defined in parent, + // not as a dataset within this group mark + let resolved = self.task_scope.resolve_scope(&source_var, scope)?; + let scoped_source_var = (resolved.var, resolved.scope); + + let source_node_index = self + .node_indexes + .get(&scoped_source_var) + .with_context(|| format!("Missing data node: {scoped_source_var:?}"))?; + + // Add directed edge + self.dependency_graph + .add_edge(*source_node_index, *node_index, ()); + + Ok(()) + } + /// Add edges into a signal node fn visit_signal(&mut self, signal: &SignalSpec, scope: &[u32]) -> Result<()> { // Scoped var for this node diff --git a/vegafusion-core/src/planning/fuse.rs b/vegafusion-core/src/planning/fuse.rs index 226ee3222..3ffed9d6f 100644 --- a/vegafusion-core/src/planning/fuse.rs +++ b/vegafusion-core/src/planning/fuse.rs @@ -12,7 +12,7 @@ use vegafusion_common::error::Result; /// transforms up through at least the first aggregation into the root dataset's transform /// pipeline. pub fn fuse_datasets(server_spec: &mut ChartSpec, do_not_fuse: &[ScopedVariable]) -> Result<()> { - let data_graph = build_dependency_graph(server_spec, &Default::default())?; + let (data_graph, _) = build_dependency_graph(server_spec, &Default::default())?; let nodes: Vec = toposort_dependency_graph(&data_graph)?; 'outer: for node_index in &nodes { diff --git a/vegafusion-core/src/planning/lift_facet_aggregations.rs b/vegafusion-core/src/planning/lift_facet_aggregations.rs new file mode 100644 index 000000000..42641e6a0 --- /dev/null +++ b/vegafusion-core/src/planning/lift_facet_aggregations.rs @@ -0,0 +1,391 @@ +use crate::planning::dependency_graph::{build_dependency_graph, DependencyGraph}; +use crate::planning::plan::PlannerConfig; +use crate::proto::gen::tasks::Variable; +use crate::spec::chart::{ChartSpec, MutChartVisitor}; +use crate::spec::data::DataSpec; +use crate::spec::mark::MarkSpec; +use crate::spec::transform::aggregate::AggregateOpSpec; +use crate::spec::transform::joinaggregate::JoinAggregateTransformSpec; +use crate::spec::transform::TransformSpec; +use crate::spec::values::Field; +use crate::task_graph::graph::ScopedVariable; +use crate::task_graph::scope::TaskScope; +use petgraph::prelude::NodeIndex; +use petgraph::Direction; +use std::collections::HashMap; +use vegafusion_common::error::Result; + +/// Optimization that lifts aggregation transforms inside facet definitions into top-level +/// datasets that VegaFusion can evaluate +pub fn lift_facet_aggregations(chart_spec: &mut ChartSpec, config: &PlannerConfig) -> Result<()> { + let (graph, node_indexes) = build_dependency_graph(chart_spec, config)?; + let mut visitor = + ExtractFacetAggregationsVisitor::new(chart_spec.to_task_scope()?, graph, node_indexes)?; + chart_spec.walk_mut(&mut visitor)?; + visitor.insert_lifted_datasets(chart_spec)?; + Ok(()) +} + +pub struct ExtractFacetAggregationsVisitor { + pub task_scope: TaskScope, + pub graph: DependencyGraph, + pub node_indexes: HashMap, + pub new_datasets: HashMap, Vec>, + pub counter: u32, +} + +impl ExtractFacetAggregationsVisitor { + pub fn new( + task_scope: TaskScope, + graph: DependencyGraph, + node_indexes: HashMap, + ) -> Result { + Ok(Self { + task_scope, + graph, + node_indexes, + new_datasets: Default::default(), + counter: 0, + }) + } + + /// Add lifted datasets to the provided ChartSpec. + /// This must be called after chart.walk_mut(&mut visitor) + pub fn insert_lifted_datasets(&self, chart_spec: &mut ChartSpec) -> Result<()> { + for (scope, new_datasets_for_scope) in &self.new_datasets { + let datasets = if scope.is_empty() { + &mut chart_spec.data + } else { + &mut chart_spec.get_nested_group_mut(scope.as_slice())?.data + }; + for new_dataset in new_datasets_for_scope { + datasets.push(new_dataset.clone()) + } + } + Ok(()) + } +} + +impl MutChartVisitor for ExtractFacetAggregationsVisitor { + fn visit_group_mark(&mut self, mark: &mut MarkSpec, scope: &[u32]) -> Result<()> { + let Some(from) = &mut mark.from else { return Ok(()) }; + let Some(facet) = &mut from.facet else { return Ok(()) }; + + // Check for child datasets + let facet_dataset_var: ScopedVariable = (Variable::new_data(&facet.name), Vec::from(scope)); + let Some(facet_dataset_idx) = self.node_indexes.get(&facet_dataset_var) else { return Ok(())}; + let edges = self + .graph + .edges_directed(*facet_dataset_idx, Direction::Outgoing); + let edges_vec = edges.into_iter().collect::>(); + if edges_vec.len() != 1 { + // We don't have exactly one child dataset so we cannot lift + return Ok(()); + } + + // Collect datasets that are immediate children of the facet dataset + let mut child_datasets = mark + .data + .iter_mut() + .filter(|d| d.source.as_ref() == Some(&facet.name)) + .collect::>(); + + if child_datasets.len() != 1 { + // Child dataset isn't located in this facet's dataset. + // I don't think this shouldn't happen, but bail out in case + return Ok(()); + } + + let child_dataset = &mut child_datasets[0]; + let Some(TransformSpec::Aggregate(mut agg)) = child_dataset.transform.get(0).cloned() else { + // dataset does not have a aggregate transform as the first transform, nothing to lift + return Ok(()) + }; + + // Add facet groupby fields as aggregate transform groupby fields + let facet_groupby_fields: Vec = facet + .groupby + .clone() + .unwrap_or_default() + .to_vec() + .into_iter() + .map(Field::String) + .collect(); + + agg.groupby.extend(facet_groupby_fields.clone()); + + let mut lifted_transforms: Vec = Vec::new(); + + // When the facet defines an aggregation, we need to perform it with a joinaggregate + // prior to the lifted aggregation. + // + // Leave `cross` field as-is + if let Some(facet_aggregate) = &mut facet.aggregate { + if facet_aggregate.fields.is_some() + && facet_aggregate.ops.is_some() + && facet_aggregate.as_.is_some() + { + // Add joinaggregate transform that performs the facet's aggregation using the same + // grouping columns as the facet + lifted_transforms.push(TransformSpec::JoinAggregate(JoinAggregateTransformSpec { + groupby: Some(facet_groupby_fields), + fields: facet_aggregate.fields.clone().unwrap(), + ops: facet_aggregate.ops.clone().unwrap(), + as_: facet_aggregate.as_.clone(), + extra: Default::default(), + })); + + // Add aggregations to the lifted aggregate transform that pass through the + // fields that the joinaggregate above calculates + let mut new_fields = agg.fields.clone().unwrap_or_default(); + let mut new_ops = agg.ops.clone().unwrap_or_default(); + let mut new_as = agg.as_.clone().unwrap_or_default(); + + new_fields.extend( + facet_aggregate + .as_ + .clone() + .unwrap() + .into_iter() + .map(|s| s.map(Field::String)), + ); + // Use min aggregate to pass through single unique value + new_ops.extend(facet_aggregate.ops.iter().map(|_| AggregateOpSpec::Min)); + new_as.extend(facet_aggregate.as_.clone().unwrap()); + + agg.fields = Some(new_fields); + agg.ops = Some(new_ops); + agg.as_ = Some(new_as); + + // Update facet aggregate to pass through the fields compute in joinaggregate + facet_aggregate.fields = Some( + facet_aggregate + .as_ + .clone() + .unwrap() + .into_iter() + .map(|s| s.map(Field::String)) + .collect(), + ); + facet_aggregate.ops = Some( + facet_aggregate + .ops + .iter() + .map(|_| AggregateOpSpec::Min) + .collect(), + ); + } else if facet_aggregate.fields.is_some() + || facet_aggregate.ops.is_some() + || facet_aggregate.as_.is_some() + { + // Not all of fields, ops, and as are defined so skip lifting + return Ok(()); + } + } + + // Add lifted aggregate transform, potentially after the joinaggregate transform + lifted_transforms.push(TransformSpec::Aggregate(agg)); + + // Create facet dataset name and increment counter to keep names unique even if the same + // source dataset is used in multiple facets + let facet_dataset_name = format!("{}_facet_{}{}", facet.data, facet.name, self.counter); + self.counter += 1; + + // Create new dataset that should be added to the spec at the resolved scope + let new_dataset = DataSpec { + name: facet_dataset_name.clone(), + source: Some(facet.data.clone()), + transform: lifted_transforms, + url: None, + format: None, + values: None, + on: None, + extra: Default::default(), + }; + + // Save new dataset at the same scope as the original input dataset + let Ok(resolved) = self.task_scope.resolve_scope( + &Variable::new_data(&facet.data), scope + ) else { return Ok(()) }; + + self.new_datasets + .entry(resolved.scope.clone()) + .or_default() + .push(new_dataset); + + // Remove leading aggregate transform from child dataset + child_dataset.transform.remove(0); + + // Rename source dataset in facet + facet.data = facet_dataset_name; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::planning::lift_facet_aggregations::lift_facet_aggregations; + use crate::spec::chart::ChartSpec; + use serde_json::json; + + #[test] + fn test_simple_facet_lift() { + let mut chart_spec: ChartSpec = serde_json::from_value(json!( + { + "$schema": "https://vega.github.io/schema/vega/v5.json", + "data": [ + { + "name": "source_0", + "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/barley.json", + "format": {"type": "json"} + } + ], + "marks": [ + { + "name": "cell", + "type": "group", + "from": { + "facet": {"name": "facet", "data": "source_0", "groupby": ["site"]} + }, + "sort": {"field": ["datum[\"site\"]"], "order": ["ascending"]}, + "data": [ + { + "source": "facet", + "name": "data_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["year"], + "ops": ["ci0", "ci1", "mean", "mean"], + "fields": ["yield", "yield", "yield", "yield"], + "as": ["lower_yield", "upper_yield", "center_yield", "mean_yield"] + } + ] + }, + { + "name": "data_1", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"lower_yield\"]) && isFinite(+datum[\"lower_yield\"])" + } + ] + }, + { + "name": "data_2", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_yield\"]) && isFinite(+datum[\"mean_yield\"])" + } + ] + } + ], + } + ], + } + )).unwrap(); + + lift_facet_aggregations(&mut chart_spec, &Default::default()).unwrap(); + let pretty_spec = serde_json::to_string_pretty(&chart_spec).unwrap(); + println!("{pretty_spec}"); + assert_eq!( + pretty_spec, + r#"{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "data": [ + { + "name": "source_0", + "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/barley.json", + "format": { + "type": "json" + } + }, + { + "name": "source_0_facet_facet0", + "source": "source_0", + "transform": [ + { + "type": "aggregate", + "groupby": [ + "year", + "site" + ], + "fields": [ + "yield", + "yield", + "yield", + "yield" + ], + "ops": [ + "ci0", + "ci1", + "mean", + "mean" + ], + "as": [ + "lower_yield", + "upper_yield", + "center_yield", + "mean_yield" + ] + } + ] + } + ], + "marks": [ + { + "type": "group", + "name": "cell", + "from": { + "facet": { + "data": "source_0_facet_facet0", + "name": "facet", + "groupby": [ + "site" + ] + } + }, + "sort": { + "field": [ + "datum[\"site\"]" + ], + "order": [ + "ascending" + ] + }, + "data": [ + { + "name": "data_0", + "source": "facet" + }, + { + "name": "data_1", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"lower_yield\"]) && isFinite(+datum[\"lower_yield\"])" + } + ] + }, + { + "name": "data_2", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_yield\"]) && isFinite(+datum[\"mean_yield\"])" + } + ] + } + ] + } + ] +}"# + ) + } +} diff --git a/vegafusion-core/src/planning/mod.rs b/vegafusion-core/src/planning/mod.rs index 4919db681..f6a68b236 100644 --- a/vegafusion-core/src/planning/mod.rs +++ b/vegafusion-core/src/planning/mod.rs @@ -1,6 +1,7 @@ pub mod dependency_graph; pub mod extract; pub mod fuse; +pub mod lift_facet_aggregations; pub mod optimize_server; pub mod plan; pub mod projection_pushdown; diff --git a/vegafusion-core/src/planning/plan.rs b/vegafusion-core/src/planning/plan.rs index 6305c6ef9..59bddf78d 100644 --- a/vegafusion-core/src/planning/plan.rs +++ b/vegafusion-core/src/planning/plan.rs @@ -1,6 +1,7 @@ use crate::error::Result; use crate::planning::extract::extract_server_data; use crate::planning::fuse::fuse_datasets; +use crate::planning::lift_facet_aggregations::lift_facet_aggregations; use crate::planning::optimize_server::split_data_url_nodes; use crate::planning::projection_pushdown::projection_pushdown; use crate::planning::split_domain_data::split_domain_data; @@ -87,6 +88,7 @@ pub struct PlannerConfig { pub extract_server_data: bool, pub allow_client_to_server_comms: bool, pub fuse_datasets: bool, + pub lift_facet_aggregations: bool, pub client_only_vars: Vec, pub keep_variables: Vec, } @@ -102,6 +104,7 @@ impl Default for PlannerConfig { extract_server_data: true, allow_client_to_server_comms: true, fuse_datasets: true, + lift_facet_aggregations: true, client_only_vars: Default::default(), keep_variables: Default::default(), } @@ -147,6 +150,11 @@ impl SpecPlan { Default::default() }; + // Lift aggregation transforms out of facets so they can be evaluated on the server + if config.lift_facet_aggregations { + lift_facet_aggregations(&mut client_spec, config)?; + } + // Attempt to limit the columns produced by each dataset to only include those // that are actually used downstream if config.projection_pushdown { diff --git a/vegafusion-core/src/planning/projection_pushdown.rs b/vegafusion-core/src/planning/projection_pushdown.rs index a068b90d1..e4cb3ec79 100644 --- a/vegafusion-core/src/planning/projection_pushdown.rs +++ b/vegafusion-core/src/planning/projection_pushdown.rs @@ -550,7 +550,7 @@ impl GetDatasetsColumnUsage for ChartSpec { // Handle data // Here we need to be careful to traverse datasets in rever topological order. - if let Ok(dep_graph) = build_dependency_graph(self, &Default::default()) { + if let Ok((dep_graph, _)) = build_dependency_graph(self, &Default::default()) { if let Ok(node_indexes) = toposort(&dep_graph, None) { // Iterate over dependencies in reverse topological order for node_idx in node_indexes.iter().rev() { diff --git a/vegafusion-core/src/planning/split_domain_data.rs b/vegafusion-core/src/planning/split_domain_data.rs index 5f13d7752..7b19e755d 100644 --- a/vegafusion-core/src/planning/split_domain_data.rs +++ b/vegafusion-core/src/planning/split_domain_data.rs @@ -138,7 +138,7 @@ impl<'a> SplitScaleDomainVisitor<'a> { let new_domain = ScaleDomainSpec::FieldsReferences(ScaleFieldsReferencesSpec { fields: new_fields .into_iter() - .map(|f| ScaleDataReferenceOrSignalSpec::Reference(f)) + .map(ScaleDataReferenceOrSignalSpec::Reference) .collect(), sort, extra: Default::default(), diff --git a/vegafusion-core/src/spec/mark.rs b/vegafusion-core/src/spec/mark.rs index 98992b259..5b3970bf1 100644 --- a/vegafusion-core/src/spec/mark.rs +++ b/vegafusion-core/src/spec/mark.rs @@ -5,7 +5,8 @@ use crate::spec::data::DataSpec; use crate::spec::scale::ScaleSpec; use crate::spec::signal::SignalSpec; use crate::spec::title::TitleSpec; -use crate::spec::values::StringOrStringList; +use crate::spec::transform::aggregate::AggregateOpSpec; +use crate::spec::values::{Field, StringOrStringList}; use serde::{Deserialize, Serialize}; use serde_json::{Number, Value}; use std::collections::HashMap; @@ -217,6 +218,12 @@ pub struct MarkFacetSpec { pub data: String, pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub groupby: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub aggregate: Option, + #[serde(flatten)] pub extra: HashMap, } @@ -246,3 +253,18 @@ pub struct MarkSort { #[serde(flatten)] pub extra: HashMap, } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MarkFacetAggregate { + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub ops: Option>, + + #[serde(rename = "as", skip_serializing_if = "Option::is_none")] + pub as_: Option>>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub cross: Option, +} diff --git a/vegafusion-core/src/spec/values.rs b/vegafusion-core/src/spec/values.rs index 9cf2f1834..113ed5c39 100644 --- a/vegafusion-core/src/spec/values.rs +++ b/vegafusion-core/src/spec/values.rs @@ -19,6 +19,12 @@ impl StringOrStringList { } } +impl Default for StringOrStringList { + fn default() -> Self { + Self::StringList(Vec::new()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(untagged)] pub enum Field { diff --git a/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.comm_plan.json b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.comm_plan.json new file mode 100644 index 000000000..1cd9bd0ae --- /dev/null +++ b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.comm_plan.json @@ -0,0 +1,40 @@ +{ + "server_to_client": [ + { + "name": "data_2", + "namespace": "data", + "scope": [] + }, + { + "name": "data_2_x_domain_MPAA_Rating_1", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3_color_domain_MPAA_Rating", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3_x_domain_MPAA_Rating_0", + "namespace": "data", + "scope": [] + }, + { + "name": "row_domain", + "namespace": "data", + "scope": [] + }, + { + "name": "source_0_facet_facet0", + "namespace": "data", + "scope": [] + } + ], + "client_to_server": [] +} \ No newline at end of file diff --git a/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.vg.json b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.vg.json new file mode 100644 index 000000000..dcbdd3470 --- /dev/null +++ b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars.vg.json @@ -0,0 +1,306 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "description": "Example from https://github.com/hex-inc/vegafusion/issues/370", + "padding": 5, + "data": [ + { + "name": "source_0", + "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/movies.json", + "format": {"type": "json"} + }, + { + "name": "row_domain", + "source": "source_0", + "transform": [{"type": "aggregate", "groupby": ["Creative_Type"]}] + }, + { + "name": "data_1", + "source": "source_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["MPAA_Rating", "Creative_Type"], + "ops": ["stdev", "mean", "mean"], + "fields": ["Worldwide_Gross", "Worldwide_Gross", "Worldwide_Gross"], + "as": [ + "extent_Worldwide_Gross", + "center_Worldwide_Gross", + "mean_Worldwide_Gross" + ] + } + ] + }, + { + "name": "data_2", + "source": "data_1", + "transform": [ + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] + datum[\"extent_Worldwide_Gross\"]", + "as": "upper_Worldwide_Gross" + }, + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] - datum[\"extent_Worldwide_Gross\"]", + "as": "lower_Worldwide_Gross" + }, + { + "type": "filter", + "expr": "isValid(datum[\"lower_Worldwide_Gross\"]) && isFinite(+datum[\"lower_Worldwide_Gross\"])" + } + ] + }, + { + "name": "data_3", + "source": "data_1", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_Worldwide_Gross\"]) && isFinite(+datum[\"mean_Worldwide_Gross\"])" + } + ] + } + ], + "signals": [ + {"name": "x_step", "value": 20}, + { + "name": "child_width", + "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step" + }, + {"name": "child_height", "value": 300} + ], + "layout": { + "padding": 20, + "offset": {"rowTitle": 10}, + "columns": 1, + "bounds": "full", + "align": "all" + }, + "marks": [ + { + "name": "row-title", + "type": "group", + "role": "row-title", + "title": { + "text": "Creative_Type", + "orient": "left", + "style": "guide-title", + "offset": 10 + } + }, + { + "name": "row_header", + "type": "group", + "role": "row-header", + "from": {"data": "row_domain"}, + "sort": {"field": "datum[\"Creative_Type\"]", "order": "ascending"}, + "title": { + "text": { + "signal": "isValid(parent[\"Creative_Type\"]) ? parent[\"Creative_Type\"] : \"\"+parent[\"Creative_Type\"]" + }, + "orient": "left", + "style": "guide-label", + "frame": "group", + "offset": 10 + }, + "encode": {"update": {"height": {"signal": "child_height"}}}, + "axes": [ + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Worldwide_Gross", + "labelOverlap": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "zindex": 0 + } + ] + }, + { + "name": "column_footer", + "type": "group", + "role": "column-footer", + "encode": {"update": {"width": {"signal": "child_width"}}}, + "axes": [ + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "MPAA_Rating", + "labelAlign": "right", + "labelAngle": 270, + "labelBaseline": "middle", + "zindex": 0 + } + ] + }, + { + "name": "cell", + "type": "group", + "style": "cell", + "from": { + "facet": { + "name": "facet", + "data": "source_0", + "groupby": ["Creative_Type"] + } + }, + "sort": {"field": ["datum[\"Creative_Type\"]"], "order": ["ascending"]}, + "data": [ + { + "source": "facet", + "name": "data_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["MPAA_Rating"], + "ops": ["stdev", "mean", "mean"], + "fields": [ + "Worldwide_Gross", + "Worldwide_Gross", + "Worldwide_Gross" + ], + "as": [ + "extent_Worldwide_Gross", + "center_Worldwide_Gross", + "mean_Worldwide_Gross" + ] + } + ] + }, + { + "name": "data_1", + "source": "data_0", + "transform": [ + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] + datum[\"extent_Worldwide_Gross\"]", + "as": "upper_Worldwide_Gross" + }, + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] - datum[\"extent_Worldwide_Gross\"]", + "as": "lower_Worldwide_Gross" + }, + { + "type": "filter", + "expr": "isValid(datum[\"lower_Worldwide_Gross\"]) && isFinite(+datum[\"lower_Worldwide_Gross\"])" + } + ] + }, + { + "name": "data_2", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_Worldwide_Gross\"]) && isFinite(+datum[\"mean_Worldwide_Gross\"])" + } + ] + } + ], + "encode": { + "update": { + "width": {"signal": "child_width"}, + "height": {"signal": "child_height"} + } + }, + "marks": [ + { + "name": "child_layer_0_marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_2"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "MPAA_Rating"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"MPAA_Rating: \" + (isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]) + \"; Mean of Worldwide_Gross: \" + (format(datum[\"mean_Worldwide_Gross\"], \"\"))" + }, + "x": {"scale": "x", "field": "MPAA_Rating"}, + "width": {"signal": "max(0.25, bandwidth('x'))"}, + "y": {"scale": "y", "field": "mean_Worldwide_Gross"}, + "y2": {"scale": "y", "value": 0} + } + } + }, + { + "name": "child_layer_1_marks", + "type": "rule", + "style": ["rule", "errorbar-rule"], + "from": {"data": "data_1"}, + "encode": { + "update": { + "ariaRoleDescription": {"value": "errorbar"}, + "stroke": {"value": "black"}, + "tooltip": { + "signal": "{\"Mean of Worldwide_Gross\": format(datum[\"center_Worldwide_Gross\"], \"\"), \"Mean + stdev of Worldwide_Gross\": format(datum[\"upper_Worldwide_Gross\"], \"\"), \"Mean - stdev of Worldwide_Gross\": format(datum[\"lower_Worldwide_Gross\"], \"\"), \"MPAA_Rating\": isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]}" + }, + "description": { + "signal": "\"MPAA_Rating: \" + (isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]) + \"; Worldwide_Gross: \" + (format(datum[\"lower_Worldwide_Gross\"], \"\")) + \"; upper_Worldwide_Gross: \" + (format(datum[\"upper_Worldwide_Gross\"], \"\")) + \"; Mean of Worldwide_Gross: \" + (format(datum[\"center_Worldwide_Gross\"], \"\")) + \"; Mean + stdev of Worldwide_Gross: \" + (format(datum[\"upper_Worldwide_Gross\"], \"\")) + \"; Mean - stdev of Worldwide_Gross: \" + (format(datum[\"lower_Worldwide_Gross\"], \"\"))" + }, + "x": {"scale": "x", "field": "MPAA_Rating", "band": 0.5}, + "y": {"scale": "y", "field": "lower_Worldwide_Gross"}, + "y2": {"scale": "y", "field": "upper_Worldwide_Gross"} + } + } + } + ], + "axes": [ + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + } + ] + } + ], + "scales": [ + { + "name": "x", + "type": "band", + "domain": { + "fields": [ + {"data": "data_3", "field": "MPAA_Rating"}, + {"data": "data_2", "field": "MPAA_Rating"} + ], + "sort": true + }, + "range": {"step": {"signal": "x_step"}}, + "paddingInner": 0.1, + "paddingOuter": 0.05 + }, + { + "name": "y", + "type": "linear", + "domain": { + "fields": [ + {"data": "data_3", "field": "mean_Worldwide_Gross"}, + {"data": "data_2", "field": "lower_Worldwide_Gross"}, + {"data": "data_2", "field": "upper_Worldwide_Gross"} + ] + }, + "range": [{"signal": "child_height"}, 0], + "nice": true, + "zero": true + }, + { + "name": "color", + "type": "ordinal", + "domain": {"data": "data_3", "field": "MPAA_Rating", "sort": true}, + "range": "category" + } + ], + "legends": [{"fill": "color", "symbolType": "square", "title": "MPAA_Rating"}] +} \ No newline at end of file diff --git a/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.comm_plan.json b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.comm_plan.json new file mode 100644 index 000000000..1cd9bd0ae --- /dev/null +++ b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.comm_plan.json @@ -0,0 +1,40 @@ +{ + "server_to_client": [ + { + "name": "data_2", + "namespace": "data", + "scope": [] + }, + { + "name": "data_2_x_domain_MPAA_Rating_1", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3_color_domain_MPAA_Rating", + "namespace": "data", + "scope": [] + }, + { + "name": "data_3_x_domain_MPAA_Rating_0", + "namespace": "data", + "scope": [] + }, + { + "name": "row_domain", + "namespace": "data", + "scope": [] + }, + { + "name": "source_0_facet_facet0", + "namespace": "data", + "scope": [] + } + ], + "client_to_server": [] +} \ No newline at end of file diff --git a/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.vg.json b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.vg.json new file mode 100644 index 000000000..50421ed71 --- /dev/null +++ b/vegafusion-runtime/tests/specs/custom/facet_grouped_bar_with_error_bars_with_sort.vg.json @@ -0,0 +1,324 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "background": "white", + "padding": 5, + "data": [ + { + "name": "source_0", + "url": "https://cdn.jsdelivr.net/npm/vega-datasets@v1.29.0/data/movies.json", + "format": {"type": "json"} + }, + { + "name": "row_domain", + "source": "source_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["Creative_Type"], + "fields": ["Worldwide_Gross"], + "ops": ["mean"], + "as": ["mean_Worldwide_Gross"] + } + ] + }, + { + "name": "data_1", + "source": "source_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["MPAA_Rating", "Creative_Type"], + "ops": ["stdev", "mean", "mean"], + "fields": ["Worldwide_Gross", "Worldwide_Gross", "Worldwide_Gross"], + "as": [ + "extent_Worldwide_Gross", + "center_Worldwide_Gross", + "mean_Worldwide_Gross" + ] + } + ] + }, + { + "name": "data_2", + "source": "data_1", + "transform": [ + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] + datum[\"extent_Worldwide_Gross\"]", + "as": "upper_Worldwide_Gross" + }, + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] - datum[\"extent_Worldwide_Gross\"]", + "as": "lower_Worldwide_Gross" + }, + { + "type": "filter", + "expr": "isValid(datum[\"lower_Worldwide_Gross\"]) && isFinite(+datum[\"lower_Worldwide_Gross\"])" + } + ] + }, + { + "name": "data_3", + "source": "data_1", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_Worldwide_Gross\"]) && isFinite(+datum[\"mean_Worldwide_Gross\"])" + } + ] + } + ], + "signals": [ + {"name": "x_step", "value": 20}, + { + "name": "child_width", + "update": "bandspace(domain('x').length, 0.1, 0.05) * x_step" + }, + {"name": "child_height", "value": 300} + ], + "layout": { + "padding": 20, + "offset": {"rowTitle": 10}, + "columns": 1, + "bounds": "full", + "align": "all" + }, + "marks": [ + { + "name": "row-title", + "type": "group", + "role": "row-title", + "title": { + "text": "Creative_Type", + "orient": "left", + "style": "guide-title", + "offset": 10 + } + }, + { + "name": "row_header", + "type": "group", + "role": "row-header", + "from": {"data": "row_domain"}, + "sort": { + "field": "datum[\"mean_Worldwide_Gross\"]", + "order": "descending" + }, + "title": { + "text": { + "signal": "isValid(parent[\"Creative_Type\"]) ? parent[\"Creative_Type\"] : \"\"+parent[\"Creative_Type\"]" + }, + "orient": "left", + "style": "guide-label", + "frame": "group", + "offset": 10 + }, + "encode": {"update": {"height": {"signal": "child_height"}}}, + "axes": [ + { + "scale": "y", + "orient": "left", + "grid": false, + "title": "Worldwide_Gross", + "labelOverlap": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "zindex": 0 + } + ] + }, + { + "name": "column_footer", + "type": "group", + "role": "column-footer", + "encode": {"update": {"width": {"signal": "child_width"}}}, + "axes": [ + { + "scale": "x", + "orient": "bottom", + "grid": false, + "title": "MPAA_Rating", + "labelAlign": "right", + "labelAngle": 270, + "labelBaseline": "middle", + "zindex": 0 + } + ] + }, + { + "name": "cell", + "type": "group", + "style": "cell", + "from": { + "facet": { + "name": "facet", + "data": "source_0", + "groupby": ["Creative_Type"], + "aggregate": { + "fields": ["Worldwide_Gross"], + "ops": ["mean"], + "as": ["mean_Worldwide_Gross_by_Creative_Type"] + } + } + }, + "sort": { + "field": ["datum[\"mean_Worldwide_Gross_by_Creative_Type\"]"], + "order": ["descending"] + }, + "data": [ + { + "source": "facet", + "name": "data_0", + "transform": [ + { + "type": "aggregate", + "groupby": ["MPAA_Rating"], + "ops": ["stdev", "mean", "mean"], + "fields": [ + "Worldwide_Gross", + "Worldwide_Gross", + "Worldwide_Gross" + ], + "as": [ + "extent_Worldwide_Gross", + "center_Worldwide_Gross", + "mean_Worldwide_Gross" + ] + } + ] + }, + { + "name": "data_1", + "source": "data_0", + "transform": [ + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] + datum[\"extent_Worldwide_Gross\"]", + "as": "upper_Worldwide_Gross" + }, + { + "type": "formula", + "expr": "datum[\"center_Worldwide_Gross\"] - datum[\"extent_Worldwide_Gross\"]", + "as": "lower_Worldwide_Gross" + }, + { + "type": "filter", + "expr": "isValid(datum[\"lower_Worldwide_Gross\"]) && isFinite(+datum[\"lower_Worldwide_Gross\"])" + } + ] + }, + { + "name": "data_2", + "source": "data_0", + "transform": [ + { + "type": "filter", + "expr": "isValid(datum[\"mean_Worldwide_Gross\"]) && isFinite(+datum[\"mean_Worldwide_Gross\"])" + } + ] + } + ], + "encode": { + "update": { + "width": {"signal": "child_width"}, + "height": {"signal": "child_height"} + } + }, + "marks": [ + { + "name": "child_layer_0_marks", + "type": "rect", + "style": ["bar"], + "from": {"data": "data_2"}, + "encode": { + "update": { + "fill": {"scale": "color", "field": "MPAA_Rating"}, + "ariaRoleDescription": {"value": "bar"}, + "description": { + "signal": "\"MPAA_Rating: \" + (isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]) + \"; Mean of Worldwide_Gross: \" + (format(datum[\"mean_Worldwide_Gross\"], \"\"))" + }, + "x": {"scale": "x", "field": "MPAA_Rating"}, + "width": {"signal": "max(0.25, bandwidth('x'))"}, + "y": {"scale": "y", "field": "mean_Worldwide_Gross"}, + "y2": {"scale": "y", "value": 0} + } + } + }, + { + "name": "child_layer_1_marks", + "type": "rule", + "style": ["rule", "errorbar-rule"], + "from": {"data": "data_1"}, + "encode": { + "update": { + "ariaRoleDescription": {"value": "errorbar"}, + "stroke": {"value": "black"}, + "tooltip": { + "signal": "{\"Mean of Worldwide_Gross\": format(datum[\"center_Worldwide_Gross\"], \"\"), \"Mean + stdev of Worldwide_Gross\": format(datum[\"upper_Worldwide_Gross\"], \"\"), \"Mean - stdev of Worldwide_Gross\": format(datum[\"lower_Worldwide_Gross\"], \"\"), \"MPAA_Rating\": isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]}" + }, + "description": { + "signal": "\"MPAA_Rating: \" + (isValid(datum[\"MPAA_Rating\"]) ? datum[\"MPAA_Rating\"] : \"\"+datum[\"MPAA_Rating\"]) + \"; Worldwide_Gross: \" + (format(datum[\"lower_Worldwide_Gross\"], \"\")) + \"; upper_Worldwide_Gross: \" + (format(datum[\"upper_Worldwide_Gross\"], \"\")) + \"; Mean of Worldwide_Gross: \" + (format(datum[\"center_Worldwide_Gross\"], \"\")) + \"; Mean + stdev of Worldwide_Gross: \" + (format(datum[\"upper_Worldwide_Gross\"], \"\")) + \"; Mean - stdev of Worldwide_Gross: \" + (format(datum[\"lower_Worldwide_Gross\"], \"\"))" + }, + "x": {"scale": "x", "field": "MPAA_Rating", "band": 0.5}, + "y": {"scale": "y", "field": "lower_Worldwide_Gross"}, + "y2": {"scale": "y", "field": "upper_Worldwide_Gross"} + } + } + } + ], + "axes": [ + { + "scale": "y", + "orient": "left", + "gridScale": "x", + "grid": true, + "tickCount": {"signal": "ceil(child_height/40)"}, + "domain": false, + "labels": false, + "aria": false, + "maxExtent": 0, + "minExtent": 0, + "ticks": false, + "zindex": 0 + } + ] + } + ], + "scales": [ + { + "name": "x", + "type": "band", + "domain": { + "fields": [ + {"data": "data_3", "field": "MPAA_Rating"}, + {"data": "data_2", "field": "MPAA_Rating"} + ], + "sort": true + }, + "range": {"step": {"signal": "x_step"}}, + "paddingInner": 0.1, + "paddingOuter": 0.05 + }, + { + "name": "y", + "type": "linear", + "domain": { + "fields": [ + {"data": "data_3", "field": "mean_Worldwide_Gross"}, + {"data": "data_2", "field": "lower_Worldwide_Gross"}, + {"data": "data_2", "field": "upper_Worldwide_Gross"} + ] + }, + "range": [{"signal": "child_height"}, 0], + "nice": true, + "zero": true + }, + { + "name": "color", + "type": "ordinal", + "domain": {"data": "data_3", "field": "MPAA_Rating", "sort": true}, + "range": "category" + } + ], + "legends": [{"fill": "color", "symbolType": "square", "title": "MPAA_Rating"}] +} \ No newline at end of file diff --git a/vegafusion-runtime/tests/specs/custom/gh_391.comm_plan.json b/vegafusion-runtime/tests/specs/custom/gh_391.comm_plan.json index 8c20e672a..4b583fca1 100644 --- a/vegafusion-runtime/tests/specs/custom/gh_391.comm_plan.json +++ b/vegafusion-runtime/tests/specs/custom/gh_391.comm_plan.json @@ -16,7 +16,7 @@ "scope": [] }, { - "name": "data_source", + "name": "data_source_facet_facet0", "namespace": "data", "scope": [] }, diff --git a/vegafusion-runtime/tests/specs/vegalite/facet_independent_scale_layer_broken.comm_plan.json b/vegafusion-runtime/tests/specs/vegalite/facet_independent_scale_layer_broken.comm_plan.json index b05f47bb4..95de8e731 100644 --- a/vegafusion-runtime/tests/specs/vegalite/facet_independent_scale_layer_broken.comm_plan.json +++ b/vegafusion-runtime/tests/specs/vegalite/facet_independent_scale_layer_broken.comm_plan.json @@ -27,7 +27,7 @@ }, { "namespace": "data", - "name": "source_0", + "name": "source_0_facet_facet0", "scope": [] } ], diff --git a/vegafusion-runtime/tests/specs/vegalite/trellis_barley.comm_plan.json b/vegafusion-runtime/tests/specs/vegalite/trellis_barley.comm_plan.json index 31d8b1ef5..8e15b3c18 100644 --- a/vegafusion-runtime/tests/specs/vegalite/trellis_barley.comm_plan.json +++ b/vegafusion-runtime/tests/specs/vegalite/trellis_barley.comm_plan.json @@ -12,7 +12,7 @@ }, { "namespace": "data", - "name": "source_0", + "name": "source_0_facet_trellis_barley_facet0", "scope": [] }, { diff --git a/vegafusion-runtime/tests/specs/vegalite/trellis_barley_independent.comm_plan.json b/vegafusion-runtime/tests/specs/vegalite/trellis_barley_independent.comm_plan.json index cf85cbaa6..948e32887 100644 --- a/vegafusion-runtime/tests/specs/vegalite/trellis_barley_independent.comm_plan.json +++ b/vegafusion-runtime/tests/specs/vegalite/trellis_barley_independent.comm_plan.json @@ -12,7 +12,7 @@ }, { "namespace": "data", - "name": "source_0", + "name": "source_0_facet_trellis_barley_facet0", "scope": [] }, { diff --git a/vegafusion-runtime/tests/test_image_comparison.rs b/vegafusion-runtime/tests/test_image_comparison.rs index c5ef2b695..251fa5de2 100644 --- a/vegafusion-runtime/tests/test_image_comparison.rs +++ b/vegafusion-runtime/tests/test_image_comparison.rs @@ -141,7 +141,9 @@ mod test_custom_specs { case("custom/gh_361", 0.001, true), case("custom/gh_379", 0.001, true), case("custom/gh_383", 0.001, true), - case("custom/gh_391", 0.001, true) + case("custom/gh_391", 0.001, true), + case("custom/facet_grouped_bar_with_error_bars", 0.001, true), + case("custom/facet_grouped_bar_with_error_bars_with_sort", 0.001, true) )] fn test_image_comparison(spec_name: &str, tolerance: f64, extract_inline_values: bool) { println!("spec_name: {spec_name}");