diff --git a/examples/compiled/airport_connections.svg b/examples/compiled/airport_connections.svg
index 725e6af54d..3489a11df3 100644
--- a/examples/compiled/airport_connections.svg
+++ b/examples/compiled/airport_connections.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/examples/compiled/airport_connections.vg.json b/examples/compiled/airport_connections.vg.json
index e7ebab19a6..ebce8e1b48 100644
--- a/examples/compiled/airport_connections.vg.json
+++ b/examples/compiled/airport_connections.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 900,
"height": 500,
+ "style": "view",
"data": [
{"name": "org_store"},
{
@@ -177,7 +178,7 @@
"name": "layer_0_marks",
"type": "shape",
"style": ["geoshape"],
- "interactive": false,
+ "interactive": true,
"from": {"data": "source_0"},
"encode": {
"update": {
diff --git a/examples/compiled/arc_donut.vg.json b/examples/compiled/arc_donut.vg.json
index 389e13d5ef..60ec25b55f 100644
--- a/examples/compiled/arc_donut.vg.json
+++ b/examples/compiled/arc_donut.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/arc_facet.vg.json b/examples/compiled/arc_facet.vg.json
index 89f6d05fb8..d88dc740bc 100644
--- a/examples/compiled/arc_facet.vg.json
+++ b/examples/compiled/arc_facet.vg.json
@@ -72,6 +72,7 @@
{
"name": "cell",
"type": "group",
+ "style": "view",
"from": {
"facet": {"name": "facet", "data": "source_0", "groupby": ["year"]}
},
diff --git a/examples/compiled/arc_ordinal_theta.vg.json b/examples/compiled/arc_ordinal_theta.vg.json
index b8aed31ada..3a2a46e8e9 100644
--- a/examples/compiled/arc_ordinal_theta.vg.json
+++ b/examples/compiled/arc_ordinal_theta.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/arc_params.vg.json b/examples/compiled/arc_params.vg.json
index 14d707be61..0af536c4cd 100644
--- a/examples/compiled/arc_params.vg.json
+++ b/examples/compiled/arc_params.vg.json
@@ -80,6 +80,7 @@
"type": "group",
"name": "concat_0_group",
"title": {"text": "Single Arc", "frame": "group"},
+ "style": "view",
"encode": {
"update": {
"width": {"signal": "childWidth"},
@@ -114,6 +115,7 @@
"type": "group",
"name": "concat_1_group",
"title": {"text": "Stacked Arcs", "frame": "group"},
+ "style": "view",
"encode": {
"update": {
"width": {"signal": "childWidth"},
diff --git a/examples/compiled/arc_pie.vg.json b/examples/compiled/arc_pie.vg.json
index f55c0ef74f..eb9e150a1b 100644
--- a/examples/compiled/arc_pie.vg.json
+++ b/examples/compiled/arc_pie.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/arc_pie_pyramid.vg.json b/examples/compiled/arc_pie_pyramid.vg.json
index e39ac16596..e49b5d398d 100644
--- a/examples/compiled/arc_pie_pyramid.vg.json
+++ b/examples/compiled/arc_pie_pyramid.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/arc_radial.vg.json b/examples/compiled/arc_radial.vg.json
index eb72aad8ad..8a23181fe6 100644
--- a/examples/compiled/arc_radial.vg.json
+++ b/examples/compiled/arc_radial.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{"name": "source_0", "values": [12, 23, 47, 6, 52, 19]},
{
diff --git a/examples/compiled/arc_radial_histogram.vg.json b/examples/compiled/arc_radial_histogram.vg.json
index 485e9da81f..3211200218 100644
--- a/examples/compiled/arc_radial_histogram.vg.json
+++ b/examples/compiled/arc_radial_histogram.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_choropleth.vg.json b/examples/compiled/geo_choropleth.vg.json
index b6b9285091..857c5be0c1 100644
--- a/examples/compiled/geo_choropleth.vg.json
+++ b/examples/compiled/geo_choropleth.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{
"name": "source_1",
diff --git a/examples/compiled/geo_circle.vg.json b/examples/compiled/geo_circle.vg.json
index d241713686..928b5138ea 100644
--- a/examples/compiled/geo_circle.vg.json
+++ b/examples/compiled/geo_circle.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_constant_value.vg.json b/examples/compiled/geo_constant_value.vg.json
index b26c519b0a..ebf40947e0 100644
--- a/examples/compiled/geo_constant_value.vg.json
+++ b/examples/compiled/geo_constant_value.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{"name": "source_0", "url": "data/airports.csv", "format": {"type": "csv"}},
{
diff --git a/examples/compiled/geo_custom_projection.vg.json b/examples/compiled/geo_custom_projection.vg.json
index ccbb878b68..11e3a1680d 100644
--- a/examples/compiled/geo_custom_projection.vg.json
+++ b/examples/compiled/geo_custom_projection.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 900,
"height": 560,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_graticule.vg.json b/examples/compiled/geo_graticule.vg.json
index b99cca1b04..4383dbdff4 100644
--- a/examples/compiled/geo_graticule.vg.json
+++ b/examples/compiled/geo_graticule.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [{"name": "source_0", "transform": [{"type": "graticule"}]}],
"projections": [
{
diff --git a/examples/compiled/geo_graticule_object.vg.json b/examples/compiled/geo_graticule_object.vg.json
index 3f9782c791..80d5094d91 100644
--- a/examples/compiled/geo_graticule_object.vg.json
+++ b/examples/compiled/geo_graticule_object.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{"name": "source_0", "transform": [{"type": "graticule", "step": [15, 15]}]}
],
diff --git a/examples/compiled/geo_layer.vg.json b/examples/compiled/geo_layer.vg.json
index 7ca74aec0f..b2b66955c1 100644
--- a/examples/compiled/geo_layer.vg.json
+++ b/examples/compiled/geo_layer.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_layer_line_london.vg.json b/examples/compiled/geo_layer_line_london.vg.json
index 2811207595..c2bf00c0f3 100644
--- a/examples/compiled/geo_layer_line_london.vg.json
+++ b/examples/compiled/geo_layer_line_london.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 700,
"height": 500,
+ "style": "view",
"encode": {"update": {"stroke": {"value": "transparent"}}},
"data": [
{
diff --git a/examples/compiled/geo_line.vg.json b/examples/compiled/geo_line.vg.json
index ee3bcb803a..6220f8efc7 100644
--- a/examples/compiled/geo_line.vg.json
+++ b/examples/compiled/geo_line.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 800,
"height": 500,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_params_projections.vg.json b/examples/compiled/geo_params_projections.vg.json
index d8933df702..03b8102be8 100644
--- a/examples/compiled/geo_params_projections.vg.json
+++ b/examples/compiled/geo_params_projections.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_point.vg.json b/examples/compiled/geo_point.vg.json
index 1ae2c7c509..4991440538 100644
--- a/examples/compiled/geo_point.vg.json
+++ b/examples/compiled/geo_point.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 500,
"height": 300,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_repeat.vg.json b/examples/compiled/geo_repeat.vg.json
index ab1e762614..52b141ba78 100644
--- a/examples/compiled/geo_repeat.vg.json
+++ b/examples/compiled/geo_repeat.vg.json
@@ -101,6 +101,7 @@
{
"type": "group",
"name": "child__row_population_group",
+ "style": "view",
"encode": {
"update": {
"width": {"signal": "width"},
@@ -145,6 +146,7 @@
{
"type": "group",
"name": "child__row_engineers_group",
+ "style": "view",
"encode": {
"update": {
"width": {"signal": "width"},
@@ -189,6 +191,7 @@
{
"type": "group",
"name": "child__row_hurricanes_group",
+ "style": "view",
"encode": {
"update": {
"width": {"signal": "width"},
diff --git a/examples/compiled/geo_rule.vg.json b/examples/compiled/geo_rule.vg.json
index 3fbd119fab..a1e005b345 100644
--- a/examples/compiled/geo_rule.vg.json
+++ b/examples/compiled/geo_rule.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 800,
"height": 500,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_sphere.vg.json b/examples/compiled/geo_sphere.vg.json
index 1665877a97..d1f0cc0189 100644
--- a/examples/compiled/geo_sphere.vg.json
+++ b/examples/compiled/geo_sphere.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{"name": "source_0", "values": [{"type": "Sphere"}]},
{"name": "source_1", "transform": [{"type": "graticule"}]}
diff --git a/examples/compiled/geo_text.vg.json b/examples/compiled/geo_text.vg.json
index fdf4712a9d..070f566b95 100644
--- a/examples/compiled/geo_text.vg.json
+++ b/examples/compiled/geo_text.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 800,
"height": 500,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/geo_trellis.vg.json b/examples/compiled/geo_trellis.vg.json
index 3da339cb3d..23073cb4ae 100644
--- a/examples/compiled/geo_trellis.vg.json
+++ b/examples/compiled/geo_trellis.vg.json
@@ -85,6 +85,7 @@
{
"name": "cell",
"type": "group",
+ "style": "view",
"from": {
"facet": {"name": "facet", "data": "source_0", "groupby": ["group"]}
},
diff --git a/examples/compiled/interactive_1d_geo_brush.png b/examples/compiled/interactive_1d_geo_brush.png
new file mode 100644
index 0000000000..17dfd8f159
Binary files /dev/null and b/examples/compiled/interactive_1d_geo_brush.png differ
diff --git a/examples/compiled/interactive_1d_geo_brush.svg b/examples/compiled/interactive_1d_geo_brush.svg
new file mode 100644
index 0000000000..d7558f3c32
--- /dev/null
+++ b/examples/compiled/interactive_1d_geo_brush.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/compiled/interactive_1d_geo_brush.vg.json b/examples/compiled/interactive_1d_geo_brush.vg.json
new file mode 100644
index 0000000000..cc4abb825d
--- /dev/null
+++ b/examples/compiled/interactive_1d_geo_brush.vg.json
@@ -0,0 +1,346 @@
+{
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
+ "background": "white",
+ "padding": 5,
+ "width": 500,
+ "height": 300,
+ "style": "view",
+ "data": [
+ {
+ "name": "brush_store",
+ "transform": [{"type": "collect", "sort": {"field": "_vgsid_"}}],
+ "values": [{"unit": "layer_1", "_vgsid_": [45, 51.5]}]
+ },
+ {
+ "name": "source_0",
+ "url": "data/us-10m.json",
+ "format": {"type": "topojson", "feature": "states"},
+ "transform": [{"type": "identifier", "as": "_vgsid_"}]
+ },
+ {
+ "name": "source_1",
+ "url": "data/airports.csv",
+ "format": {"type": "csv"},
+ "transform": [
+ {"type": "identifier", "as": "_vgsid_"},
+ {
+ "type": "filter",
+ "expr": "datum.state !== 'PR' && datum.state !== 'VI'"
+ },
+ {
+ "type": "geojson",
+ "fields": ["longitude", "latitude"],
+ "signal": "layer_1_geojson_0"
+ },
+ {
+ "type": "geopoint",
+ "projection": "projection",
+ "fields": ["longitude", "latitude"],
+ "as": ["layer_1_x", "layer_1_y"]
+ }
+ ]
+ }
+ ],
+ "projections": [
+ {
+ "name": "projection",
+ "size": {"signal": "[width, height]"},
+ "fit": {"signal": "[data('source_0'), layer_1_geojson_0]"},
+ "type": "albersUsa"
+ }
+ ],
+ "signals": [
+ {
+ "name": "unit",
+ "value": {},
+ "on": [
+ {"events": "mousemove", "update": "isTuple(group()) ? group() : unit"}
+ ]
+ },
+ {
+ "name": "geo_interval_init_tick",
+ "value": null,
+ "on": [
+ {
+ "events": "timer{1}",
+ "update": "geo_interval_init_tick === null ? {} : geo_interval_init_tick"
+ }
+ ]
+ },
+ {
+ "name": "brush",
+ "update": "vlSelectionResolve(\"brush_store\", \"union\")"
+ },
+ {
+ "name": "projection_center",
+ "update": "invert(\"projection\", [width/2, height/2])"
+ },
+ {
+ "name": "brush_init",
+ "init": "[scale(\"projection\", [projection_center[0], 45]), scale(\"projection\", [projection_center[0], 51.5])]"
+ },
+ {
+ "name": "brush_latitude_1",
+ "init": "[brush_init[0][1], brush_init[1][1]]",
+ "on": [
+ {
+ "events": {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ "update": "[y(unit), y(unit)]"
+ },
+ {
+ "events": {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ {"source": "window", "type": "mouseup"}
+ ]
+ },
+ "update": "[brush_latitude_1[0], clamp(y(unit), 0, height)]"
+ },
+ {
+ "events": [{"source": "view", "type": "dblclick"}],
+ "update": "[0, 0]"
+ },
+ {
+ "events": {"signal": "brush_translate_delta"},
+ "update": "clampRange(panLinear(brush_translate_anchor.extent_y, brush_translate_delta.y / span(brush_translate_anchor.extent_y)), 0, height)"
+ },
+ {
+ "events": {"signal": "brush_zoom_delta"},
+ "update": "clampRange(zoomLinear(brush_latitude_1, brush_zoom_anchor.y, brush_zoom_delta), 0, height)"
+ }
+ ]
+ },
+ {
+ "name": "brush_tuple",
+ "on": [
+ {
+ "events": [
+ {"signal": "brush_latitude_1"},
+ {"signal": "geo_interval_init_tick"}
+ ],
+ "update": "vlSelectionTuples(intersect([[0, brush_latitude_1[0]],[width, brush_latitude_1[1]]], {markname: \"layer_1_marks\"}, unit.mark), {unit: \"layer_1\"})"
+ }
+ ]
+ },
+ {
+ "name": "brush_translate_anchor",
+ "value": {},
+ "on": [
+ {
+ "events": [
+ {"source": "scope", "type": "mousedown", "markname": "brush_brush"}
+ ],
+ "update": "{x: x(unit), y: y(unit), extent_y: slice(brush_latitude_1)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_translate_delta",
+ "value": {},
+ "on": [
+ {
+ "events": [
+ {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "markname": "brush_brush"
+ },
+ {"source": "window", "type": "mouseup"}
+ ]
+ }
+ ],
+ "update": "{x: brush_translate_anchor.x - x(unit), y: brush_translate_anchor.y - y(unit)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_zoom_anchor",
+ "on": [
+ {
+ "events": [
+ {
+ "source": "scope",
+ "type": "wheel",
+ "consume": true,
+ "markname": "brush_brush"
+ }
+ ],
+ "update": "{x: x(unit), y: y(unit)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_zoom_delta",
+ "on": [
+ {
+ "events": [
+ {
+ "source": "scope",
+ "type": "wheel",
+ "consume": true,
+ "markname": "brush_brush"
+ }
+ ],
+ "force": true,
+ "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
+ }
+ ]
+ },
+ {
+ "name": "brush_modify",
+ "on": [
+ {
+ "events": {"signal": "brush_tuple"},
+ "update": "modify(\"brush_store\", brush_tuple, true)"
+ }
+ ]
+ }
+ ],
+ "marks": [
+ {
+ "name": "brush_brush_bg",
+ "type": "rect",
+ "clip": true,
+ "encode": {
+ "enter": {"fill": {"value": "#333"}, "fillOpacity": {"value": 0.125}},
+ "update": {
+ "x": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "value": 0
+ },
+ {"value": 0}
+ ],
+ "y": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "signal": "brush_latitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "x2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "field": {"group": "width"}
+ },
+ {"value": 0}
+ ],
+ "y2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "signal": "brush_latitude_1[1]"
+ },
+ {"value": 0}
+ ]
+ }
+ }
+ },
+ {
+ "name": "layer_0_marks",
+ "type": "shape",
+ "style": ["geoshape"],
+ "interactive": true,
+ "from": {"data": "source_0"},
+ "encode": {
+ "update": {
+ "fill": {"value": "lightgray"},
+ "stroke": {"value": "white"},
+ "ariaRoleDescription": {"value": "geoshape"}
+ }
+ },
+ "transform": [{"type": "geoshape", "projection": "projection"}]
+ },
+ {
+ "name": "layer_1_marks",
+ "type": "symbol",
+ "style": ["circle"],
+ "interactive": true,
+ "from": {"data": "source_1"},
+ "encode": {
+ "update": {
+ "opacity": {"value": 0.7},
+ "fill": [
+ {
+ "test": "length(data(\"brush_store\")) && vlSelectionIdTest(\"brush_store\", datum)",
+ "value": "goldenrod"
+ },
+ {"value": "steelblue"}
+ ],
+ "ariaRoleDescription": {"value": "circle"},
+ "description": {
+ "signal": "\"longitude: \" + (format(datum[\"longitude\"], \"\")) + \"; latitude: \" + (format(datum[\"latitude\"], \"\"))"
+ },
+ "x": {"field": "layer_1_x"},
+ "y": {"field": "layer_1_y"},
+ "size": {"value": 10},
+ "shape": {"value": "circle"}
+ }
+ }
+ },
+ {
+ "name": "brush_brush",
+ "type": "rect",
+ "clip": true,
+ "encode": {
+ "enter": {"fill": {"value": "transparent"}},
+ "update": {
+ "x": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "value": 0
+ },
+ {"value": 0}
+ ],
+ "y": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "signal": "brush_latitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "x2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "field": {"group": "width"}
+ },
+ {"value": 0}
+ ],
+ "y2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"layer_1\"",
+ "signal": "brush_latitude_1[1]"
+ },
+ {"value": 0}
+ ],
+ "stroke": [
+ {
+ "test": "brush_latitude_1[0] !== brush_latitude_1[1]",
+ "value": "white"
+ },
+ {"value": null}
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/examples/compiled/interactive_airport_crossfilter.png b/examples/compiled/interactive_airport_crossfilter.png
new file mode 100644
index 0000000000..416f6efead
Binary files /dev/null and b/examples/compiled/interactive_airport_crossfilter.png differ
diff --git a/examples/compiled/interactive_airport_crossfilter.svg b/examples/compiled/interactive_airport_crossfilter.svg
new file mode 100644
index 0000000000..216bb5f98b
--- /dev/null
+++ b/examples/compiled/interactive_airport_crossfilter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/compiled/interactive_airport_crossfilter.vg.json b/examples/compiled/interactive_airport_crossfilter.vg.json
new file mode 100644
index 0000000000..a5840d1077
--- /dev/null
+++ b/examples/compiled/interactive_airport_crossfilter.vg.json
@@ -0,0 +1,841 @@
+{
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
+ "background": "white",
+ "padding": 5,
+ "data": [
+ {
+ "name": "brush_store",
+ "transform": [{"type": "collect", "sort": {"field": "_vgsid_"}}],
+ "values": [{"unit": "concat_0_layer_1", "_vgsid_": [30, 40]}]
+ },
+ {"name": "source_2", "url": "data/airports.csv", "format": {"type": "csv"}},
+ {
+ "name": "source_0",
+ "url": "data/us-10m.json",
+ "format": {"type": "topojson", "feature": "states"},
+ "transform": [{"type": "identifier", "as": "_vgsid_"}]
+ },
+ {
+ "name": "source_1",
+ "url": "data/flights-airport.csv",
+ "format": {"type": "csv"},
+ "transform": [
+ {"type": "identifier", "as": "_vgsid_"},
+ {
+ "type": "aggregate",
+ "groupby": ["origin"],
+ "ops": ["count"],
+ "fields": [null],
+ "as": ["routes"]
+ },
+ {"type": "identifier", "as": "_vgsid_"},
+ {
+ "type": "lookup",
+ "from": "source_2",
+ "key": "iata",
+ "fields": ["origin"],
+ "values": ["state", "latitude", "longitude"]
+ },
+ {
+ "type": "filter",
+ "expr": "datum.state !== 'PR' && datum.state !== 'VI'"
+ },
+ {
+ "type": "geojson",
+ "fields": ["longitude", "latitude"],
+ "signal": "concat_0_layer_1_geojson_0"
+ },
+ {
+ "type": "geopoint",
+ "projection": "projection",
+ "fields": ["longitude", "latitude"],
+ "as": ["concat_0_layer_1_x", "concat_0_layer_1_y"]
+ },
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"routes\"]) && isFinite(+datum[\"routes\"])"
+ }
+ ]
+ },
+ {
+ "name": "data_0",
+ "source": "source_1",
+ "transform": [
+ {
+ "type": "filter",
+ "expr": "!length(data(\"brush_store\")) || vlSelectionIdTest(\"brush_store\", datum)"
+ }
+ ]
+ },
+ {
+ "name": "source_3",
+ "url": "data/flights-2k.json",
+ "format": {"type": "json", "parse": {"date": "date"}},
+ "transform": [
+ {"type": "identifier", "as": "_vgsid_"},
+ {
+ "type": "extent",
+ "field": "delay",
+ "signal": "child__column_delay_layer_1_bin_maxbins_20_delay_extent"
+ },
+ {
+ "type": "bin",
+ "field": "delay",
+ "as": ["bin_maxbins_20_delay", "bin_maxbins_20_delay_end"],
+ "signal": "child__column_delay_layer_1_bin_maxbins_20_delay_bins",
+ "extent": {
+ "signal": "child__column_delay_layer_1_bin_maxbins_20_delay_extent"
+ },
+ "maxbins": 20
+ },
+ {
+ "type": "extent",
+ "field": "distance",
+ "signal": "child__column_distance_layer_0_bin_maxbins_20_distance_extent"
+ },
+ {
+ "type": "bin",
+ "field": "distance",
+ "as": ["bin_maxbins_20_distance", "bin_maxbins_20_distance_end"],
+ "signal": "child__column_distance_layer_0_bin_maxbins_20_distance_bins",
+ "extent": {
+ "signal": "child__column_distance_layer_0_bin_maxbins_20_distance_extent"
+ },
+ "maxbins": 20
+ },
+ {
+ "type": "lookup",
+ "from": "data_0",
+ "key": "origin",
+ "fields": ["origin"],
+ "as": ["brush"]
+ }
+ ]
+ },
+ {
+ "name": "data_1",
+ "source": "source_3",
+ "transform": [
+ {
+ "type": "filter",
+ "expr": "data('brush_store').length && isValid(datum.brush)"
+ }
+ ]
+ },
+ {
+ "name": "data_2",
+ "source": "data_1",
+ "transform": [
+ {
+ "type": "aggregate",
+ "groupby": ["bin_maxbins_20_delay", "bin_maxbins_20_delay_end"],
+ "ops": ["count"],
+ "fields": [null],
+ "as": ["__count"]
+ },
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"bin_maxbins_20_delay\"]) && isFinite(+datum[\"bin_maxbins_20_delay\"])"
+ }
+ ]
+ },
+ {
+ "name": "data_3",
+ "source": "data_1",
+ "transform": [
+ {
+ "type": "aggregate",
+ "groupby": ["bin_maxbins_20_distance", "bin_maxbins_20_distance_end"],
+ "ops": ["count"],
+ "fields": [null],
+ "as": ["__count"]
+ },
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"bin_maxbins_20_distance\"]) && isFinite(+datum[\"bin_maxbins_20_distance\"])"
+ }
+ ]
+ },
+ {
+ "name": "data_4",
+ "source": "source_3",
+ "transform": [
+ {
+ "type": "aggregate",
+ "groupby": ["bin_maxbins_20_distance", "bin_maxbins_20_distance_end"],
+ "ops": ["count"],
+ "fields": [null],
+ "as": ["__count"]
+ },
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"bin_maxbins_20_distance\"]) && isFinite(+datum[\"bin_maxbins_20_distance\"])"
+ }
+ ]
+ },
+ {
+ "name": "data_5",
+ "source": "source_3",
+ "transform": [
+ {
+ "type": "aggregate",
+ "groupby": ["bin_maxbins_20_delay", "bin_maxbins_20_delay_end"],
+ "ops": ["count"],
+ "fields": [null],
+ "as": ["__count"]
+ },
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"bin_maxbins_20_delay\"]) && isFinite(+datum[\"bin_maxbins_20_delay\"])"
+ }
+ ]
+ }
+ ],
+ "projections": [
+ {
+ "name": "projection",
+ "size": {"signal": "[concat_0_width, concat_0_height]"},
+ "fit": {"signal": "[data('source_0'), concat_0_layer_1_geojson_0]"},
+ "type": "albersUsa"
+ }
+ ],
+ "signals": [
+ {"name": "concat_0_width", "value": 500},
+ {"name": "concat_0_height", "value": 300},
+ {"name": "concat_1_childWidth", "value": 200},
+ {"name": "concat_1_childHeight", "value": 200},
+ {
+ "name": "unit",
+ "value": {},
+ "on": [
+ {"events": "mousemove", "update": "isTuple(group()) ? group() : unit"}
+ ]
+ },
+ {
+ "name": "geo_interval_init_tick",
+ "value": null,
+ "on": [
+ {
+ "events": "timer{1}",
+ "update": "geo_interval_init_tick === null ? {} : geo_interval_init_tick"
+ }
+ ]
+ },
+ {
+ "name": "brush",
+ "update": "vlSelectionResolve(\"brush_store\", \"union\")"
+ }
+ ],
+ "layout": {"padding": 20, "columns": 1, "bounds": "full", "align": "each"},
+ "marks": [
+ {
+ "type": "group",
+ "name": "concat_0_group",
+ "style": "view",
+ "encode": {
+ "update": {
+ "width": {"signal": "concat_0_width"},
+ "height": {"signal": "concat_0_height"}
+ }
+ },
+ "signals": [
+ {
+ "name": "brush_init",
+ "init": "[scale(\"projection\", [-86, 30]), scale(\"projection\", [-118, 40])]"
+ },
+ {
+ "name": "brush_latitude_1",
+ "init": "[brush_init[0][1], brush_init[1][1]]",
+ "on": [
+ {
+ "events": {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ "update": "[y(unit), y(unit)]"
+ },
+ {
+ "events": {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ {"source": "window", "type": "mouseup"}
+ ]
+ },
+ "update": "[brush_latitude_1[0], clamp(y(unit), 0, concat_0_height)]"
+ },
+ {
+ "events": [{"source": "view", "type": "dblclick"}],
+ "update": "[0, 0]"
+ },
+ {
+ "events": {"signal": "brush_translate_delta"},
+ "update": "clampRange(panLinear(brush_translate_anchor.extent_y, brush_translate_delta.y / span(brush_translate_anchor.extent_y)), 0, concat_0_height)"
+ },
+ {
+ "events": {"signal": "brush_zoom_delta"},
+ "update": "clampRange(zoomLinear(brush_latitude_1, brush_zoom_anchor.y, brush_zoom_delta), 0, concat_0_height)"
+ }
+ ]
+ },
+ {
+ "name": "brush_longitude_1",
+ "init": "[brush_init[0][0], brush_init[1][0]]",
+ "on": [
+ {
+ "events": {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ "update": "[x(unit), x(unit)]"
+ },
+ {
+ "events": {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "filter": [
+ "!event.item || event.item.mark.name !== \"brush_brush\""
+ ]
+ },
+ {"source": "window", "type": "mouseup"}
+ ]
+ },
+ "update": "[brush_longitude_1[0], clamp(x(unit), 0, concat_0_width)]"
+ },
+ {
+ "events": [{"source": "view", "type": "dblclick"}],
+ "update": "[0, 0]"
+ },
+ {
+ "events": {"signal": "brush_translate_delta"},
+ "update": "clampRange(panLinear(brush_translate_anchor.extent_x, brush_translate_delta.x / span(brush_translate_anchor.extent_x)), 0, concat_0_width)"
+ },
+ {
+ "events": {"signal": "brush_zoom_delta"},
+ "update": "clampRange(zoomLinear(brush_longitude_1, brush_zoom_anchor.x, brush_zoom_delta), 0, concat_0_width)"
+ }
+ ]
+ },
+ {
+ "name": "brush_tuple",
+ "on": [
+ {
+ "events": [
+ {"signal": "brush_latitude_1 || brush_longitude_1"},
+ {"signal": "geo_interval_init_tick"}
+ ],
+ "update": "vlSelectionTuples(intersect([[brush_longitude_1[0], brush_latitude_1[0]],[brush_longitude_1[1], brush_latitude_1[1]]], {markname: \"concat_0_layer_1_marks\"}, unit.mark), {unit: \"concat_0_layer_1\"})"
+ }
+ ]
+ },
+ {
+ "name": "brush_translate_anchor",
+ "value": {},
+ "on": [
+ {
+ "events": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "markname": "brush_brush"
+ }
+ ],
+ "update": "{x: x(unit), y: y(unit), extent_x: slice(brush_longitude_1), extent_y: slice(brush_latitude_1)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_translate_delta",
+ "value": {},
+ "on": [
+ {
+ "events": [
+ {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {
+ "source": "scope",
+ "type": "mousedown",
+ "markname": "brush_brush"
+ },
+ {"source": "window", "type": "mouseup"}
+ ]
+ }
+ ],
+ "update": "{x: brush_translate_anchor.x - x(unit), y: brush_translate_anchor.y - y(unit)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_zoom_anchor",
+ "on": [
+ {
+ "events": [
+ {
+ "source": "scope",
+ "type": "wheel",
+ "consume": true,
+ "markname": "brush_brush"
+ }
+ ],
+ "update": "{x: x(unit), y: y(unit)}"
+ }
+ ]
+ },
+ {
+ "name": "brush_zoom_delta",
+ "on": [
+ {
+ "events": [
+ {
+ "source": "scope",
+ "type": "wheel",
+ "consume": true,
+ "markname": "brush_brush"
+ }
+ ],
+ "force": true,
+ "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
+ }
+ ]
+ },
+ {
+ "name": "brush_modify",
+ "on": [
+ {
+ "events": {"signal": "brush_tuple"},
+ "update": "modify(\"brush_store\", brush_tuple, true)"
+ }
+ ]
+ }
+ ],
+ "marks": [
+ {
+ "name": "brush_brush_bg",
+ "type": "rect",
+ "clip": true,
+ "encode": {
+ "enter": {
+ "fill": {"value": "#333"},
+ "fillOpacity": {"value": 0.125}
+ },
+ "update": {
+ "x": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_longitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "y": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_latitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "x2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_longitude_1[1]"
+ },
+ {"value": 0}
+ ],
+ "y2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_latitude_1[1]"
+ },
+ {"value": 0}
+ ]
+ }
+ }
+ },
+ {
+ "name": "concat_0_layer_0_marks",
+ "type": "shape",
+ "style": ["geoshape"],
+ "interactive": true,
+ "from": {"data": "source_0"},
+ "encode": {
+ "update": {
+ "fill": {"value": "lightgray"},
+ "stroke": {"value": "white"},
+ "ariaRoleDescription": {"value": "geoshape"}
+ }
+ },
+ "transform": [{"type": "geoshape", "projection": "projection"}]
+ },
+ {
+ "name": "concat_0_layer_1_marks",
+ "type": "symbol",
+ "style": ["circle"],
+ "interactive": true,
+ "from": {"data": "source_1"},
+ "encode": {
+ "update": {
+ "opacity": {"value": 0.7},
+ "fill": [
+ {
+ "test": "length(data(\"brush_store\")) && vlSelectionIdTest(\"brush_store\", datum)",
+ "value": "goldenrod"
+ },
+ {"value": "steelblue"}
+ ],
+ "ariaRoleDescription": {"value": "circle"},
+ "description": {
+ "signal": "\"longitude: \" + (format(datum[\"longitude\"], \"\")) + \"; latitude: \" + (format(datum[\"latitude\"], \"\")) + \"; routes: \" + (format(datum[\"routes\"], \"\"))"
+ },
+ "x": {"field": "concat_0_layer_1_x"},
+ "y": {"field": "concat_0_layer_1_y"},
+ "size": {"scale": "size", "field": "routes"},
+ "shape": {"value": "circle"}
+ }
+ }
+ },
+ {
+ "name": "brush_brush",
+ "type": "rect",
+ "clip": true,
+ "encode": {
+ "enter": {"fill": {"value": "transparent"}},
+ "update": {
+ "x": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_longitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "y": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_latitude_1[0]"
+ },
+ {"value": 0}
+ ],
+ "x2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_longitude_1[1]"
+ },
+ {"value": 0}
+ ],
+ "y2": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0_layer_1\"",
+ "signal": "brush_latitude_1[1]"
+ },
+ {"value": 0}
+ ],
+ "stroke": [
+ {
+ "test": "brush_longitude_1[0] !== brush_longitude_1[1] && brush_latitude_1[0] !== brush_latitude_1[1]",
+ "value": "white"
+ },
+ {"value": null}
+ ]
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "group",
+ "name": "concat_1_group",
+ "layout": {"padding": 20, "columns": 2, "bounds": "full", "align": "all"},
+ "marks": [
+ {
+ "type": "group",
+ "name": "child__column_distance_group",
+ "style": "cell",
+ "encode": {
+ "update": {
+ "width": {"signal": "concat_1_childWidth"},
+ "height": {"signal": "concat_1_childHeight"}
+ }
+ },
+ "marks": [
+ {
+ "name": "child__column_distance_layer_0_marks",
+ "type": "rect",
+ "style": ["bar"],
+ "interactive": false,
+ "from": {"data": "data_4"},
+ "encode": {
+ "update": {
+ "fill": {"value": "steelblue"},
+ "ariaRoleDescription": {"value": "bar"},
+ "description": {
+ "signal": "\"distance (binned): \" + (!isValid(datum[\"bin_maxbins_20_distance\"]) || !isFinite(+datum[\"bin_maxbins_20_distance\"]) ? \"null\" : format(datum[\"bin_maxbins_20_distance\"], \"\") + \" – \" + format(datum[\"bin_maxbins_20_distance_end\"], \"\")) + \"; Count of Records: \" + (format(datum[\"__count\"], \"\"))"
+ },
+ "x2": {
+ "scale": "child__column_distance_x",
+ "field": "bin_maxbins_20_distance",
+ "offset": 1
+ },
+ "x": {
+ "scale": "child__column_distance_x",
+ "field": "bin_maxbins_20_distance_end"
+ },
+ "y": {
+ "scale": "child__column_distance_y",
+ "field": "__count"
+ },
+ "y2": {"scale": "child__column_distance_y", "value": 0}
+ }
+ }
+ },
+ {
+ "name": "child__column_distance_layer_1_marks",
+ "type": "rect",
+ "style": ["bar"],
+ "interactive": false,
+ "from": {"data": "data_3"},
+ "encode": {
+ "update": {
+ "fill": {"value": "goldenrod"},
+ "ariaRoleDescription": {"value": "bar"},
+ "description": {
+ "signal": "\"distance (binned): \" + (!isValid(datum[\"bin_maxbins_20_distance\"]) || !isFinite(+datum[\"bin_maxbins_20_distance\"]) ? \"null\" : format(datum[\"bin_maxbins_20_distance\"], \"\") + \" – \" + format(datum[\"bin_maxbins_20_distance_end\"], \"\")) + \"; Count of Records: \" + (format(datum[\"__count\"], \"\"))"
+ },
+ "x2": {
+ "scale": "child__column_distance_x",
+ "field": "bin_maxbins_20_distance",
+ "offset": 1
+ },
+ "x": {
+ "scale": "child__column_distance_x",
+ "field": "bin_maxbins_20_distance_end"
+ },
+ "y": {
+ "scale": "child__column_distance_y",
+ "field": "__count"
+ },
+ "y2": {"scale": "child__column_distance_y", "value": 0}
+ }
+ }
+ }
+ ],
+ "axes": [
+ {
+ "scale": "child__column_distance_y",
+ "orient": "left",
+ "gridScale": "child__column_distance_x",
+ "grid": true,
+ "tickCount": {"signal": "ceil(concat_1_childHeight/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "child__column_distance_x",
+ "orient": "bottom",
+ "grid": false,
+ "title": "distance (binned)",
+ "labelFlush": true,
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(concat_1_childWidth/10)"},
+ "zindex": 0
+ },
+ {
+ "scale": "child__column_distance_y",
+ "orient": "left",
+ "grid": false,
+ "title": "Count of Records",
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(concat_1_childHeight/40)"},
+ "zindex": 0
+ }
+ ]
+ },
+ {
+ "type": "group",
+ "name": "child__column_delay_group",
+ "style": "cell",
+ "encode": {
+ "update": {
+ "width": {"signal": "concat_1_childWidth"},
+ "height": {"signal": "concat_1_childHeight"}
+ }
+ },
+ "marks": [
+ {
+ "name": "child__column_delay_layer_0_marks",
+ "type": "rect",
+ "style": ["bar"],
+ "interactive": false,
+ "from": {"data": "data_5"},
+ "encode": {
+ "update": {
+ "fill": {"value": "steelblue"},
+ "ariaRoleDescription": {"value": "bar"},
+ "description": {
+ "signal": "\"delay (binned): \" + (!isValid(datum[\"bin_maxbins_20_delay\"]) || !isFinite(+datum[\"bin_maxbins_20_delay\"]) ? \"null\" : format(datum[\"bin_maxbins_20_delay\"], \"\") + \" – \" + format(datum[\"bin_maxbins_20_delay_end\"], \"\")) + \"; Count of Records: \" + (format(datum[\"__count\"], \"\"))"
+ },
+ "x2": {
+ "scale": "child__column_delay_x",
+ "field": "bin_maxbins_20_delay",
+ "offset": 1
+ },
+ "x": {
+ "scale": "child__column_delay_x",
+ "field": "bin_maxbins_20_delay_end"
+ },
+ "y": {"scale": "child__column_delay_y", "field": "__count"},
+ "y2": {"scale": "child__column_delay_y", "value": 0}
+ }
+ }
+ },
+ {
+ "name": "child__column_delay_layer_1_marks",
+ "type": "rect",
+ "style": ["bar"],
+ "interactive": false,
+ "from": {"data": "data_2"},
+ "encode": {
+ "update": {
+ "fill": {"value": "goldenrod"},
+ "ariaRoleDescription": {"value": "bar"},
+ "description": {
+ "signal": "\"delay (binned): \" + (!isValid(datum[\"bin_maxbins_20_delay\"]) || !isFinite(+datum[\"bin_maxbins_20_delay\"]) ? \"null\" : format(datum[\"bin_maxbins_20_delay\"], \"\") + \" – \" + format(datum[\"bin_maxbins_20_delay_end\"], \"\")) + \"; Count of Records: \" + (format(datum[\"__count\"], \"\"))"
+ },
+ "x2": {
+ "scale": "child__column_delay_x",
+ "field": "bin_maxbins_20_delay",
+ "offset": 1
+ },
+ "x": {
+ "scale": "child__column_delay_x",
+ "field": "bin_maxbins_20_delay_end"
+ },
+ "y": {"scale": "child__column_delay_y", "field": "__count"},
+ "y2": {"scale": "child__column_delay_y", "value": 0}
+ }
+ }
+ }
+ ],
+ "axes": [
+ {
+ "scale": "child__column_delay_y",
+ "orient": "left",
+ "gridScale": "child__column_delay_x",
+ "grid": true,
+ "tickCount": {"signal": "ceil(concat_1_childHeight/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "child__column_delay_x",
+ "orient": "bottom",
+ "grid": false,
+ "title": "delay (binned)",
+ "labelFlush": true,
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(concat_1_childWidth/10)"},
+ "zindex": 0
+ },
+ {
+ "scale": "child__column_delay_y",
+ "orient": "left",
+ "grid": false,
+ "title": "Count of Records",
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(concat_1_childHeight/40)"},
+ "zindex": 0
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "scales": [
+ {
+ "name": "size",
+ "type": "linear",
+ "domain": {"data": "source_1", "field": "routes"},
+ "range": [0, 500],
+ "zero": true
+ },
+ {
+ "name": "child__column_distance_x",
+ "type": "linear",
+ "domain": {
+ "signal": "[child__column_distance_layer_0_bin_maxbins_20_distance_bins.start, child__column_distance_layer_0_bin_maxbins_20_distance_bins.stop]"
+ },
+ "range": [0, {"signal": "concat_1_childWidth"}],
+ "bins": {
+ "signal": "child__column_distance_layer_0_bin_maxbins_20_distance_bins"
+ },
+ "zero": false
+ },
+ {
+ "name": "child__column_distance_y",
+ "type": "linear",
+ "domain": {
+ "fields": [
+ {"data": "data_4", "field": "__count"},
+ {"data": "data_3", "field": "__count"}
+ ]
+ },
+ "range": [{"signal": "concat_1_childHeight"}, 0],
+ "nice": true,
+ "zero": true
+ },
+ {
+ "name": "child__column_delay_x",
+ "type": "linear",
+ "domain": {
+ "signal": "[child__column_delay_layer_1_bin_maxbins_20_delay_bins.start, child__column_delay_layer_1_bin_maxbins_20_delay_bins.stop]"
+ },
+ "range": [0, {"signal": "concat_1_childWidth"}],
+ "bins": {
+ "signal": "child__column_delay_layer_1_bin_maxbins_20_delay_bins"
+ },
+ "zero": false
+ },
+ {
+ "name": "child__column_delay_y",
+ "type": "linear",
+ "domain": {
+ "fields": [
+ {"data": "data_5", "field": "__count"},
+ {"data": "data_2", "field": "__count"}
+ ]
+ },
+ "range": [{"signal": "concat_1_childHeight"}, 0],
+ "nice": true,
+ "zero": true
+ }
+ ]
+}
diff --git a/examples/compiled/interactive_global_development.vg.json b/examples/compiled/interactive_global_development.vg.json
index eaa4821ab8..a03ebdf8ae 100644
--- a/examples/compiled/interactive_global_development.vg.json
+++ b/examples/compiled/interactive_global_development.vg.json
@@ -5,7 +5,7 @@
"padding": 5,
"width": 800,
"height": 500,
- "style": "cell",
+ "style": ["view", "cell"],
"data": [
{
"name": "year_store",
diff --git a/examples/compiled/layer_arc_label.vg.json b/examples/compiled/layer_arc_label.vg.json
index 4605a56604..558ba6bfe2 100644
--- a/examples/compiled/layer_arc_label.vg.json
+++ b/examples/compiled/layer_arc_label.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 200,
"height": 200,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/layer_point_line_regression.vg.json b/examples/compiled/layer_point_line_regression.vg.json
index 3821e0a01e..1343f87402 100644
--- a/examples/compiled/layer_point_line_regression.vg.json
+++ b/examples/compiled/layer_point_line_regression.vg.json
@@ -4,7 +4,7 @@
"padding": 5,
"width": 200,
"height": 200,
- "style": "cell",
+ "style": ["cell", "view"],
"data": [
{"name": "source_0", "url": "data/movies.json", "format": {"type": "json"}},
{
diff --git a/examples/compiled/point_angle_windvector.vg.json b/examples/compiled/point_angle_windvector.vg.json
index 176d380288..5719b2b98b 100644
--- a/examples/compiled/point_angle_windvector.vg.json
+++ b/examples/compiled/point_angle_windvector.vg.json
@@ -5,6 +5,7 @@
"padding": 5,
"width": 615,
"height": 560,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/point_overlap.vg.json b/examples/compiled/point_overlap.vg.json
index 690003f29a..3097b82c0a 100644
--- a/examples/compiled/point_overlap.vg.json
+++ b/examples/compiled/point_overlap.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 20,
"height": 20,
+ "style": "view",
"data": [
{
"name": "source_0",
diff --git a/examples/compiled/test_single_point_color.vg.json b/examples/compiled/test_single_point_color.vg.json
index 45956b336d..daba568187 100644
--- a/examples/compiled/test_single_point_color.vg.json
+++ b/examples/compiled/test_single_point_color.vg.json
@@ -4,6 +4,7 @@
"padding": 5,
"width": 20,
"height": 20,
+ "style": "view",
"data": [{"name": "source_0", "values": [{"a": 2}]}],
"marks": [
{
diff --git a/examples/specs/interactive_1d_geo_brush.vl.json b/examples/specs/interactive_1d_geo_brush.vl.json
new file mode 100644
index 0000000000..d9fad3c115
--- /dev/null
+++ b/examples/specs/interactive_1d_geo_brush.vl.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "width": 500,
+ "height": 300,
+ "projection": {"type": "albersUsa"},
+ "layer": [
+ {
+ "data": {
+ "url": "data/us-10m.json",
+ "format": {"type": "topojson", "feature": "states"}
+ },
+ "mark": {"type": "geoshape", "fill": "lightgray", "stroke": "white"}
+ },
+ {
+ "data": {"url": "data/airports.csv"},
+ "transform": [
+ {"filter": "datum.state !== 'PR' && datum.state !== 'VI'"}
+ ],
+ "params": [
+ {
+ "name": "brush",
+ "select": {"type": "interval", "encodings": ["latitude"]},
+ "value": {"latitude": [45, 51.5]}
+ }
+ ],
+ "mark": "circle",
+ "encoding": {
+ "longitude": {"field": "longitude", "type": "quantitative"},
+ "latitude": {"field": "latitude", "type": "quantitative"},
+ "color": {
+ "condition": {"param": "brush", "empty": false, "value": "goldenrod"},
+ "value": "steelblue"
+ },
+ "size": {"value": 10}
+ }
+ }
+ ]
+}
diff --git a/examples/specs/interactive_airport_crossfilter.vl.json b/examples/specs/interactive_airport_crossfilter.vl.json
new file mode 100644
index 0000000000..fc2164b9d1
--- /dev/null
+++ b/examples/specs/interactive_airport_crossfilter.vl.json
@@ -0,0 +1,115 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "vconcat": [
+ {
+ "width": 500,
+ "height": 300,
+ "projection": {
+ "type": "albersUsa"
+ },
+ "layer": [
+ {
+ "data": {
+ "url": "data/us-10m.json",
+ "format": {
+ "type": "topojson",
+ "feature": "states"
+ }
+ },
+ "mark": {
+ "type": "geoshape",
+ "fill": "lightgray",
+ "stroke": "white"
+ }
+ },
+ {
+ "data": {"url": "data/flights-airport.csv"},
+ "transform": [
+ {"aggregate": [{"op": "count", "as": "routes"}], "groupby": ["origin"]},
+ {
+ "lookup": "origin",
+ "from": {
+ "data": {"url": "data/airports.csv"},
+ "key": "iata",
+ "fields": ["state", "latitude", "longitude"]
+ }
+ },
+ {"filter": "datum.state !== 'PR' && datum.state !== 'VI'"}
+ ],
+ "params": [
+ {
+ "name": "brush",
+ "select": "interval",
+ "value": {"latitude": [30, 40], "longitude": [-86, -118]}
+ }
+ ],
+ "mark": "circle",
+ "encoding": {
+ "longitude": {
+ "field": "longitude",
+ "type": "quantitative"
+ },
+ "latitude": {
+ "field": "latitude",
+ "type": "quantitative"
+ },
+ "size": {
+ "field": "routes",
+ "type": "quantitative",
+ "scale": {"rangeMax": 500},
+ "legend": null
+ },
+ "color": {
+ "condition": {
+ "param": "brush", "empty": false,
+ "value": "goldenrod"
+ },
+ "value": "steelblue"}
+ }
+ }
+ ]
+ },
+ {
+ "data": {
+ "url": "data/flights-2k.json",
+ "format": {"parse": {"date": "date"}}
+ },
+ "transform": [
+ {
+ "lookup": "origin",
+ "from": {"param": "brush", "key": "origin"}
+ }
+ ],
+ "repeat": {"column": ["distance", "delay"]},
+ "spec": {
+ "layer": [
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": {"repeat": "column"},
+ "bin": {"maxbins": 20}
+ },
+ "y": {"aggregate": "count"},
+ "color": {"value": "steelblue"}
+ }
+ },
+ {
+ "transform": [{
+ "filter": "data('brush_store').length && isValid(datum.brush)"
+ }],
+ "mark": "bar",
+ "encoding": {
+ "x": {
+ "field": {"repeat": "column"},
+ "bin": {"maxbins": 20}
+ },
+ "y": {"aggregate": "count"},
+ "color": {"value": "goldenrod"}
+ }
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/examples/specs/normalized/interactive_1d_geo_brush_normalized.vl.json b/examples/specs/normalized/interactive_1d_geo_brush_normalized.vl.json
new file mode 100644
index 0000000000..fce30944c7
--- /dev/null
+++ b/examples/specs/normalized/interactive_1d_geo_brush_normalized.vl.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "width": 500,
+ "height": 300,
+ "layer": [
+ {
+ "data": {
+ "url": "data/us-10m.json",
+ "format": {"type": "topojson", "feature": "states"}
+ },
+ "mark": {"type": "geoshape", "fill": "lightgray", "stroke": "white"},
+ "projection": {"type": "albersUsa"}
+ },
+ {
+ "data": {"url": "data/airports.csv"},
+ "params": [
+ {
+ "name": "brush",
+ "select": {"type": "interval", "encodings": ["latitude"]},
+ "value": {"latitude": [45, 51.5]}
+ }
+ ],
+ "mark": "circle",
+ "encoding": {
+ "longitude": {"field": "longitude", "type": "quantitative"},
+ "latitude": {"field": "latitude", "type": "quantitative"},
+ "color": {
+ "condition": {"param": "brush", "empty": false, "value": "goldenrod"},
+ "value": "steelblue"
+ },
+ "size": {"value": 10}
+ },
+ "transform": [{"filter": "datum.state !== 'PR' && datum.state !== 'VI'"}],
+ "projection": {"type": "albersUsa"}
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/specs/normalized/interactive_airport_crossfilter_normalized.vl.json b/examples/specs/normalized/interactive_airport_crossfilter_normalized.vl.json
new file mode 100644
index 0000000000..796b0de451
--- /dev/null
+++ b/examples/specs/normalized/interactive_airport_crossfilter_normalized.vl.json
@@ -0,0 +1,125 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "vconcat": [
+ {
+ "width": 500,
+ "height": 300,
+ "layer": [
+ {
+ "data": {
+ "url": "data/us-10m.json",
+ "format": {"type": "topojson", "feature": "states"}
+ },
+ "mark": {"type": "geoshape", "fill": "lightgray", "stroke": "white"},
+ "projection": {"type": "albersUsa"}
+ },
+ {
+ "data": {"url": "data/flights-airport.csv"},
+ "params": [
+ {
+ "name": "brush",
+ "select": "interval",
+ "value": {"latitude": [30, 40], "longitude": [-86, -118]}
+ }
+ ],
+ "mark": "circle",
+ "encoding": {
+ "longitude": {"field": "longitude", "type": "quantitative"},
+ "latitude": {"field": "latitude", "type": "quantitative"},
+ "size": {
+ "field": "routes",
+ "type": "quantitative",
+ "scale": {"rangeMax": 500},
+ "legend": null
+ },
+ "color": {
+ "condition": {
+ "param": "brush",
+ "empty": false,
+ "value": "goldenrod"
+ },
+ "value": "steelblue"
+ }
+ },
+ "transform": [
+ {
+ "aggregate": [{"op": "count", "as": "routes"}],
+ "groupby": ["origin"]
+ },
+ {
+ "lookup": "origin",
+ "from": {
+ "data": {"url": "data/airports.csv"},
+ "key": "iata",
+ "fields": ["state", "latitude", "longitude"]
+ }
+ },
+ {"filter": "datum.state !== 'PR' && datum.state !== 'VI'"}
+ ],
+ "projection": {"type": "albersUsa"}
+ }
+ ]
+ },
+ {
+ "data": {
+ "url": "data/flights-2k.json",
+ "format": {"parse": {"date": "date"}}
+ },
+ "align": "all",
+ "transform": [
+ {"lookup": "origin", "from": {"param": "brush", "key": "origin"}}
+ ],
+ "columns": 2,
+ "concat": [
+ {
+ "layer": [
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "distance", "bin": {"maxbins": 20}},
+ "y": {"aggregate": "count"},
+ "color": {"value": "steelblue"}
+ }
+ },
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "distance", "bin": {"maxbins": 20}},
+ "y": {"aggregate": "count"},
+ "color": {"value": "goldenrod"}
+ },
+ "transform": [
+ {"filter": "data('brush_store').length && isValid(datum.brush)"}
+ ]
+ }
+ ],
+ "name": "child__column_distance"
+ },
+ {
+ "layer": [
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "delay", "bin": {"maxbins": 20}},
+ "y": {"aggregate": "count"},
+ "color": {"value": "steelblue"}
+ }
+ },
+ {
+ "mark": "bar",
+ "encoding": {
+ "x": {"field": "delay", "bin": {"maxbins": 20}},
+ "y": {"aggregate": "count"},
+ "color": {"value": "goldenrod"}
+ },
+ "transform": [
+ {"filter": "data('brush_store').length && isValid(datum.brush)"}
+ ]
+ }
+ ],
+ "name": "child__column_delay"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/compile/mark/mark.ts b/src/compile/mark/mark.ts
index 119d32eb8c..009d974753 100644
--- a/src/compile/mark/mark.ts
+++ b/src/compile/mark/mark.ts
@@ -376,7 +376,7 @@ function interactiveFlag(model: UnitModel) {
}
return parentCount
? {
- interactive: unitCount > 0 || !!model.encoding.tooltip
+ interactive: unitCount > 0 || model.mark === 'geoshape' || !!model.encoding.tooltip
}
: null;
}
diff --git a/src/compile/selection/assemble.ts b/src/compile/selection/assemble.ts
index e3069c4795..1b12eb3ee3 100644
--- a/src/compile/selection/assemble.ts
+++ b/src/compile/selection/assemble.ts
@@ -5,7 +5,7 @@ import {MODIFY, STORE, unitName, VL_SELECTION_RESOLVE, TUPLE, selectionCompilers
import {dateTimeToExpr, isDateTime, dateTimeToTimestamp} from '../../datetime';
import {hasContinuousDomain} from '../../scale';
import {SelectionInit, SelectionInitInterval, ParameterExtent, SELECTION_ID} from '../../selection';
-import {keys, stringify, vals} from '../../util';
+import {keys, replacePathInField, stringify, vals} from '../../util';
import {VgData, VgDomain} from '../../vega.schema';
import {FacetModel} from '../facet';
import {LayerModel} from '../layer';
@@ -13,6 +13,13 @@ import {isUnitModel, Model} from '../model';
import {ScaleComponent} from '../scale/component';
import {UnitModel} from '../unit';
import {parseSelectionExtent} from './parse';
+import {SelectionProjection} from './project';
+
+export function assembleProjection(proj: SelectionProjection) {
+ const {signals, hasLegend, index, ...rest} = proj;
+ rest.field = replacePathInField(rest.field);
+ return rest;
+}
export function assembleInit(
init: readonly (SelectionInit | readonly SelectionInit[] | SelectionInitInterval)[] | SelectionInit,
@@ -124,10 +131,7 @@ export function assembleUnitSelectionData(model: UnitModel, data: readonly VgDat
}
if (selCmpt.init) {
- const fields = selCmpt.project.items.map(proj => {
- const {signals, ...rest} = proj;
- return rest;
- });
+ const fields = selCmpt.project.items.map(assembleProjection);
store.values = selCmpt.project.hasSelectionId
? selCmpt.init.map(i => ({unit, [SELECTION_ID]: assembleInit(i, false)[0]}))
diff --git a/src/compile/selection/index.ts b/src/compile/selection/index.ts
index 604e84a637..7c0045c245 100644
--- a/src/compile/selection/index.ts
+++ b/src/compile/selection/index.ts
@@ -1,4 +1,4 @@
-import {Binding, isString, NewSignal, Signal, Stream} from 'vega';
+import {Binding, isString, Signal, Stream} from 'vega';
import {stringValue} from 'vega-util';
import {FACET_CHANNELS} from '../../channel';
import {
@@ -45,8 +45,6 @@ export interface SelectionComponent {
bind?: 'scales' | Binding | Dict | LegendBinding;
resolve: SelectionResolution;
mark?: BrushConfig;
-
- // Transforms
project: SelectionProjectionComponent;
scales?: SelectionProjection[];
toggle?: string;
@@ -59,8 +57,8 @@ export interface SelectionComponent {
export interface SelectionCompiler {
defined: (selCmpt: SelectionComponent) => boolean;
parse?: (model: UnitModel, selCmpt: SelectionComponent, def: SelectionParameter) => void;
- signals?: (model: UnitModel, selCmpt: SelectionComponent, signals: NewSignal[]) => Signal[]; // the output can be a new or a push signal
- topLevelSignals?: (model: Model, selCmpt: SelectionComponent, signals: NewSignal[]) => NewSignal[];
+ signals?: (model: UnitModel, selCmpt: SelectionComponent, signals: Signal[]) => Signal[];
+ topLevelSignals?: (model: Model, selCmpt: SelectionComponent, signals: Signal[]) => Signal[];
modifyExpr?: (model: UnitModel, selCmpt: SelectionComponent, expr: string) => string;
marks?: (model: UnitModel, selCmpt: SelectionComponent, marks: any[]) => any[];
}
diff --git a/src/compile/selection/inputs.ts b/src/compile/selection/inputs.ts
index a434dc24e7..8e8e6c3afd 100644
--- a/src/compile/selection/inputs.ts
+++ b/src/compile/selection/inputs.ts
@@ -6,6 +6,7 @@ import nearest from './nearest';
import {TUPLE_FIELDS} from './project';
import {SelectionCompiler} from '.';
import {isLegendBinding} from '../../selection';
+import {NewSignal} from 'vega';
const inputBindings: SelectionCompiler<'point'> = {
defined: selCmpt => {
@@ -54,7 +55,7 @@ const inputBindings: SelectionCompiler<'point'> = {
signals: (model, selCmpt, signals) => {
const name = selCmpt.name;
const proj = selCmpt.project;
- const signal = signals.filter(s => s.name === name + TUPLE)[0];
+ const signal: NewSignal = signals.filter(s => s.name === name + TUPLE)[0];
const fields = name + TUPLE_FIELDS;
const values = proj.items.map(p => varName(`${name}_${p.field}`));
const valid = values.map(v => `${v} !== null`).join(' && ');
diff --git a/src/compile/selection/interval.ts b/src/compile/selection/interval.ts
index a06b9c0ce2..cda65f2b39 100644
--- a/src/compile/selection/interval.ts
+++ b/src/compile/selection/interval.ts
@@ -1,11 +1,14 @@
-import {NewSignal, OnEvent, Stream} from 'vega';
+import {isObject, NewSignal, OnEvent, SignalValue, Stream} from 'vega';
import {array, stringValue} from 'vega-util';
import {SelectionCompiler, SelectionComponent, STORE, TUPLE, unitName} from '.';
-import {ScaleChannel, X, Y} from '../../channel';
+import {GeoPositionChannel, LATITUDE, LONGITUDE, ScaleChannel, X, Y} from '../../channel';
+import {FieldName} from '../../channeldef';
import {warn} from '../../log';
import {hasContinuousDomain} from '../../scale';
-import {SelectionInitInterval} from '../../selection';
-import {keys} from '../../util';
+import {IntervalSelectionConfigWithoutType, SelectionInitInterval, SELECTION_ID} from '../../selection';
+import {keys, vals} from '../../util';
+import {LayoutSizeIndex} from '../layoutsize/component';
+import {isUnitModel} from '../model';
import {UnitModel} from '../unit';
import {assembleInit} from './assemble';
import {SelectionProjection, TUPLE_FIELDS} from './project';
@@ -13,92 +16,183 @@ import scales from './scales';
export const BRUSH = '_brush';
export const SCALE_TRIGGER = '_scale_trigger';
+export const GEO_INIT_TICK = 'geo_interval_init_tick'; // Workaround for https://github.com/vega/vega/issues/3481
+const INIT = '_init';
+const CENTER = '_center';
+
+// Separate type because the "fields" property is only used internally and we don't want to leak it to the schema.
+export type IntervalSelectionConfigWithField = IntervalSelectionConfigWithoutType & {fields?: FieldName[]};
const interval: SelectionCompiler<'interval'> = {
defined: selCmpt => selCmpt.type === 'interval',
- signals: (model, selCmpt, signals) => {
- const name = selCmpt.name;
- const fieldsSg = name + TUPLE_FIELDS;
- const hasScales = scales.defined(selCmpt);
- const init = selCmpt.init ? selCmpt.init[0] : null;
- const dataSignals: string[] = [];
- const scaleTriggers: {
- scaleName: string;
- expr: string;
- }[] = [];
-
- if (selCmpt.translate && !hasScales) {
- const filterExpr = `!event.item || event.item.mark.name !== ${stringValue(name + BRUSH)}`;
- events(selCmpt, (on: OnEvent[], evt: Stream) => {
+ parse: (model, selCmpt, selDef) => {
+ if (model.hasProjection) {
+ const def: IntervalSelectionConfigWithField = {...(isObject(selDef.select) ? selDef.select : {})};
+ def.fields = [SELECTION_ID];
+ if (!def.encodings) {
+ // Remap default x/y projection
+ def.encodings = selDef.value ? (keys(selDef.value) as GeoPositionChannel[]) : [LONGITUDE, LATITUDE];
+ }
+
+ selDef.select = {type: 'interval', ...def};
+ }
+
+ if (selCmpt.translate && !scales.defined(selCmpt)) {
+ const filterExpr = `!event.item || event.item.mark.name !== ${stringValue(selCmpt.name + BRUSH)}`;
+ for (const evt of selCmpt.events) {
+ if (!evt.between) {
+ warn(`${evt} is not an ordered event stream for interval selections.`);
+ continue;
+ }
+
const filters = array((evt.between[0].filter ??= []));
- if (!filters.includes(filterExpr)) {
+ if (filters.indexOf(filterExpr) < 0) {
filters.push(filterExpr);
}
- return on;
- });
+ }
}
+ },
+
+ signals: (model, selCmpt, signals) => {
+ const name = selCmpt.name;
+ const tupleSg = name + TUPLE;
+ const channels = vals(selCmpt.project.hasChannel).filter(p => p.channel === X || p.channel === Y);
+ const init = selCmpt.init ? selCmpt.init[0] : null;
+
+ signals.push(
+ ...channels.reduce((arr, proj) => arr.concat(channelSignals(model, selCmpt, proj, init && init[proj.index])), [])
+ );
- selCmpt.project.items.forEach((proj, i) => {
- const channel = proj.channel;
- if (channel !== X && channel !== Y) {
- warn('Interval selections only support x and y encoding channels.');
- return;
+ if (!model.hasProjection) {
+ // Proxy scale reactions to ensure that an infinite loop doesn't occur
+ // when an interval selection filter touches the scale.
+ if (!scales.defined(selCmpt)) {
+ const triggerSg = name + SCALE_TRIGGER;
+ const scaleTriggers = channels.map(proj => {
+ const channel = proj.channel as 'x' | 'y';
+ const {data: dname, visual: vname} = proj.signals;
+ const scaleName = stringValue(model.scaleName(channel));
+ const scaleType = model.getScaleComponent(channel).get('type');
+ const toNum = hasContinuousDomain(scaleType) ? '+' : '';
+ return (
+ `(!isArray(${dname}) || ` +
+ `(${toNum}invert(${scaleName}, ${vname})[0] === ${toNum}${dname}[0] && ` +
+ `${toNum}invert(${scaleName}, ${vname})[1] === ${toNum}${dname}[1]))`
+ );
+ });
+
+ if (scaleTriggers.length) {
+ signals.push({
+ name: triggerSg,
+ value: {},
+ on: [
+ {
+ events: channels.map(proj => ({scale: model.scaleName(proj.channel)})),
+ update: scaleTriggers.join(' && ') + ` ? ${triggerSg} : {}`
+ }
+ ]
+ });
+ }
}
- const val = init ? init[i] : null;
- const cs = channelSignals(model, selCmpt, proj, val);
- const dname = proj.signals.data;
- const vname = proj.signals.visual;
- const scaleName = stringValue(model.scaleName(channel));
- const scaleType = model.getScaleComponent(channel).get('type');
- const toNum = hasContinuousDomain(scaleType) ? '+' : '';
-
- signals.push(...cs);
- dataSignals.push(dname);
-
- scaleTriggers.push({
- scaleName: model.scaleName(channel),
- expr:
- `(!isArray(${dname}) || ` +
- `(${toNum}invert(${scaleName}, ${vname})[0] === ${toNum}${dname}[0] && ` +
- `${toNum}invert(${scaleName}, ${vname})[1] === ${toNum}${dname}[1]))`
+ // Only add an interval to the store if it has valid data extents. Data extents
+ // are set to null if pixel extents are equal to account for intervals over
+ // ordinal/nominal domains which, when inverted, will still produce a valid datum.
+ const dataSignals = channels.map(proj => proj.signals.data);
+ const update = `unit: ${unitName(model)}, fields: ${name + TUPLE_FIELDS}, values`;
+ return signals.concat({
+ name: tupleSg,
+ ...(init ? {init: `{${update}: ${assembleInit(init)}}`} : {}),
+ ...(dataSignals.length
+ ? {
+ on: [
+ {
+ events: [{signal: dataSignals.join(' || ')}], // Prevents double invocation, see https://github.com/vega/vega/issues/1672.
+ update: `${dataSignals.join(' && ')} ? {${update}: [${dataSignals}]} : null`
+ }
+ ]
+ }
+ : {})
});
- });
+ } else {
+ const projection = stringValue(model.projectionName());
+ const centerSg = model.projectionName() + CENTER;
+ const {x, y} = selCmpt.project.hasChannel;
+ const xvname = x && x.signals.visual;
+ const yvname = y && y.signals.visual;
+ const xinit = x ? init && init[x.index] : `${centerSg}[0]`;
+ const yinit = y ? init && init[y.index] : `${centerSg}[1]`;
+ const sizeSg = (layout: keyof LayoutSizeIndex) => model.getSizeSignalRef(layout).signal;
+ const bbox =
+ `[` +
+ `[${xvname ? xvname + '[0]' : '0'}, ${yvname ? yvname + '[0]' : '0'}],` +
+ `[${xvname ? xvname + '[1]' : sizeSg('width')}, ` +
+ `${yvname ? yvname + '[1]' : sizeSg('height')}]` +
+ `]`;
+
+ if (init) {
+ signals.unshift({
+ name: name + INIT,
+ init:
+ `[scale(${projection}, [${x ? xinit[0] : xinit}, ${y ? yinit[0] : yinit}]), ` +
+ `scale(${projection}, [${x ? xinit[1] : xinit}, ${y ? yinit[1] : yinit}])]`
+ });
- // Proxy scale reactions to ensure that an infinite loop doesn't occur
- // when an interval selection filter touches the scale.
- if (!hasScales && scaleTriggers.length) {
- signals.push({
- name: name + SCALE_TRIGGER,
- value: {},
+ if (!x || !y) {
+ // If initializing a uni-dimensional brush, use the center of the view to determine the other coord
+ const hasCenterSg = signals.find(s => s.name === centerSg);
+ if (!hasCenterSg) {
+ signals.unshift({
+ name: centerSg,
+ update: `invert(${projection}, [${sizeSg('width')}/2, ${sizeSg('height')}/2])`
+ });
+ }
+ }
+ }
+
+ const intersect = `intersect(${bbox}, {markname: ${stringValue(model.getName('marks'))}}, unit.mark)`;
+ const base = `{unit: ${unitName(model)}}`;
+ const update = `vlSelectionTuples(${intersect}, ${base})`;
+ const visualSignals = channels.map(proj => proj.signals.visual);
+
+ return signals.concat({
+ name: tupleSg,
on: [
{
- events: scaleTriggers.map(t => ({scale: t.scaleName})),
- update: `${scaleTriggers.map(t => t.expr).join(' && ')} ? ${name + SCALE_TRIGGER} : {}`
+ events: [
+ ...(visualSignals.length ? [{signal: visualSignals.join(' || ')}] : []),
+ ...(init ? [{signal: GEO_INIT_TICK}] : [])
+ ],
+ update
}
]
});
}
+ },
- // Only add an interval to the store if it has valid data extents. Data extents
- // are set to null if pixel extents are equal to account for intervals over
- // ordinal/nominal domains which, when inverted, will still produce a valid datum.
- const update = `unit: ${unitName(model)}, fields: ${fieldsSg}, values`;
- return signals.concat({
- name: name + TUPLE,
- ...(init ? {init: `{${update}: ${assembleInit(init)}}`} : {}),
- ...(dataSignals.length
- ? {
- on: [
- {
- events: [{signal: dataSignals.join(' || ')}], // Prevents double invocation, see https://github.com/vega/vega#1672.
- update: `${dataSignals.join(' && ')} ? {${update}: [${dataSignals}]} : null`
- }
- ]
- }
- : {})
- });
+ topLevelSignals: (model, selCmpt, signals) => {
+ if (isUnitModel(model) && model.hasProjection && selCmpt.init) {
+ // Workaround for https://github.com/vega/vega/issues/3481
+ // The scenegraph isn't populated on the first pulse. So we use a timer signal
+ // to re-pulse the dataflow as soon as possible. We return an object to ensure
+ // this only occurs once.
+ const hasTick = signals.filter(s => s.name === GEO_INIT_TICK);
+ if (!hasTick.length) {
+ signals.unshift({
+ name: GEO_INIT_TICK,
+ value: null,
+ on: [
+ {
+ events: 'timer{1}',
+ update: `${GEO_INIT_TICK} === null ? {} : ${GEO_INIT_TICK}`
+ }
+ ]
+ });
+ }
+ }
+
+ return signals;
},
marks: (model, selCmpt, marks) => {
@@ -192,62 +286,59 @@ function channelSignals(
model: UnitModel,
selCmpt: SelectionComponent<'interval'>,
proj: SelectionProjection,
- init?: SelectionInitInterval
+ init: SelectionInitInterval
): NewSignal[] {
+ const scaledInterval = !model.hasProjection;
const channel = proj.channel;
const vname = proj.signals.visual;
- const dname = proj.signals.data;
- const hasScales = scales.defined(selCmpt);
- const scaleName = stringValue(model.scaleName(channel));
- const scale = model.getScaleComponent(channel as ScaleChannel);
- const scaleType = scale ? scale.get('type') : undefined;
+
+ const scaleName = stringValue(scaledInterval ? model.scaleName(channel) : model.projectionName());
const scaled = (str: string) => `scale(${scaleName}, ${str})`;
+
const size = model.getSizeSignalRef(channel === X ? 'width' : 'height').signal;
const coord = `${channel}(unit)`;
-
- const on = events(selCmpt, (def: OnEvent[], evt: Stream) => {
+ const von = selCmpt.events.reduce((def: OnEvent[], evt: Stream) => {
return [
...def,
{events: evt.between[0], update: `[${coord}, ${coord}]`}, // Brush Start
{events: evt, update: `[${vname}[0], clamp(${coord}, 0, ${size})]`} // Brush End
];
- });
-
- // React to pan/zooms of continuous scales. Non-continuous scales
- // (band, point) cannot be pan/zoomed and any other changes
- // to their domains (e.g., filtering) should clear the brushes.
- on.push({
- events: {signal: selCmpt.name + SCALE_TRIGGER},
- update: hasContinuousDomain(scaleType) ? `[${scaled(`${dname}[0]`)}, ${scaled(`${dname}[1]`)}]` : `[0, 0]`
- });
-
- return hasScales
- ? [{name: dname, on: []}]
- : [
- {
- name: vname,
- ...(init ? {init: assembleInit(init, true, scaled)} : {value: []}),
- on
- },
- {
- name: dname,
- ...(init ? {init: assembleInit(init)} : {}), // Cannot be `value` as `init` may require datetime exprs.
- on: [
- {
- events: {signal: vname},
- update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleName}, ${vname})`
- }
- ]
- }
- ];
-}
+ }, []);
-function events(selCmpt: SelectionComponent<'interval'>, cb: (def: OnEvent[], evt: Stream) => OnEvent[]): OnEvent[] {
- return selCmpt.events.reduce((on, evt) => {
- if (!evt.between) {
- warn(`${evt} is not an ordered event stream for interval selections.`);
- return on;
- }
- return cb(on, evt);
- }, [] as OnEvent[]);
+ if (scaledInterval) {
+ const dname = proj.signals.data;
+ const hasScales = scales.defined(selCmpt);
+ const scale = model.getScaleComponent(channel as ScaleChannel);
+ const scaleType = scale ? scale.get('type') : undefined;
+ const vinit: SignalValue = init ? {init: assembleInit(init, true, scaled)} : {value: []};
+
+ // React to pan/zooms of continuous scales. Non-continuous scales
+ // (band, point) cannot be pan/zoomed and any other changes
+ // to their domains (e.g., filtering) should clear the brushes.
+ von.push({
+ events: {signal: selCmpt.name + SCALE_TRIGGER},
+ update: hasContinuousDomain(scaleType) ? `[${scaled(`${dname}[0]`)}, ${scaled(`${dname}[1]`)}]` : `[0, 0]`
+ });
+
+ return hasScales
+ ? [{name: dname, on: []}]
+ : [
+ {name: vname, ...vinit, on: von},
+ {
+ name: dname,
+ ...(init ? {init: assembleInit(init)} : {}), // Cannot be `value` as `init` may require datetime exprs.
+ on: [
+ {
+ events: {signal: vname},
+ update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleName}, ${vname})`
+ }
+ ]
+ }
+ ];
+ } else {
+ const initIdx = channel === X ? 0 : 1;
+ const initSg = selCmpt.name + INIT;
+ const vinit: SignalValue = init ? {init: `[${initSg}[0][${initIdx}], ${initSg}[1][${initIdx}]]`} : {value: []};
+ return [{name: vname, ...vinit, on: von}];
+ }
}
diff --git a/src/compile/selection/legends.ts b/src/compile/selection/legends.ts
index a649383b1c..ad9a51876e 100644
--- a/src/compile/selection/legends.ts
+++ b/src/compile/selection/legends.ts
@@ -1,4 +1,4 @@
-import {isObject, MergedStream, Stream} from 'vega';
+import {isObject, MergedStream, NewSignal, Stream} from 'vega';
import {parseSelector} from 'vega-event-selector';
import {array, isString} from 'vega-util';
import {disableDirectManipulation, TUPLE} from '.';
@@ -85,7 +85,7 @@ const legendBindings: SelectionCompiler<'point'> = {
signals: (model, selCmpt, signals) => {
const name = selCmpt.name;
const proj = selCmpt.project;
- const tuple = signals.find(s => s.name === name + TUPLE);
+ const tuple: NewSignal = signals.find(s => s.name === name + TUPLE);
const fields = name + TUPLE_FIELDS;
const values = proj.items.filter(p => p.hasLegend).map(p => varName(`${name}_${varName(p.field)}_legend`));
const valid = values.map(v => `${v} !== null`).join(' && ');
diff --git a/src/compile/selection/parse.ts b/src/compile/selection/parse.ts
index 554ef8fa36..d816af1505 100644
--- a/src/compile/selection/parse.ts
+++ b/src/compile/selection/parse.ts
@@ -39,7 +39,7 @@ export function parseUnitSelection(model: UnitModel, selDefs: SelectionParameter
}
if (defaults[key] === undefined || defaults[key] === true) {
- defaults[key] = cfg[key] ?? defaults[key];
+ defaults[key] = duplicate(cfg[key] ?? defaults[key]);
}
}
@@ -52,9 +52,10 @@ export function parseUnitSelection(model: UnitModel, selDefs: SelectionParameter
events: isString(defaults.on) ? parseSelector(defaults.on, 'scope') : array(duplicate(defaults.on))
} as any);
+ const def_ = duplicate(def); // defensive copy to prevent compilers from causing side effects
for (const c of selectionCompilers) {
if (c.defined(selCmpt) && c.parse) {
- c.parse(model, selCmpt, def);
+ c.parse(model, selCmpt, def_);
}
}
}
diff --git a/src/compile/selection/project.ts b/src/compile/selection/project.ts
index 706e75ea16..9cfd1ece1c 100644
--- a/src/compile/selection/project.ts
+++ b/src/compile/selection/project.ts
@@ -1,11 +1,19 @@
import {array, isObject} from 'vega-util';
-import {isSingleDefUnitChannel, ScaleChannel, SingleDefUnitChannel} from '../../channel';
+import {
+ GeoPositionChannel,
+ getPositionChannelFromLatLong,
+ isGeoPositionChannel,
+ isScaleChannel,
+ isSingleDefUnitChannel,
+ SingleDefUnitChannel
+} from '../../channel';
import * as log from '../../log';
import {hasContinuousDomain} from '../../scale';
import {PointSelectionConfig, SelectionInitIntervalMapping, SelectionInitMapping, SELECTION_ID} from '../../selection';
-import {Dict, hash, keys, replacePathInField, varName, isEmpty} from '../../util';
+import {Dict, hash, keys, varName, isEmpty} from '../../util';
import {TimeUnitComponent, TimeUnitNode} from '../data/timeunit';
import {SelectionCompiler} from '.';
+import {assembleProjection} from './assemble';
export const TUPLE_FIELDS = '_tuple_fields';
/**
@@ -22,7 +30,9 @@ export type TupleStoreType =
export interface SelectionProjection {
type: TupleStoreType;
field: string;
+ index: number;
channel?: SingleDefUnitChannel;
+ geoChannel?: GeoPositionChannel;
signals?: {data?: string; visual?: string};
hasLegend?: boolean;
}
@@ -86,10 +96,10 @@ const project: SelectionCompiler = {
(encodings || (encodings = [])).push(key as SingleDefUnitChannel);
} else {
if (type === 'interval') {
- log.warn(log.message.INTERVAL_INITIALIZED_WITH_X_Y);
+ log.warn(log.message.INTERVAL_INITIALIZED_WITH_POS);
encodings = cfg.encodings;
} else {
- (fields || (fields = [])).push(key);
+ (fields ??= []).push(key);
}
}
}
@@ -140,21 +150,28 @@ const project: SelectionCompiler = {
// Determine whether the tuple will store enumerated or ranged values.
// Interval selections store ranges for continuous scales, and enumerations otherwise.
// Single/multi selections store ranges for binned fields, and enumerations otherwise.
- let tplType: TupleStoreType = 'E';
- if (type === 'interval') {
- const scaleType = model.getScaleComponent(channel as ScaleChannel).get('type');
- if (hasContinuousDomain(scaleType)) {
- tplType = 'R';
- }
- } else if (fieldDef.bin) {
- tplType = 'R-RE';
- }
-
- const p: SelectionProjection = {field, channel, type: tplType};
+ const tplType: TupleStoreType =
+ type === 'interval' &&
+ isScaleChannel(channel) &&
+ hasContinuousDomain(model.getScaleComponent(channel).get('type'))
+ ? 'R'
+ : fieldDef.bin
+ ? 'R-RE'
+ : 'E';
+
+ const p: SelectionProjection = {field, channel, type: tplType, index: proj.items.length};
p.signals = {...signalName(p, 'data'), ...signalName(p, 'visual')};
proj.items.push((parsed[field] = p));
- proj.hasField[field] = proj.hasChannel[channel] = parsed[field];
+ proj.hasField[field] = parsed[field];
proj.hasSelectionId = proj.hasSelectionId || field === SELECTION_ID;
+
+ if (isGeoPositionChannel(channel)) {
+ p.geoChannel = channel;
+ p.channel = getPositionChannelFromLatLong(channel);
+ proj.hasChannel[p.channel] = parsed[field];
+ } else {
+ proj.hasChannel[channel] = parsed[field];
+ }
}
} else {
log.warn(log.message.cannotProjectOnChannelWithoutField(channel));
@@ -163,7 +180,7 @@ const project: SelectionCompiler = {
for (const field of fields ?? []) {
if (proj.hasField[field]) continue;
- const p: SelectionProjection = {type: 'E', field};
+ const p: SelectionProjection = {type: 'E', field, index: proj.items.length};
p.signals = {...signalName(p, 'data')};
proj.items.push(p);
proj.hasField[field] = p;
@@ -174,7 +191,9 @@ const project: SelectionCompiler = {
selCmpt.init = (init as any).map((v: SelectionInitMapping | SelectionInitIntervalMapping) => {
// Selections can be initialized either with a full object that maps projections to values
// or scalar values to smoothen the abstraction gradient from variable params to point selections.
- return proj.items.map(p => (isObject(v) ? (v[p.channel] !== undefined ? v[p.channel] : v[p.field]) : v));
+ return proj.items.map(p =>
+ isObject(v) ? (v[p.geoChannel || p.channel] !== undefined ? v[p.geoChannel || p.channel] : v[p.field]) : v
+ );
});
}
@@ -190,11 +209,7 @@ const project: SelectionCompiler = {
? allSignals
: allSignals.concat({
name,
- value: selCmpt.project.items.map(proj => {
- const {signals, hasLegend, ...rest} = proj;
- rest.field = replacePathInField(rest.field);
- return rest;
- })
+ value: selCmpt.project.items.map(assembleProjection)
});
}
};
diff --git a/src/compile/selection/scales.ts b/src/compile/selection/scales.ts
index d05ebccda6..d716bfa20e 100644
--- a/src/compile/selection/scales.ts
+++ b/src/compile/selection/scales.ts
@@ -8,6 +8,7 @@ import {UnitModel} from '../unit';
import {SelectionProjection} from './project';
import {SelectionCompiler} from '.';
import {replacePathInField} from '../../util';
+import {NewSignal} from 'vega';
const scaleBindings: SelectionCompiler<'interval'> = {
defined: selCmpt => {
@@ -52,7 +53,7 @@ const scaleBindings: SelectionCompiler<'interval'> = {
// state is captured by the top-level signals that we insert and "push
// outer" to from within the units. We need to reassemble this state into
// the top-level named signal, except no single selCmpt has a global view.
- const namedSg = signals.filter(s => s.name === selCmpt.name)[0];
+ const namedSg: NewSignal = signals.filter(s => s.name === selCmpt.name)[0];
let update = namedSg.update;
if (update.indexOf(VL_SELECTION_RESOLVE) >= 0) {
namedSg.update = `{${bound
diff --git a/src/compile/selection/translate.ts b/src/compile/selection/translate.ts
index f412e5fab5..0c2602c6e6 100644
--- a/src/compile/selection/translate.ts
+++ b/src/compile/selection/translate.ts
@@ -18,12 +18,12 @@ const translate: SelectionCompiler<'interval'> = {
signals: (model, selCmpt, signals) => {
const name = selCmpt.name;
- const hasScales = scalesCompiler.defined(selCmpt);
+ const boundScales = scalesCompiler.defined(selCmpt);
const anchor = name + ANCHOR;
const {x, y} = selCmpt.project.hasChannel;
let events = parseSelector(selCmpt.translate, 'scope');
- if (!hasScales) {
+ if (!boundScales) {
events = events.map(e => ((e.between[0].markname = name + INTERVAL_BRUSH), e));
}
@@ -36,8 +36,8 @@ const translate: SelectionCompiler<'interval'> = {
events: events.map(e => e.between[0]),
update:
'{x: x(unit), y: y(unit)' +
- (x !== undefined ? `, extent_x: ${hasScales ? domain(model, X) : `slice(${x.signals.visual})`}` : '') +
- (y !== undefined ? `, extent_y: ${hasScales ? domain(model, Y) : `slice(${y.signals.visual})`}` : '') +
+ (x !== undefined ? `, extent_x: ${boundScales ? domain(model, X) : `slice(${x.signals.visual})`}` : '') +
+ (y !== undefined ? `, extent_y: ${boundScales ? domain(model, Y) : `slice(${y.signals.visual})`}` : '') +
'}'
}
]
@@ -79,25 +79,26 @@ function onDelta(
const anchor = name + ANCHOR;
const delta = name + DELTA;
const channel = proj.channel as ScaleChannel;
- const hasScales = scalesCompiler.defined(selCmpt);
- const signal = signals.filter(s => s.name === proj.signals[hasScales ? 'data' : 'visual'])[0];
+ const boundScales = scalesCompiler.defined(selCmpt);
+ const signal = signals.filter(s => s.name === proj.signals[boundScales ? 'data' : 'visual'])[0];
const sizeSg = model.getSizeSignalRef(size).signal;
const scaleCmpt = model.getScaleComponent(channel);
- const scaleType = scaleCmpt.get('type');
- const reversed = scaleCmpt.get('reverse'); // scale parsing sets this flag for fieldDef.sort
- const sign = !hasScales ? '' : channel === X ? (reversed ? '' : '-') : reversed ? '-' : '';
+ const scaleType = scaleCmpt && scaleCmpt.get('type');
+ const reversed = scaleCmpt && scaleCmpt.get('reverse'); // scale parsing sets this flag for fieldDef.sort
+ const sign = !boundScales ? '' : channel === X ? (reversed ? '' : '-') : reversed ? '-' : '';
const extent = `${anchor}.extent_${channel}`;
- const offset = `${sign}${delta}.${channel} / ${hasScales ? `${sizeSg}` : `span(${extent})`}`;
- const panFn = !hasScales
- ? 'panLinear'
- : scaleType === 'log'
- ? 'panLog'
- : scaleType === 'symlog'
- ? 'panSymlog'
- : scaleType === 'pow'
- ? 'panPow'
- : 'panLinear';
- const arg = !hasScales
+ const offset = `${sign}${delta}.${channel} / ${boundScales ? `${sizeSg}` : `span(${extent})`}`;
+ const panFn =
+ !boundScales || !scaleCmpt
+ ? 'panLinear'
+ : scaleType === 'log'
+ ? 'panLog'
+ : scaleType === 'symlog'
+ ? 'panSymlog'
+ : scaleType === 'pow'
+ ? 'panPow'
+ : 'panLinear';
+ const arg = !boundScales
? ''
: scaleType === 'pow'
? `, ${scaleCmpt.get('exponent') ?? 1}`
@@ -108,6 +109,6 @@ function onDelta(
signal.on.push({
events: {signal: delta},
- update: hasScales ? update : `clampRange(${update}, 0, ${sizeSg})`
+ update: boundScales ? update : `clampRange(${update}, 0, ${sizeSg})`
});
}
diff --git a/src/compile/selection/zoom.ts b/src/compile/selection/zoom.ts
index accdfc3077..3b3e6ce2ec 100644
--- a/src/compile/selection/zoom.ts
+++ b/src/compile/selection/zoom.ts
@@ -19,14 +19,14 @@ const zoom: SelectionCompiler<'interval'> = {
signals: (model, selCmpt, signals) => {
const name = selCmpt.name;
- const hasScales = scalesCompiler.defined(selCmpt);
+ const boundScales = scalesCompiler.defined(selCmpt);
const delta = name + DELTA;
const {x, y} = selCmpt.project.hasChannel;
const sx = stringValue(model.scaleName(X));
const sy = stringValue(model.scaleName(Y));
let events = parseSelector(selCmpt.zoom, 'scope');
- if (!hasScales) {
+ if (!boundScales) {
events = events.map(e => ((e.markname = name + INTERVAL_BRUSH), e));
}
@@ -36,7 +36,7 @@ const zoom: SelectionCompiler<'interval'> = {
on: [
{
events,
- update: !hasScales
+ update: !boundScales
? `{x: x(unit), y: y(unit)}`
: '{' +
[sx ? `x: invert(${sx}, x(unit))` : '', sy ? `y: invert(${sy}, y(unit))` : '']
@@ -81,24 +81,25 @@ function onDelta(
) {
const name = selCmpt.name;
const channel = proj.channel as ScaleChannel;
- const hasScales = scalesCompiler.defined(selCmpt);
- const signal = signals.filter(s => s.name === proj.signals[hasScales ? 'data' : 'visual'])[0];
+ const boundScales = scalesCompiler.defined(selCmpt);
+ const signal = signals.filter(s => s.name === proj.signals[boundScales ? 'data' : 'visual'])[0];
const sizeSg = model.getSizeSignalRef(size).signal;
const scaleCmpt = model.getScaleComponent(channel);
- const scaleType = scaleCmpt.get('type');
- const base = hasScales ? domain(model, channel) : signal.name;
+ const scaleType = scaleCmpt && scaleCmpt.get('type');
+ const base = boundScales ? domain(model, channel) : signal.name;
const delta = name + DELTA;
const anchor = `${name}${ANCHOR}.${channel}`;
- const zoomFn = !hasScales
- ? 'zoomLinear'
- : scaleType === 'log'
- ? 'zoomLog'
- : scaleType === 'symlog'
- ? 'zoomSymlog'
- : scaleType === 'pow'
- ? 'zoomPow'
- : 'zoomLinear';
- const arg = !hasScales
+ const zoomFn =
+ !boundScales || !scaleCmpt
+ ? 'zoomLinear'
+ : scaleType === 'log'
+ ? 'zoomLog'
+ : scaleType === 'symlog'
+ ? 'zoomSymlog'
+ : scaleType === 'pow'
+ ? 'zoomPow'
+ : 'zoomLinear';
+ const arg = !boundScales
? ''
: scaleType === 'pow'
? `, ${scaleCmpt.get('exponent') ?? 1}`
@@ -109,6 +110,6 @@ function onDelta(
signal.on.push({
events: {signal: delta},
- update: hasScales ? update : `clampRange(${update}, 0, ${sizeSg})`
+ update: boundScales ? update : `clampRange(${update}, 0, ${sizeSg})`
});
}
diff --git a/src/compile/unit.ts b/src/compile/unit.ts
index 6ccf1384fc..c13ebde5b3 100644
--- a/src/compile/unit.ts
+++ b/src/compile/unit.ts
@@ -281,7 +281,7 @@ export class UnitModel extends ModelWithField {
if (this.encoding.x || this.encoding.y) {
return 'cell';
} else {
- return undefined;
+ return 'view';
}
}
diff --git a/src/log/message.ts b/src/log/message.ts
index c64a20c44d..adb059e2fe 100644
--- a/src/log/message.ts
+++ b/src/log/message.ts
@@ -97,7 +97,8 @@ export function noSameUnitLookup(name: string) {
export const NEEDS_SAME_SELECTION = 'The same selection must be used to override scale domains in a layered view.';
-export const INTERVAL_INITIALIZED_WITH_X_Y = 'Interval selections should be initialized using "x" and/or "y" keys.';
+export const INTERVAL_INITIALIZED_WITH_POS =
+ 'Interval selections should be initialized using "x", "y", "longitude", or "latitude" keys.';
// REPEAT
export function noSuchRepeatedValue(field: string) {
diff --git a/test-runtime/interval.test.ts b/test-runtime/interval.test.ts
index f16ff1a48c..1ece7a2dd2 100644
--- a/test-runtime/interval.test.ts
+++ b/test-runtime/interval.test.ts
@@ -1,6 +1,6 @@
import {TopLevelSpec} from '../src';
import {SelectionType} from '../src/selection';
-import {brush, embedFn, hits as hitsMaster, spec, testRenderFn, tuples} from './util';
+import {brush, embedFn, geoSpec, hits as hitsMaster, spec, testRenderFn, tuples} from './util';
import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
describe('interval selections at runtime in unit views', () => {
@@ -197,4 +197,26 @@ describe('interval selections at runtime in unit views', () => {
await testRender(`logpow_${i}`);
}
});
+
+ describe('geo-intervals', () => {
+ it('should add IDs to the store', async () => {
+ await embed(geoSpec());
+ const store = await page.evaluate(brush('drag', 1));
+ expect(store).toHaveLength(13);
+ for (const t of store) {
+ expect(t).toHaveProperty('_vgsid_');
+ }
+ await testRender(`geo_1`);
+ });
+
+ it('should respect projections', async () => {
+ await embed(geoSpec({encodings: ['longitude']}));
+ const store = await page.evaluate(brush('drag', 0));
+ expect(store).toHaveLength(20);
+ for (const t of store) {
+ expect(t).toHaveProperty('_vgsid_');
+ }
+ await testRender(`geo_0`);
+ });
+ });
});
diff --git a/test-runtime/resources/interval/translate/geo-0.svg b/test-runtime/resources/interval/translate/geo-0.svg
new file mode 100644
index 0000000000..520738812d
--- /dev/null
+++ b/test-runtime/resources/interval/translate/geo-0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/translate/geo-1.svg b/test-runtime/resources/interval/translate/geo-1.svg
new file mode 100644
index 0000000000..3d5cea9393
--- /dev/null
+++ b/test-runtime/resources/interval/translate/geo-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/translate/geo-2.svg b/test-runtime/resources/interval/translate/geo-2.svg
new file mode 100644
index 0000000000..ec05c730b5
--- /dev/null
+++ b/test-runtime/resources/interval/translate/geo-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/unit/geo_0.svg b/test-runtime/resources/interval/unit/geo_0.svg
new file mode 100644
index 0000000000..c135608c7b
--- /dev/null
+++ b/test-runtime/resources/interval/unit/geo_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/unit/geo_1.svg b/test-runtime/resources/interval/unit/geo_1.svg
new file mode 100644
index 0000000000..520738812d
--- /dev/null
+++ b/test-runtime/resources/interval/unit/geo_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/zoom/geo-0.svg b/test-runtime/resources/interval/zoom/geo-0.svg
new file mode 100644
index 0000000000..520738812d
--- /dev/null
+++ b/test-runtime/resources/interval/zoom/geo-0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/zoom/geo-1.svg b/test-runtime/resources/interval/zoom/geo-1.svg
new file mode 100644
index 0000000000..2a8697a217
--- /dev/null
+++ b/test-runtime/resources/interval/zoom/geo-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/interval/zoom/geo-2.svg b/test-runtime/resources/interval/zoom/geo-2.svg
new file mode 100644
index 0000000000..bba75f4357
--- /dev/null
+++ b/test-runtime/resources/interval/zoom/geo-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/translate.test.ts b/test-runtime/translate.test.ts
index 2f944c8fd3..ba0c7f3cd9 100644
--- a/test-runtime/translate.test.ts
+++ b/test-runtime/translate.test.ts
@@ -6,6 +6,7 @@ import {
brush,
compositeTypes,
embedFn,
+ geoSpec,
hits as hitsMaster,
parentSelector,
spec,
@@ -16,117 +17,97 @@ import {
import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
import {TopLevelSpec} from '../src';
-for (const bind of [bound, unbound]) {
- describe(`Translate ${bind} interval selections at runtime`, () => {
- let page: Page;
- let embed: (specification: TopLevelSpec) => Promise;
- let testRender: (filename: string) => Promise;
-
- beforeAll(async () => {
- page = await (global as any).__BROWSER__.newPage();
- embed = embedFn(page);
- testRender = testRenderFn(page, `interval/translate/${bind}`);
- await page.goto('http://0.0.0.0:8000/test-runtime/');
- });
+describe('Translate interval selections at runtime', () => {
+ let page: Page;
+ let embed: (specification: TopLevelSpec) => Promise;
+ let testRender: (filename: string) => Promise;
- afterAll(async () => {
- await page.close();
- });
+ beforeAll(async () => {
+ page = await (global as any).__BROWSER__.newPage();
+ embed = embedFn(page);
+ await page.goto('http://0.0.0.0:8000/test-runtime/');
+ });
- const type = 'interval';
- const hits = hitsMaster.interval;
- const binding = bind === bound ? {bind: 'scales'} : {};
-
- const assertExtent = {
- [unbound]: {
- x: ['isAbove', 'isBelow'],
- y: ['isBelow', 'isAbove']
- },
- [bound]: {
- x: ['isBelow', 'isAbove'],
- y: ['isAbove', 'isBelow']
- }
- };
-
- it('should move back-and-forth', async () => {
- for (let i = 0; i < hits.translate.length; i++) {
- await embed(spec('unit', i, {type, ...binding}));
- const drag = (await page.evaluate(brush('drag', i)))[0];
- await testRender(`${i}-0`);
- const translate = (await page.evaluate(brush('translate', i, null, bind === unbound)))[0];
- assert[assertExtent[bind].x[i]](translate.values[0][0], drag.values[0][0]);
- assert[assertExtent[bind].x[i]](translate.values[0][1], drag.values[0][1]);
- assert[assertExtent[bind].y[i]](translate.values[1][0], drag.values[1][0]);
- assert[assertExtent[bind].y[i]](translate.values[1][1], drag.values[1][1]);
- await testRender(`${i}-1`);
- }
- });
+ afterAll(async () => {
+ await page.close();
+ });
- it('should work with binned domains', async () => {
- for (let i = 0; i < hits.bins.length; i++) {
- await embed(
- spec(
- 'unit',
- 1,
- {type, ...binding, encodings: ['y']},
- {
- x: {aggregate: 'count', type: 'quantitative'},
- y: {bin: true},
- color: {value: 'steelblue', field: null, type: null}
- }
- )
- );
- const drag = (await page.evaluate(brush('bins', i)))[0];
- await testRender(`bins_${i}-0`);
- const translate = (await page.evaluate(brush('bins_translate', i, null, bind === unbound)))[0];
- assert[assertExtent[bind].y[i]](translate.values[0][0], drag.values[0][0]);
- assert[assertExtent[bind].y[i]](translate.values[0][1], drag.values[0][1]);
- await testRender(`bins_${i}-1`);
- }
- });
+ const hits = hitsMaster.interval;
- it('should work with temporal domains', async () => {
- // await jestPuppeteer.debug();
- const values = tuples.map(d => ({...d, a: new Date(2017, d.a)}));
- const toNumber = (a: any) => a[0].values[0].map((d: any) => +d);
-
- for (let i = 0; i < hits.translate.length; i++) {
- await embed(spec('unit', i, {type, ...binding, encodings: ['x']}, {values, x: {type: 'temporal'}}));
- const drag = toNumber(await page.evaluate(brush('drag', i)));
- await testRender(`temporal_${i}-0`);
- const translate = toNumber(await page.evaluate(brush('translate', i, null, bind === unbound)));
- assert[assertExtent[bind].x[i]](translate[0], drag[0]);
- assert[assertExtent[bind].x[i]](translate[1], drag[1]);
- await testRender(`temporal_${i}-1`);
- }
- });
+ for (const bind of [bound, unbound]) {
+ describe(`${bind} intervals`, () => {
+ beforeAll(() => {
+ testRender = testRenderFn(page, `interval/translate/${bind}`);
+ });
- it('should work with log/pow scales', async () => {
- for (let i = 0; i < hits.translate.length; i++) {
- await embed(
- spec(
- 'unit',
- i,
- {type, ...binding},
- {
- x: {scale: {type: 'pow', exponent: 1.5}},
- y: {scale: {type: 'log'}}
- }
- )
- );
- const drag = (await page.evaluate(brush('drag', i)))[0];
- await testRender(`logpow_${i}-0`);
- const translate = (await page.evaluate(brush('translate', i, null, bind === unbound)))[0];
- assert[assertExtent[bind].x[i]](translate.values[0][0], drag.values[0][0]);
- assert[assertExtent[bind].x[i]](translate.values[0][1], drag.values[0][1]);
- assert[assertExtent[bind].y[i]](translate.values[1][0], drag.values[1][0]);
- assert[assertExtent[bind].y[i]](translate.values[1][1], drag.values[1][1]);
- await testRender(`logpow_${i}-1`);
- }
- });
+ const type = 'interval';
+ const binding = bind === bound ? {bind: 'scales'} : {};
- if (bind === unbound) {
- it('should work with ordinal/nominal domains', async () => {
+ const assertExtent = {
+ [unbound]: {
+ x: ['isAbove', 'isBelow'],
+ y: ['isBelow', 'isAbove']
+ },
+ [bound]: {
+ x: ['isBelow', 'isAbove'],
+ y: ['isAbove', 'isBelow']
+ }
+ };
+
+ it('should move back-and-forth', async () => {
+ for (let i = 0; i < hits.translate.length; i++) {
+ await embed(spec('unit', i, {type, ...binding}));
+ const drag = (await page.evaluate(brush('drag', i)))[0];
+ await testRender(`${i}-0`);
+ const translate = (await page.evaluate(brush('translate', i, null, bind === unbound)))[0];
+ assert[assertExtent[bind].x[i]](translate.values[0][0], drag.values[0][0]);
+ assert[assertExtent[bind].x[i]](translate.values[0][1], drag.values[0][1]);
+ assert[assertExtent[bind].y[i]](translate.values[1][0], drag.values[1][0]);
+ assert[assertExtent[bind].y[i]](translate.values[1][1], drag.values[1][1]);
+ await testRender(`${i}-1`);
+ }
+ });
+
+ it('should work with binned domains', async () => {
+ for (let i = 0; i < hits.bins.length; i++) {
+ await embed(
+ spec(
+ 'unit',
+ 1,
+ {type, ...binding, encodings: ['y']},
+ {
+ x: {aggregate: 'count', type: 'quantitative'},
+ y: {bin: true},
+ color: {value: 'steelblue', field: null, type: null}
+ }
+ )
+ );
+ const drag = (await page.evaluate(brush('bins', i)))[0];
+ await testRender(`bins_${i}-0`);
+ const translate = (await page.evaluate(brush('bins_translate', i, null, bind === unbound)))[0];
+ assert[assertExtent[bind].y[i]](translate.values[0][0], drag.values[0][0]);
+ assert[assertExtent[bind].y[i]](translate.values[0][1], drag.values[0][1]);
+ await testRender(`bins_${i}-1`);
+ }
+ });
+
+ it('should work with temporal domains', async () => {
+ // await jestPuppeteer.debug();
+ const values = tuples.map(d => ({...d, a: new Date(2017, d.a)}));
+ const toNumber = (a: any) => a[0].values[0].map((d: any) => +d);
+
+ for (let i = 0; i < hits.translate.length; i++) {
+ await embed(spec('unit', i, {type, ...binding, encodings: ['x']}, {values, x: {type: 'temporal'}}));
+ const drag = toNumber(await page.evaluate(brush('drag', i)));
+ await testRender(`temporal_${i}-0`);
+ const translate = toNumber(await page.evaluate(brush('translate', i, null, bind === unbound)));
+ assert[assertExtent[bind].x[i]](translate[0], drag[0]);
+ assert[assertExtent[bind].x[i]](translate[1], drag[1]);
+ await testRender(`temporal_${i}-1`);
+ }
+ });
+
+ it('should work with log/pow scales', async () => {
for (let i = 0; i < hits.translate.length; i++) {
await embed(
spec(
@@ -134,48 +115,89 @@ for (const bind of [bound, unbound]) {
i,
{type, ...binding},
{
- x: {type: 'ordinal'},
- y: {type: 'nominal'}
+ x: {scale: {type: 'pow', exponent: 1.5}},
+ y: {scale: {type: 'log'}}
}
)
);
const drag = (await page.evaluate(brush('drag', i)))[0];
- await testRender(`ord_${i}-0`);
- const translate = (await page.evaluate(brush('translate', i, null, true)))[0];
+ await testRender(`logpow_${i}-0`);
+ const translate = (await page.evaluate(brush('translate', i, null, bind === unbound)))[0];
assert[assertExtent[bind].x[i]](translate.values[0][0], drag.values[0][0]);
assert[assertExtent[bind].x[i]](translate.values[0][1], drag.values[0][1]);
assert[assertExtent[bind].y[i]](translate.values[1][0], drag.values[1][0]);
assert[assertExtent[bind].y[i]](translate.values[1][1], drag.values[1][1]);
- await testRender(`ord_${i}-1`);
+ await testRender(`logpow_${i}-1`);
}
});
- } else {
- for (const specType of compositeTypes) {
- const assertExtents = {
- repeat: {
- x: ['isBelow', 'isBelow', 'isBelow'],
- y: ['isAbove', 'isAbove', 'isAbove']
- },
- facet: {
- x: ['isBelow', 'isBelow', 'isBelow'],
- y: ['isBelow', 'isAbove', 'isBelow']
- }
- };
- it(`should work with shared scales in ${specType} views`, async () => {
- for (let i = 0; i < hits[specType].length; i++) {
- await embed(spec(specType, 0, {type, ...binding}, {resolve: {scale: {x: 'shared', y: 'shared'}}}));
- const parent = parentSelector(specType, i);
- const xscale = await page.evaluate('view._runtime.scales.x.value.domain()');
- const yscale = await page.evaluate('view._runtime.scales.y.value.domain()');
- const drag = (await page.evaluate(brush(specType, i, parent)))[0];
- assert[assertExtents[specType].x[i]](drag.values[0][0], xscale[0], `iter: ${i}`);
- assert[assertExtents[specType].x[i]](drag.values[0][1], xscale[1], `iter: ${i}`);
- assert[assertExtents[specType].y[i]](drag.values[1][0], yscale[0], `iter: ${i}`);
- assert[assertExtents[specType].y[i]](drag.values[1][1], yscale[1], `iter: ${i}`);
- await testRender(`${specType}_${i}`);
+
+ if (bind === unbound) {
+ it('should work with ordinal/nominal domains', async () => {
+ for (let i = 0; i < hits.translate.length; i++) {
+ await embed(
+ spec(
+ 'unit',
+ i,
+ {type, ...binding},
+ {
+ x: {type: 'ordinal'},
+ y: {type: 'nominal'}
+ }
+ )
+ );
+ const drag = (await page.evaluate(brush('drag', i)))[0];
+ await testRender(`ord_${i}-0`);
+ const translate = (await page.evaluate(brush('translate', i, null, true)))[0];
+ assert[assertExtent[bind].x[i]](translate.values[0][0], drag.values[0][0]);
+ assert[assertExtent[bind].x[i]](translate.values[0][1], drag.values[0][1]);
+ assert[assertExtent[bind].y[i]](translate.values[1][0], drag.values[1][0]);
+ assert[assertExtent[bind].y[i]](translate.values[1][1], drag.values[1][1]);
+ await testRender(`ord_${i}-1`);
}
});
+ } else {
+ for (const specType of compositeTypes) {
+ const assertExtents = {
+ repeat: {
+ x: ['isBelow', 'isBelow', 'isBelow'],
+ y: ['isAbove', 'isAbove', 'isAbove']
+ },
+ facet: {
+ x: ['isBelow', 'isBelow', 'isBelow'],
+ y: ['isBelow', 'isAbove', 'isBelow']
+ }
+ };
+ it(`should work with shared scales in ${specType} views`, async () => {
+ for (let i = 0; i < hits[specType].length; i++) {
+ await embed(spec(specType, 0, {type, ...binding}, {resolve: {scale: {x: 'shared', y: 'shared'}}}));
+ const parent = parentSelector(specType, i);
+ const xscale = await page.evaluate('view._runtime.scales.x.value.domain()');
+ const yscale = await page.evaluate('view._runtime.scales.y.value.domain()');
+ const drag = (await page.evaluate(brush(specType, i, parent)))[0];
+ assert[assertExtents[specType].x[i]](drag.values[0][0], xscale[0], `iter: ${i}`);
+ assert[assertExtents[specType].x[i]](drag.values[0][1], xscale[1], `iter: ${i}`);
+ assert[assertExtents[specType].y[i]](drag.values[1][0], yscale[0], `iter: ${i}`);
+ assert[assertExtents[specType].y[i]](drag.values[1][1], yscale[1], `iter: ${i}`);
+ await testRender(`${specType}_${i}`);
+ }
+ });
+ }
}
+ });
+ }
+
+ it('should work with geo intervals', async () => {
+ testRender = testRenderFn(page, `interval/translate`);
+
+ await embed(geoSpec());
+ const drag = await page.evaluate(brush('drag', 1));
+ expect(drag).toHaveLength(13);
+ await testRender(`geo-0`);
+
+ for (let i = 0; i < hits.translate.length; i++) {
+ const translate = await page.evaluate(brush('translate', i, null, true));
+ expect(translate.length).toBeGreaterThan(0);
+ await testRender(`geo-${i + 1}`);
}
});
-}
+});
diff --git a/test-runtime/util.ts b/test-runtime/util.ts
index 192974f3a5..62c30db31a 100644
--- a/test-runtime/util.ts
+++ b/test-runtime/util.ts
@@ -3,7 +3,7 @@ import {sync as mkdirp} from 'mkdirp';
import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
import {promisify} from 'util';
import {stringValue} from 'vega-util';
-import {SelectionResolution, SelectionType} from '../src/selection';
+import {IntervalSelectionConfigWithoutType, SelectionResolution, SelectionType} from '../src/selection';
import {NormalizedLayerSpec, NormalizedUnitSpec, TopLevelSpec} from '../src/spec';
const generate = process.env.VL_GENERATE_TESTS;
@@ -107,6 +107,14 @@ export const hits = {
}
};
+const config = {
+ // reduce changes in generated SVGs
+ aria: false,
+
+ // A lot of magic numbers in this file use the old step = 21
+ view: {discreteWidth: {step: 21}, discreteHeight: {step: 21}}
+};
+
function base(iter: number, selDef: any, opts: any = {}): NormalizedUnitSpec | NormalizedLayerSpec {
const data = {values: opts.values ?? tuples};
const x = {field: 'a', type: 'quantitative', ...opts.x};
@@ -160,13 +168,6 @@ function base(iter: number, selDef: any, opts: any = {}): NormalizedUnitSpec | N
export function spec(compose: ComposeType, iter: number, sel: any, opts: any = {}): TopLevelSpec {
const {data, ...specification} = base(iter, sel, opts);
const resolve = opts.resolve;
- const config = {
- // reduce changes in generated SVGs
- aria: false,
-
- // A lot of magic numbers in this file use the old step = 21
- view: {discreteWidth: {step: 21}, discreteHeight: {step: 21}}
- };
switch (compose) {
case 'unit':
return {data, ...specification, config} as TopLevelSpec;
@@ -189,6 +190,65 @@ export function spec(compose: ComposeType, iter: number, sel: any, opts: any = {
}
}
+export function geoSpec(selDef?: IntervalSelectionConfigWithoutType): TopLevelSpec {
+ return {
+ width: 500,
+ height: 300,
+ projection: {type: 'albersUsa'},
+ config,
+ data: {
+ values: [
+ {latitude: 31.95376472, longitude: -89.23450472},
+ {latitude: 30.68586111, longitude: -95.01792778},
+ {latitude: 38.94574889, longitude: -104.5698933},
+ {latitude: 42.74134667, longitude: -78.05208056},
+ {latitude: 30.6880125, longitude: -81.90594389},
+ {latitude: 34.49166667, longitude: -88.20111111},
+ {latitude: 32.85048667, longitude: -86.61145333},
+ {latitude: 43.08751, longitude: -88.17786917},
+ {latitude: 40.67331278, longitude: -80.64140639},
+ {latitude: 40.44725889, longitude: -92.22696056},
+ {latitude: 33.93011222, longitude: -89.34285194},
+ {latitude: 46.88384889, longitude: -96.35089861},
+ {latitude: 41.51961917, longitude: -87.40109333},
+ {latitude: 31.42127556, longitude: -97.79696778},
+ {latitude: 39.60416667, longitude: -116.0050597},
+ {latitude: 32.46047167, longitude: -85.68003611},
+ {latitude: 41.98934083, longitude: -88.10124278},
+ {latitude: 48.88434111, longitude: -99.62087694},
+ {latitude: 33.53456583, longitude: -89.31256917},
+ {latitude: 41.43156583, longitude: -74.39191722},
+ {latitude: 41.97602222, longitude: -114.6580911},
+ {latitude: 41.30716667, longitude: -85.06433333},
+ {latitude: 32.52883861, longitude: -94.97174556},
+ {latitude: 42.57450861, longitude: -84.81143139},
+ {latitude: 41.11668056, longitude: -98.05033639},
+ {latitude: 32.52943944, longitude: -86.32822139},
+ {latitude: 48.30079861, longitude: -102.4063514},
+ {latitude: 40.65138528, longitude: -98.07978667},
+ {latitude: 32.76124611, longitude: -89.53007139},
+ {latitude: 32.11931306, longitude: -88.1274625}
+ ]
+ },
+ mark: 'circle',
+ params: [
+ {
+ name: 'sel',
+ select: {type: 'interval', ...selDef}
+ }
+ ],
+ encoding: {
+ longitude: {field: 'longitude', type: 'quantitative'},
+ latitude: {field: 'latitude', type: 'quantitative'},
+ color: {
+ condition: {param: 'sel', empty: false, value: 'goldenrod'},
+ value: 'steelblue'
+ },
+ size: {value: 10}
+ }
+ };
+}
+
export function unitNameRegex(specType: ComposeType, idx: number) {
const name = UNIT_NAMES[specType][idx].replace('child_', '');
return new RegExp(`child(.*?)_${name}`);
diff --git a/test-runtime/zoom.test.ts b/test-runtime/zoom.test.ts
index 4ae6169ef4..f823ec3ba0 100644
--- a/test-runtime/zoom.test.ts
+++ b/test-runtime/zoom.test.ts
@@ -1,7 +1,18 @@
/* eslint-disable jest/expect-expect */
import {assert} from 'chai';
-import {bound, brush, compositeTypes, embedFn, parentSelector, spec, testRenderFn, tuples, unbound} from './util';
+import {
+ bound,
+ brush,
+ compositeTypes,
+ embedFn,
+ geoSpec,
+ parentSelector,
+ spec,
+ testRenderFn,
+ tuples,
+ unbound
+} from './util';
const hits = {
zoom: [9, 23],
bins: [8, 2]
@@ -16,140 +27,115 @@ function zoom(key: string, idx: number, direction: InOut, parent?: string, targe
return `zoom(${hits[key][idx]}, ${delta}, ${parent}, ${targetBrush})`;
}
-const cmp = (a: number, b: number) => a - b;
+describe('Zoom interval selections at runtime', () => {
+ let page: Page;
+ let embed: (specification: TopLevelSpec) => Promise;
+ let testRender: (filename: string) => Promise;
-for (const bind of [bound, unbound]) {
- describe(`Zoom ${bind} interval selections at runtime`, () => {
- let page: Page;
- let embed: (specification: TopLevelSpec) => Promise;
- let testRender: (filename: string) => Promise;
-
- beforeAll(async () => {
- page = await (global as any).__BROWSER__.newPage();
- embed = embedFn(page);
- testRender = testRenderFn(page, `interval/zoom/${bind}`);
- await page.goto('http://0.0.0.0:8000/test-runtime/');
- });
-
- afterAll(async () => {
- await page.close();
- });
+ beforeAll(async () => {
+ page = await (global as any).__BROWSER__.newPage();
+ embed = embedFn(page);
+ await page.goto('http://0.0.0.0:8000/test-runtime/');
+ });
- const type = 'interval';
- const binding = bind === bound ? {bind: 'scales'} : {};
+ afterAll(async () => {
+ await page.close();
+ });
- const assertExtent = {
- in: ['isAtLeast', 'isAtMost'],
- out: ['isAtMost', 'isAtLeast']
- };
+ for (const bind of [bound, unbound]) {
+ describe(`Zoom ${bind} interval selections at runtime`, () => {
+ beforeAll(() => {
+ testRender = testRenderFn(page, `interval/zoom/${bind}`);
+ });
- async function setup(brushKey: string, idx: number, encodings: string[], parent?: string) {
- const inOut: InOut = idx % 2 ? 'out' : 'in';
- let xold: number[];
- let yold: number[];
+ const type = 'interval';
+ const binding = bind === bound ? {bind: 'scales'} : {};
+ const cmp = (a: number, b: number) => a - b;
+
+ const assertExtent = {
+ in: ['isAtLeast', 'isAtMost'],
+ out: ['isAtMost', 'isAtLeast']
+ };
+
+ async function setup(brushKey: string, idx: number, encodings: string[], parent?: string) {
+ const inOut: InOut = idx % 2 ? 'out' : 'in';
+ let xold: number[];
+ let yold: number[];
+
+ if (bind === unbound) {
+ const drag = (await page.evaluate(brush(brushKey, idx, parent)))[0];
+ xold = drag.values[0].sort(cmp);
+ yold = encodings.includes('y') ? drag.values[encodings.indexOf('x') + 1].sort(cmp) : null;
+ } else {
+ xold = JSON.parse((await page.evaluate('JSON.stringify(view._runtime.scales.x.value.domain())')) as string);
+ yold = (await page.evaluate('view._runtime.scales.y.value.domain()')) as number[];
+ }
- if (bind === unbound) {
- const drag = (await page.evaluate(brush(brushKey, idx, parent)))[0];
- xold = drag.values[0].sort(cmp);
- yold = encodings.includes('y') ? drag.values[encodings.indexOf('x') + 1].sort(cmp) : null;
- } else {
- xold = JSON.parse((await page.evaluate('JSON.stringify(view._runtime.scales.x.value.domain())')) as string);
- yold = (await page.evaluate('view._runtime.scales.y.value.domain()')) as number[];
+ return {inOut, xold, yold};
}
- return {inOut, xold, yold};
- }
+ it('should zoom in and out', async () => {
+ for (let i = 0; i < hits.zoom.length; i++) {
+ await embed(spec('unit', i, {type, ...binding}));
+ const {inOut, xold, yold} = await setup('drag', i, ['x', 'y']);
+ await testRender(`${inOut}-0`);
- it('should zoom in and out', async () => {
- for (let i = 0; i < hits.zoom.length; i++) {
- await embed(spec('unit', i, {type, ...binding}));
- const {inOut, xold, yold} = await setup('drag', i, ['x', 'y']);
- await testRender(`${inOut}-0`);
-
- const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
- const xnew = zoomed.values[0].sort(cmp);
- const ynew = zoomed.values[1].sort(cmp);
- await testRender(`${inOut}-1`);
- assert[assertExtent[inOut][0]](xnew[0], xold[0]);
- assert[assertExtent[inOut][1]](xnew[1], xold[1]);
- assert[assertExtent[inOut][0]](ynew[0], yold[0]);
- assert[assertExtent[inOut][1]](ynew[1], yold[1]);
- }
- });
+ const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
+ const xnew = zoomed.values[0].sort(cmp);
+ const ynew = zoomed.values[1].sort(cmp);
+ await testRender(`${inOut}-1`);
+ assert[assertExtent[inOut][0]](xnew[0], xold[0]);
+ assert[assertExtent[inOut][1]](xnew[1], xold[1]);
+ assert[assertExtent[inOut][0]](ynew[0], yold[0]);
+ assert[assertExtent[inOut][1]](ynew[1], yold[1]);
+ }
+ });
- it('should work with binned domains', async () => {
- for (let i = 0; i < hits.bins.length; i++) {
- const encodings = ['y'];
- await embed(
- spec(
- 'unit',
- 1,
- {type, ...binding, encodings},
- {
- x: {aggregate: 'count', type: 'quantitative'},
- y: {bin: true},
- color: {value: 'steelblue', field: null, type: null}
- }
- )
- );
+ it('should work with binned domains', async () => {
+ for (let i = 0; i < hits.bins.length; i++) {
+ const encodings = ['y'];
+ await embed(
+ spec(
+ 'unit',
+ 1,
+ {type, ...binding, encodings},
+ {
+ x: {aggregate: 'count', type: 'quantitative'},
+ y: {bin: true},
+ color: {value: 'steelblue', field: null, type: null}
+ }
+ )
+ );
- const {inOut, yold} = await setup('bins', i, encodings);
- await testRender(`bins_${inOut}-0`);
+ const {inOut, yold} = await setup('bins', i, encodings);
+ await testRender(`bins_${inOut}-0`);
- const zoomed = (await page.evaluate(zoom('bins', i, inOut, null, bind === unbound)))[0];
- const ynew = zoomed.values[0].sort(cmp);
- assert[assertExtent[inOut][0]](ynew[0], yold[0]);
- assert[assertExtent[inOut][1]](ynew[1], yold[1]);
- await testRender(`bins_${inOut}-1`);
- }
- });
-
- it('should work with temporal domains', async () => {
- const values = tuples.map(d => ({...d, a: new Date(2017, d.a)}));
- const encodings = ['x'];
+ const zoomed = (await page.evaluate(zoom('bins', i, inOut, null, bind === unbound)))[0];
+ const ynew = zoomed.values[0].sort(cmp);
+ assert[assertExtent[inOut][0]](ynew[0], yold[0]);
+ assert[assertExtent[inOut][1]](ynew[1], yold[1]);
+ await testRender(`bins_${inOut}-1`);
+ }
+ });
- for (let i = 0; i < hits.zoom.length; i++) {
- await embed(spec('unit', i, {type, ...binding, encodings}, {values, x: {type: 'temporal'}}));
- const {inOut, xold} = await setup('drag', i, encodings);
- await testRender(`temporal_${inOut}-0`);
+ it('should work with temporal domains', async () => {
+ const values = tuples.map(d => ({...d, a: new Date(2017, d.a)}));
+ const encodings = ['x'];
- const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
- const xnew = zoomed.values[0].sort(cmp);
- assert[assertExtent[inOut][0]](+xnew[0], +new Date(xold[0]));
- assert[assertExtent[inOut][1]](+xnew[1], +new Date(xold[1]));
- await testRender(`temporal_${inOut}-1`);
- }
- });
+ for (let i = 0; i < hits.zoom.length; i++) {
+ await embed(spec('unit', i, {type, ...binding, encodings}, {values, x: {type: 'temporal'}}));
+ const {inOut, xold} = await setup('drag', i, encodings);
+ await testRender(`temporal_${inOut}-0`);
- it('should work with log/pow scales', async () => {
- for (let i = 0; i < hits.zoom.length; i++) {
- await embed(
- spec(
- 'unit',
- i,
- {type, ...binding},
- {
- x: {scale: {type: 'pow', exponent: 1.5}},
- y: {scale: {type: 'log'}}
- }
- )
- );
- const {inOut, xold, yold} = await setup('drag', i, ['x', 'y']);
- await testRender(`logpow_${inOut}-0`);
-
- const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
- const xnew = zoomed.values[0].sort(cmp);
- const ynew = zoomed.values[1].sort(cmp);
- assert[assertExtent[inOut][0]](xnew[0], xold[0]);
- assert[assertExtent[inOut][1]](xnew[1], xold[1]);
- assert[assertExtent[inOut][0]](ynew[0], yold[0]);
- assert[assertExtent[inOut][1]](ynew[1], yold[1]);
- await testRender(`logpow_${inOut}-1`);
- }
- });
+ const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
+ const xnew = zoomed.values[0].sort(cmp);
+ assert[assertExtent[inOut][0]](+xnew[0], +new Date(xold[0]));
+ assert[assertExtent[inOut][1]](+xnew[1], +new Date(xold[1]));
+ await testRender(`temporal_${inOut}-1`);
+ }
+ });
- if (bind === unbound) {
- it('should work with ordinal/nominal domains', async () => {
+ it('should work with log/pow scales', async () => {
for (let i = 0; i < hits.zoom.length; i++) {
await embed(
spec(
@@ -157,47 +143,91 @@ for (const bind of [bound, unbound]) {
i,
{type, ...binding},
{
- x: {type: 'ordinal'},
- y: {type: 'nominal'}
+ x: {scale: {type: 'pow', exponent: 1.5}},
+ y: {scale: {type: 'log'}}
}
)
);
const {inOut, xold, yold} = await setup('drag', i, ['x', 'y']);
- await testRender(`ord_${inOut}-0`);
+ await testRender(`logpow_${inOut}-0`);
const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
const xnew = zoomed.values[0].sort(cmp);
const ynew = zoomed.values[1].sort(cmp);
-
- if (inOut === 'in') {
- expect(xnew.length).toBeLessThanOrEqual(xold.length);
- expect(ynew.length).toBeLessThanOrEqual(yold.length);
- } else {
- expect(xnew.length).toBeGreaterThanOrEqual(xold.length);
- expect(ynew.length).toBeGreaterThanOrEqual(yold.length);
- }
-
- await testRender(`ord_${inOut}-1`);
+ assert[assertExtent[inOut][0]](xnew[0], xold[0]);
+ assert[assertExtent[inOut][1]](xnew[1], xold[1]);
+ assert[assertExtent[inOut][0]](ynew[0], yold[0]);
+ assert[assertExtent[inOut][1]](ynew[1], yold[1]);
+ await testRender(`logpow_${inOut}-1`);
}
});
- } else {
- for (const specType of compositeTypes) {
- it(`should work with shared scales in ${specType} views`, async () => {
- for (let i = 0; i < hits.bins.length; i++) {
- await embed(spec(specType, 0, {type, ...binding}, {resolve: {scale: {x: 'shared', y: 'shared'}}}));
- const parent = parentSelector(specType, i);
- const {inOut, xold, yold} = await setup(specType, i, ['x', 'y'], parent);
- const zoomed = (await page.evaluate(zoom('bins', i, inOut, null, bind === unbound)))[0];
+
+ if (bind === unbound) {
+ it('should work with ordinal/nominal domains', async () => {
+ for (let i = 0; i < hits.zoom.length; i++) {
+ await embed(
+ spec(
+ 'unit',
+ i,
+ {type, ...binding},
+ {
+ x: {type: 'ordinal'},
+ y: {type: 'nominal'}
+ }
+ )
+ );
+ const {inOut, xold, yold} = await setup('drag', i, ['x', 'y']);
+ await testRender(`ord_${inOut}-0`);
+
+ const zoomed = (await page.evaluate(zoom('zoom', i, inOut, null, bind === unbound)))[0];
const xnew = zoomed.values[0].sort(cmp);
const ynew = zoomed.values[1].sort(cmp);
- assert[assertExtent[inOut][0]](xnew[0], xold[0]);
- assert[assertExtent[inOut][1]](xnew[1], xold[1]);
- assert[assertExtent[inOut][0]](ynew[0], yold[0]);
- assert[assertExtent[inOut][1]](ynew[1], yold[1]);
- await testRender(`${specType}_${inOut}`);
+
+ if (inOut === 'in') {
+ expect(xnew.length).toBeLessThanOrEqual(xold.length);
+ expect(ynew.length).toBeLessThanOrEqual(yold.length);
+ } else {
+ expect(xnew.length).toBeGreaterThanOrEqual(xold.length);
+ expect(ynew.length).toBeGreaterThanOrEqual(yold.length);
+ }
+
+ await testRender(`ord_${inOut}-1`);
}
});
+ } else {
+ for (const specType of compositeTypes) {
+ it(`should work with shared scales in ${specType} views`, async () => {
+ for (let i = 0; i < hits.bins.length; i++) {
+ await embed(spec(specType, 0, {type, ...binding}, {resolve: {scale: {x: 'shared', y: 'shared'}}}));
+ const parent = parentSelector(specType, i);
+ const {inOut, xold, yold} = await setup(specType, i, ['x', 'y'], parent);
+ const zoomed = (await page.evaluate(zoom('bins', i, inOut, null, bind === unbound)))[0];
+ const xnew = zoomed.values[0].sort(cmp);
+ const ynew = zoomed.values[1].sort(cmp);
+ assert[assertExtent[inOut][0]](xnew[0], xold[0]);
+ assert[assertExtent[inOut][1]](xnew[1], xold[1]);
+ assert[assertExtent[inOut][0]](ynew[0], yold[0]);
+ assert[assertExtent[inOut][1]](ynew[1], yold[1]);
+ await testRender(`${specType}_${inOut}`);
+ }
+ });
+ }
}
+ });
+ }
+
+ it('should work with geo intervals', async () => {
+ testRender = testRenderFn(page, `interval/zoom`);
+
+ await embed(geoSpec());
+ const drag = await page.evaluate(brush('drag', 1));
+ expect(drag).toHaveLength(13);
+ await testRender(`geo-0`);
+
+ for (let i = 0; i < hits.zoom.length; i++) {
+ const zoomed = await page.evaluate(zoom('zoom', i, i % 2 ? 'out' : 'in', null, true));
+ expect(zoomed.length).toBeGreaterThan(0);
+ await testRender(`geo-${i + 1}`);
}
});
-}
+});
diff --git a/test/compile/model.test.ts b/test/compile/model.test.ts
index 1f919833d3..7389cb1915 100644
--- a/test/compile/model.test.ts
+++ b/test/compile/model.test.ts
@@ -32,7 +32,7 @@ describe('Model', () => {
mark: 'point'
});
- expect(model.assembleGroupStyle()).toBeUndefined();
+ expect(model.assembleGroupStyle()).toBe('view');
});
it('returns cell by default for cartesian plots', () => {
diff --git a/test/compile/selection/interval.test.ts b/test/compile/selection/interval.test.ts
index 1b8841a882..c626cd0784 100644
--- a/test/compile/selection/interval.test.ts
+++ b/test/compile/selection/interval.test.ts
@@ -1,391 +1,517 @@
import {parseSelector} from 'vega-event-selector';
import {assembleUnitSelectionSignals} from '../../../src/compile/selection/assemble';
-import interval from '../../../src/compile/selection/interval';
+import interval, {GEO_INIT_TICK} from '../../../src/compile/selection/interval';
import {parseUnitSelection} from '../../../src/compile/selection/parse';
-import {parseUnitModel} from '../../util';
+import {parseUnitModel, parseUnitModelWithScale, parseUnitModelWithScaleAndLayoutSize} from '../../util';
describe('Interval Selections', () => {
- const model = parseUnitModel({
- mark: 'circle',
- encoding: {
- x: {field: 'Horsepower', type: 'quantitative'},
- y: {field: 'Miles-per-Gallon', type: 'quantitative'},
- color: {field: 'Origin', type: 'nominal'}
- }
- });
- model.parseScale();
-
- const selCmpts = (model.component.selection = parseUnitSelection(model, [
- {
- name: 'one',
- select: {type: 'interval', encodings: ['x'], clear: false, translate: false, zoom: false}
- },
- {
- name: 'two',
- select: {
- type: 'interval',
- encodings: ['y'],
- clear: false,
- translate: false,
- zoom: false
+ describe('Scaled intervals', () => {
+ const model = parseUnitModelWithScale({
+ mark: 'circle',
+ encoding: {
+ x: {field: 'Horsepower', type: 'quantitative'},
+ y: {field: 'Miles-per-Gallon', type: 'quantitative'},
+ color: {field: 'Origin', type: 'nominal'}
+ }
+ });
+
+ const selCmpts = (model.component.selection = parseUnitSelection(model, [
+ {
+ name: 'one',
+ select: {type: 'interval', encodings: ['x'], clear: false, translate: false, zoom: false}
},
- bind: 'scales'
- },
- {
- name: 'thr-ee',
- select: {
- type: 'interval',
- on: '[mousedown, mouseup] > mousemove, [keydown, keyup] > keypress',
- clear: false,
- translate: false,
- zoom: false,
- resolve: 'intersect',
- mark: {
- fill: 'red',
- fillOpacity: 0.75,
- stroke: 'black',
- strokeWidth: 4,
- strokeDash: [10, 5],
- strokeDashOffset: 3,
- strokeOpacity: 0.25
+ {
+ name: 'two',
+ select: {
+ type: 'interval',
+ encodings: ['y'],
+ clear: false,
+ translate: false,
+ zoom: false
+ },
+ bind: 'scales'
+ },
+ {
+ name: 'thr-ee',
+ select: {
+ type: 'interval',
+ on: '[mousedown, mouseup] > mousemove, [keydown, keyup] > keypress',
+ clear: false,
+ translate: false,
+ zoom: false,
+ resolve: 'intersect',
+ mark: {
+ fill: 'red',
+ fillOpacity: 0.75,
+ stroke: 'black',
+ strokeWidth: 4,
+ strokeDash: [10, 5],
+ strokeDashOffset: 3,
+ strokeOpacity: 0.25
+ }
}
- }
- },
- {
- name: 'four',
- value: {x: [50, 70]},
- select: {
- type: 'interval',
- encodings: ['x'],
- clear: false,
- translate: false,
- zoom: false
- }
- },
- {
- name: 'five',
- value: {x: [50, 60], y: [23, 54]},
- select: {
- type: 'interval',
- clear: false,
- translate: false,
- zoom: false
- }
- },
- {
- name: 'six',
- value: {
- x: [
- {year: 2000, month: 10, date: 5},
- {year: 2001, month: 1, date: 13}
- ]
},
- select: {
- type: 'interval',
- clear: false,
- translate: false,
- zoom: false,
- encodings: ['x']
+ {
+ name: 'four',
+ value: {x: [50, 70]},
+ select: {
+ type: 'interval',
+ encodings: ['x'],
+ clear: false,
+ translate: false,
+ zoom: false
+ }
+ },
+ {
+ name: 'five',
+ value: {x: [50, 60], y: [23, 54]},
+ select: {
+ type: 'interval',
+ clear: false,
+ translate: false,
+ zoom: false
+ }
+ },
+ {
+ name: 'six',
+ value: {
+ x: [
+ {year: 2000, month: 10, date: 5},
+ {year: 2001, month: 1, date: 13}
+ ]
+ },
+ select: {
+ type: 'interval',
+ clear: false,
+ translate: false,
+ zoom: false,
+ encodings: ['x']
+ }
}
- }
- ]));
+ ]));
- describe('Tuple Signals', () => {
- it('builds projection signals', () => {
- const oneSg = interval.signals(model, selCmpts['one'], []);
- expect(oneSg).toEqual(
- expect.arrayContaining([
- {
- name: 'one_x',
- value: [],
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
- update: '[one_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: {signal: 'one_scale_trigger'},
- update: '[scale("x", one_Horsepower[0]), scale("x", one_Horsepower[1])]'
- }
- ]
- },
- {
- name: 'one_Horsepower',
- on: [
- {
- events: {signal: 'one_x'},
- update: 'one_x[0] === one_x[1] ? null : invert("x", one_x)'
- }
- ]
- },
- {
- name: 'one_scale_trigger',
- value: {},
- on: [
- {
- events: [{scale: 'x'}],
- update:
- '(!isArray(one_Horsepower) || (+invert("x", one_x)[0] === +one_Horsepower[0] && +invert("x", one_x)[1] === +one_Horsepower[1])) ? one_scale_trigger : {}'
- }
- ]
- }
- ])
- );
+ describe('Tuple Signals', () => {
+ it('builds projection signals', () => {
+ const oneSg = interval.signals(model, selCmpts['one'], []);
+ expect(oneSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'one_x',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[one_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: {signal: 'one_scale_trigger'},
+ update: '[scale("x", one_Horsepower[0]), scale("x", one_Horsepower[1])]'
+ }
+ ]
+ },
+ {
+ name: 'one_Horsepower',
+ on: [
+ {
+ events: {signal: 'one_x'},
+ update: 'one_x[0] === one_x[1] ? null : invert("x", one_x)'
+ }
+ ]
+ },
+ {
+ name: 'one_scale_trigger',
+ value: {},
+ on: [
+ {
+ events: [{scale: 'x'}],
+ update:
+ '(!isArray(one_Horsepower) || (+invert("x", one_x)[0] === +one_Horsepower[0] && +invert("x", one_x)[1] === +one_Horsepower[1])) ? one_scale_trigger : {}'
+ }
+ ]
+ }
+ ])
+ );
+
+ const twoSg = interval.signals(model, selCmpts['two'], []);
+ expect(twoSg).toContainEqual({
+ name: 'two_Miles_per_Gallon',
+ on: []
+ });
+
+ const threeSg = interval.signals(model, selCmpts['thr_ee'], []);
+ expect(threeSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'thr_ee_x',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0],
+ update: '[thr_ee_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: parseSelector('keydown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[keydown, keyup] > keypress', 'scope')[0],
+ update: '[thr_ee_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: {signal: 'thr_ee_scale_trigger'},
+ update: '[scale("x", thr_ee_Horsepower[0]), scale("x", thr_ee_Horsepower[1])]'
+ }
+ ]
+ },
+ {
+ name: 'thr_ee_Horsepower',
+ on: [
+ {
+ events: {signal: 'thr_ee_x'},
+ update: 'thr_ee_x[0] === thr_ee_x[1] ? null : invert("x", thr_ee_x)'
+ }
+ ]
+ },
+ {
+ name: 'thr_ee_y',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0],
+ update: '[thr_ee_y[0], clamp(y(unit), 0, height)]'
+ },
+ {
+ events: parseSelector('keydown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[keydown, keyup] > keypress', 'scope')[0],
+ update: '[thr_ee_y[0], clamp(y(unit), 0, height)]'
+ },
+ {
+ events: {signal: 'thr_ee_scale_trigger'},
+ update: '[scale("y", thr_ee_Miles_per_Gallon[0]), scale("y", thr_ee_Miles_per_Gallon[1])]'
+ }
+ ]
+ },
+ {
+ name: 'thr_ee_Miles_per_Gallon',
+ on: [
+ {
+ events: {signal: 'thr_ee_y'},
+ update: 'thr_ee_y[0] === thr_ee_y[1] ? null : invert("y", thr_ee_y)'
+ }
+ ]
+ },
+ {
+ name: 'thr_ee_scale_trigger',
+ value: {},
+ on: [
+ {
+ events: [{scale: 'x'}, {scale: 'y'}],
+ update:
+ '(!isArray(thr_ee_Horsepower) || (+invert("x", thr_ee_x)[0] === +thr_ee_Horsepower[0] && +invert("x", thr_ee_x)[1] === +thr_ee_Horsepower[1])) && (!isArray(thr_ee_Miles_per_Gallon) || (+invert("y", thr_ee_y)[0] === +thr_ee_Miles_per_Gallon[0] && +invert("y", thr_ee_y)[1] === +thr_ee_Miles_per_Gallon[1])) ? thr_ee_scale_trigger : {}'
+ }
+ ]
+ }
+ ])
+ );
- const twoSg = interval.signals(model, selCmpts['two'], []);
- expect(twoSg).toContainEqual({
- name: 'two_Miles_per_Gallon',
- on: []
+ const fourSg = interval.signals(model, selCmpts['four'], []);
+ expect(fourSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'four_x',
+ init: '[scale("x", 50), scale("x", 70)]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[four_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: {signal: 'four_scale_trigger'},
+ update: '[scale("x", four_Horsepower[0]), scale("x", four_Horsepower[1])]'
+ }
+ ]
+ },
+ {
+ name: 'four_Horsepower',
+ init: '[50, 70]',
+ on: [
+ {
+ events: {signal: 'four_x'},
+ update: 'four_x[0] === four_x[1] ? null : invert("x", four_x)'
+ }
+ ]
+ },
+ {
+ name: 'four_scale_trigger',
+ value: {},
+ on: [
+ {
+ events: [{scale: 'x'}],
+ update:
+ '(!isArray(four_Horsepower) || (+invert("x", four_x)[0] === +four_Horsepower[0] && +invert("x", four_x)[1] === +four_Horsepower[1])) ? four_scale_trigger : {}'
+ }
+ ]
+ }
+ ])
+ );
+
+ const fiveSg = interval.signals(model, selCmpts['five'], []);
+ expect(fiveSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'five_x',
+ init: '[scale("x", 50), scale("x", 60)]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[five_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: {signal: 'five_scale_trigger'},
+ update: '[scale("x", five_Horsepower[0]), scale("x", five_Horsepower[1])]'
+ }
+ ]
+ },
+ {
+ name: 'five_Horsepower',
+ init: '[50, 60]',
+ on: [
+ {
+ events: {signal: 'five_x'},
+ update: 'five_x[0] === five_x[1] ? null : invert("x", five_x)'
+ }
+ ]
+ },
+ {
+ name: 'five_y',
+ init: '[scale("y", 23), scale("y", 54)]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[five_y[0], clamp(y(unit), 0, height)]'
+ },
+ {
+ events: {signal: 'five_scale_trigger'},
+ update: '[scale("y", five_Miles_per_Gallon[0]), scale("y", five_Miles_per_Gallon[1])]'
+ }
+ ]
+ },
+ {
+ name: 'five_Miles_per_Gallon',
+ init: '[23, 54]',
+ on: [
+ {
+ events: {signal: 'five_y'},
+ update: 'five_y[0] === five_y[1] ? null : invert("y", five_y)'
+ }
+ ]
+ },
+ {
+ name: 'five_scale_trigger',
+ value: {},
+ on: [
+ {
+ events: [{scale: 'x'}, {scale: 'y'}],
+ update:
+ '(!isArray(five_Horsepower) || (+invert("x", five_x)[0] === +five_Horsepower[0] && +invert("x", five_x)[1] === +five_Horsepower[1])) && (!isArray(five_Miles_per_Gallon) || (+invert("y", five_y)[0] === +five_Miles_per_Gallon[0] && +invert("y", five_y)[1] === +five_Miles_per_Gallon[1])) ? five_scale_trigger : {}'
+ }
+ ]
+ }
+ ])
+ );
+
+ const sixSg = interval.signals(model, selCmpts['six'], []);
+ expect(sixSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'six_x',
+ init: '[scale("x", datetime(2000, 9, 5, 0, 0, 0, 0)), scale("x", datetime(2001, 0, 13, 0, 0, 0, 0))]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[six_x[0], clamp(x(unit), 0, width)]'
+ },
+ {
+ events: {signal: 'six_scale_trigger'},
+ update: '[scale("x", six_Horsepower[0]), scale("x", six_Horsepower[1])]'
+ }
+ ]
+ },
+ {
+ name: 'six_Horsepower',
+ init: '[datetime(2000, 9, 5, 0, 0, 0, 0), datetime(2001, 0, 13, 0, 0, 0, 0)]',
+ on: [
+ {
+ events: {signal: 'six_x'},
+ update: 'six_x[0] === six_x[1] ? null : invert("x", six_x)'
+ }
+ ]
+ },
+ {
+ name: 'six_scale_trigger',
+ value: {},
+ on: [
+ {
+ events: [{scale: 'x'}],
+ update:
+ '(!isArray(six_Horsepower) || (+invert("x", six_x)[0] === +six_Horsepower[0] && +invert("x", six_x)[1] === +six_Horsepower[1])) ? six_scale_trigger : {}'
+ }
+ ]
+ }
+ ])
+ );
});
- const threeSg = interval.signals(model, selCmpts['thr_ee'], []);
- expect(threeSg).toEqual(
- expect.arrayContaining([
- {
- name: 'thr_ee_x',
- value: [],
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0],
- update: '[thr_ee_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: parseSelector('keydown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[keydown, keyup] > keypress', 'scope')[0],
- update: '[thr_ee_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: {signal: 'thr_ee_scale_trigger'},
- update: '[scale("x", thr_ee_Horsepower[0]), scale("x", thr_ee_Horsepower[1])]'
- }
- ]
- },
- {
- name: 'thr_ee_Horsepower',
- on: [
- {
- events: {signal: 'thr_ee_x'},
- update: 'thr_ee_x[0] === thr_ee_x[1] ? null : invert("x", thr_ee_x)'
- }
- ]
- },
- {
- name: 'thr_ee_y',
- value: [],
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[y(unit), y(unit)]'
- },
- {
- events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0],
- update: '[thr_ee_y[0], clamp(y(unit), 0, height)]'
- },
- {
- events: parseSelector('keydown', 'scope')[0],
- update: '[y(unit), y(unit)]'
- },
- {
- events: parseSelector('[keydown, keyup] > keypress', 'scope')[0],
- update: '[thr_ee_y[0], clamp(y(unit), 0, height)]'
- },
- {
- events: {signal: 'thr_ee_scale_trigger'},
- update: '[scale("y", thr_ee_Miles_per_Gallon[0]), scale("y", thr_ee_Miles_per_Gallon[1])]'
- }
- ]
- },
- {
- name: 'thr_ee_Miles_per_Gallon',
- on: [
- {
- events: {signal: 'thr_ee_y'},
- update: 'thr_ee_y[0] === thr_ee_y[1] ? null : invert("y", thr_ee_y)'
- }
- ]
- },
- {
- name: 'thr_ee_scale_trigger',
- value: {},
- on: [
- {
- events: [{scale: 'x'}, {scale: 'y'}],
- update:
- '(!isArray(thr_ee_Horsepower) || (+invert("x", thr_ee_x)[0] === +thr_ee_Horsepower[0] && +invert("x", thr_ee_x)[1] === +thr_ee_Horsepower[1])) && (!isArray(thr_ee_Miles_per_Gallon) || (+invert("y", thr_ee_y)[0] === +thr_ee_Miles_per_Gallon[0] && +invert("y", thr_ee_y)[1] === +thr_ee_Miles_per_Gallon[1])) ? thr_ee_scale_trigger : {}'
- }
- ]
- }
- ])
- );
+ it('builds trigger signals', () => {
+ const oneSg = interval.signals(model, selCmpts['one'], []);
+ expect(oneSg).toContainEqual({
+ name: 'one_tuple',
+ on: [
+ {
+ events: [{signal: 'one_Horsepower'}],
+ update: 'one_Horsepower ? {unit: "", fields: one_tuple_fields, values: [one_Horsepower]} : null'
+ }
+ ]
+ });
- const fourSg = interval.signals(model, selCmpts['four'], []);
- expect(fourSg).toEqual(
- expect.arrayContaining([
- {
- name: 'four_x',
- init: '[scale("x", 50), scale("x", 70)]',
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
- update: '[four_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: {signal: 'four_scale_trigger'},
- update: '[scale("x", four_Horsepower[0]), scale("x", four_Horsepower[1])]'
- }
- ]
- },
- {
- name: 'four_Horsepower',
- init: '[50, 70]',
- on: [
- {
- events: {signal: 'four_x'},
- update: 'four_x[0] === four_x[1] ? null : invert("x", four_x)'
- }
- ]
- },
- {
- name: 'four_scale_trigger',
- value: {},
- on: [
- {
- events: [{scale: 'x'}],
- update:
- '(!isArray(four_Horsepower) || (+invert("x", four_x)[0] === +four_Horsepower[0] && +invert("x", four_x)[1] === +four_Horsepower[1])) ? four_scale_trigger : {}'
- }
- ]
+ const twoSg = interval.signals(model, selCmpts['two'], []);
+ expect(twoSg).toContainEqual({
+ name: 'two_tuple',
+ on: [
+ {
+ events: [{signal: 'two_Miles_per_Gallon'}],
+ update:
+ 'two_Miles_per_Gallon ? {unit: "", fields: two_tuple_fields, values: [two_Miles_per_Gallon]} : null'
+ }
+ ]
+ });
+
+ const threeSg = interval.signals(model, selCmpts['thr_ee'], []);
+ expect(threeSg).toContainEqual({
+ name: 'thr_ee_tuple',
+ on: [
+ {
+ events: [{signal: 'thr_ee_Horsepower || thr_ee_Miles_per_Gallon'}],
+ update:
+ 'thr_ee_Horsepower && thr_ee_Miles_per_Gallon ? {unit: "", fields: thr_ee_tuple_fields, values: [thr_ee_Horsepower,thr_ee_Miles_per_Gallon]} : null'
+ }
+ ]
+ });
+
+ const fourSg = interval.signals(model, selCmpts['four'], []);
+ expect(fourSg).toContainEqual({
+ name: 'four_tuple',
+ init: '{unit: "", fields: four_tuple_fields, values: [[50, 70]]}',
+ on: [
+ {
+ events: [{signal: 'four_Horsepower'}],
+ update: 'four_Horsepower ? {unit: "", fields: four_tuple_fields, values: [four_Horsepower]} : null'
+ }
+ ]
+ });
+
+ const fiveSg = interval.signals(model, selCmpts['five'], []);
+ expect(fiveSg).toContainEqual({
+ name: 'five_tuple',
+ init: '{unit: "", fields: five_tuple_fields, values: [[50, 60], [23, 54]]}',
+ on: [
+ {
+ events: [{signal: 'five_Horsepower || five_Miles_per_Gallon'}],
+ update:
+ 'five_Horsepower && five_Miles_per_Gallon ? {unit: "", fields: five_tuple_fields, values: [five_Horsepower,five_Miles_per_Gallon]} : null'
+ }
+ ]
+ });
+ });
+
+ it('namespaces signals when encoding/fields collide', () => {
+ const model2 = parseUnitModel({
+ mark: 'circle',
+ encoding: {
+ x: {field: 'x', type: 'quantitative'},
+ y: {field: 'y', type: 'quantitative'}
}
- ])
- );
+ });
- const fiveSg = interval.signals(model, selCmpts['five'], []);
- expect(fiveSg).toEqual(
- expect.arrayContaining([
- {
- name: 'five_x',
- init: '[scale("x", 50), scale("x", 60)]',
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
- update: '[five_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: {signal: 'five_scale_trigger'},
- update: '[scale("x", five_Horsepower[0]), scale("x", five_Horsepower[1])]'
- }
- ]
- },
- {
- name: 'five_Horsepower',
- init: '[50, 60]',
- on: [
- {
- events: {signal: 'five_x'},
- update: 'five_x[0] === five_x[1] ? null : invert("x", five_x)'
- }
- ]
- },
- {
- name: 'five_y',
- init: '[scale("y", 23), scale("y", 54)]',
- on: [
- {
- events: parseSelector('mousedown', 'scope')[0],
- update: '[y(unit), y(unit)]'
- },
- {
- events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
- update: '[five_y[0], clamp(y(unit), 0, height)]'
- },
- {
- events: {signal: 'five_scale_trigger'},
- update: '[scale("y", five_Miles_per_Gallon[0]), scale("y", five_Miles_per_Gallon[1])]'
- }
- ]
- },
- {
- name: 'five_Miles_per_Gallon',
- init: '[23, 54]',
- on: [
- {
- events: {signal: 'five_y'},
- update: 'five_y[0] === five_y[1] ? null : invert("y", five_y)'
- }
- ]
- },
+ model2.parseScale();
+
+ const selCmpts2 = (model2.component.selection = parseUnitSelection(model2, [
{
- name: 'five_scale_trigger',
- value: {},
- on: [
- {
- events: [{scale: 'x'}, {scale: 'y'}],
- update:
- '(!isArray(five_Horsepower) || (+invert("x", five_x)[0] === +five_Horsepower[0] && +invert("x", five_x)[1] === +five_Horsepower[1])) && (!isArray(five_Miles_per_Gallon) || (+invert("y", five_y)[0] === +five_Miles_per_Gallon[0] && +invert("y", five_y)[1] === +five_Miles_per_Gallon[1])) ? five_scale_trigger : {}'
- }
- ]
+ name: 'one',
+ select: {
+ type: 'interval',
+ encodings: ['x'],
+ translate: false,
+ zoom: false
+ }
}
- ])
- );
+ ]));
- const sixSg = interval.signals(model, selCmpts['six'], []);
- expect(sixSg).toEqual(
+ const sg = interval.signals(model, selCmpts2['one'], []);
+ expect(sg[0].name).toBe('one_x_1');
+ expect(sg[1].name).toBe('one_x');
+ });
+ });
+
+ it('builds modify signals', () => {
+ const signals = assembleUnitSelectionSignals(model, []);
+ expect(signals).toEqual(
expect.arrayContaining([
{
- name: 'six_x',
- init: '[scale("x", datetime(2000, 9, 5, 0, 0, 0, 0)), scale("x", datetime(2001, 0, 13, 0, 0, 0, 0))]',
+ name: 'one_modify',
on: [
{
- events: parseSelector('mousedown', 'scope')[0],
- update: '[x(unit), x(unit)]'
- },
- {
- events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
- update: '[six_x[0], clamp(x(unit), 0, width)]'
- },
- {
- events: {signal: 'six_scale_trigger'},
- update: '[scale("x", six_Horsepower[0]), scale("x", six_Horsepower[1])]'
+ events: {signal: 'one_tuple'},
+ update: `modify("one_store", one_tuple, true)`
}
]
},
{
- name: 'six_Horsepower',
- init: '[datetime(2000, 9, 5, 0, 0, 0, 0), datetime(2001, 0, 13, 0, 0, 0, 0)]',
+ name: 'two_modify',
on: [
{
- events: {signal: 'six_x'},
- update: 'six_x[0] === six_x[1] ? null : invert("x", six_x)'
+ events: {signal: 'two_tuple'},
+ update: `modify("two_store", two_tuple, true)`
}
]
},
{
- name: 'six_scale_trigger',
- value: {},
+ name: 'thr_ee_modify',
on: [
{
- events: [{scale: 'x'}],
- update:
- '(!isArray(six_Horsepower) || (+invert("x", six_x)[0] === +six_Horsepower[0] && +invert("x", six_x)[1] === +six_Horsepower[1])) ? six_scale_trigger : {}'
+ events: {signal: 'thr_ee_tuple'},
+ update: `modify("thr_ee_store", thr_ee_tuple, {unit: ""})`
}
]
}
@@ -393,446 +519,538 @@ describe('Interval Selections', () => {
);
});
- it('builds trigger signals', () => {
- const oneSg = interval.signals(model, selCmpts['one'], []);
- expect(oneSg).toContainEqual({
- name: 'one_tuple',
- on: [
- {
- events: [{signal: 'one_Horsepower'}],
- update: 'one_Horsepower ? {unit: "", fields: one_tuple_fields, values: [one_Horsepower]} : null'
+ it('builds brush mark', () => {
+ const marks: any[] = [{hello: 'world'}];
+ expect(interval.marks(model, selCmpts['one'], marks)).toEqual([
+ {
+ name: 'one_brush_bg',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {
+ fill: {value: '#333'},
+ fillOpacity: {value: 0.125}
+ },
+ update: {
+ x: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ signal: 'one_x[0]'
+ },
+ {
+ value: 0
+ }
+ ],
+ y: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ value: 0
+ },
+ {
+ value: 0
+ }
+ ],
+ x2: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ signal: 'one_x[1]'
+ },
+ {
+ value: 0
+ }
+ ],
+ y2: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ field: {
+ group: 'height'
+ }
+ },
+ {
+ value: 0
+ }
+ ]
+ }
}
- ]
- });
-
- const twoSg = interval.signals(model, selCmpts['two'], []);
- expect(twoSg).toContainEqual({
- name: 'two_tuple',
- on: [
- {
- events: [{signal: 'two_Miles_per_Gallon'}],
- update: 'two_Miles_per_Gallon ? {unit: "", fields: two_tuple_fields, values: [two_Miles_per_Gallon]} : null'
+ },
+ {hello: 'world'},
+ {
+ name: 'one_brush',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {
+ fill: {value: 'transparent'}
+ },
+ update: {
+ stroke: [
+ {
+ test: 'one_x[0] !== one_x[1]',
+ value: 'white'
+ },
+ {
+ value: null
+ }
+ ],
+ x: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ signal: 'one_x[0]'
+ },
+ {
+ value: 0
+ }
+ ],
+ y: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ value: 0
+ },
+ {
+ value: 0
+ }
+ ],
+ x2: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ signal: 'one_x[1]'
+ },
+ {
+ value: 0
+ }
+ ],
+ y2: [
+ {
+ test: 'data("one_store").length && data("one_store")[0].unit === ""',
+ field: {
+ group: 'height'
+ }
+ },
+ {
+ value: 0
+ }
+ ]
+ }
}
- ]
- });
+ }
+ ]);
- const threeSg = interval.signals(model, selCmpts['thr_ee'], []);
- expect(threeSg).toContainEqual({
- name: 'thr_ee_tuple',
- on: [
- {
- events: [{signal: 'thr_ee_Horsepower || thr_ee_Miles_per_Gallon'}],
- update:
- 'thr_ee_Horsepower && thr_ee_Miles_per_Gallon ? {unit: "", fields: thr_ee_tuple_fields, values: [thr_ee_Horsepower,thr_ee_Miles_per_Gallon]} : null'
- }
- ]
- });
+ // Scale-bound interval selections should not add a brush mark.
+ expect(interval.marks(model, selCmpts['two'], marks)).toEqual(marks);
- const fourSg = interval.signals(model, selCmpts['four'], []);
- expect(fourSg).toContainEqual({
- name: 'four_tuple',
- init: '{unit: "", fields: four_tuple_fields, values: [[50, 70]]}',
- on: [
- {
- events: [{signal: 'four_Horsepower'}],
- update: 'four_Horsepower ? {unit: "", fields: four_tuple_fields, values: [four_Horsepower]} : null'
+ expect(interval.marks(model, selCmpts['thr_ee'], marks)).toEqual([
+ {
+ name: 'thr_ee_brush_bg',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {
+ fill: {value: 'red'},
+ fillOpacity: {value: 0.75}
+ },
+ update: {
+ x: {
+ signal: 'thr_ee_x[0]'
+ },
+ y: {
+ signal: 'thr_ee_y[0]'
+ },
+ x2: {
+ signal: 'thr_ee_x[1]'
+ },
+ y2: {
+ signal: 'thr_ee_y[1]'
+ }
+ }
}
- ]
- });
-
- const fiveSg = interval.signals(model, selCmpts['five'], []);
- expect(fiveSg).toContainEqual({
- name: 'five_tuple',
- init: '{unit: "", fields: five_tuple_fields, values: [[50, 60], [23, 54]]}',
- on: [
- {
- events: [{signal: 'five_Horsepower || five_Miles_per_Gallon'}],
- update:
- 'five_Horsepower && five_Miles_per_Gallon ? {unit: "", fields: five_tuple_fields, values: [five_Horsepower,five_Miles_per_Gallon]} : null'
+ },
+ {hello: 'world'},
+ {
+ name: 'thr_ee_brush',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {
+ fill: {value: 'transparent'}
+ },
+ update: {
+ stroke: [
+ {
+ test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
+ value: 'black'
+ },
+ {value: null}
+ ],
+ strokeWidth: [
+ {
+ test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
+ value: 4
+ },
+ {value: null}
+ ],
+ strokeDash: [
+ {
+ test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
+ value: [10, 5]
+ },
+ {value: null}
+ ],
+ strokeDashOffset: [
+ {
+ test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
+ value: 3
+ },
+ {value: null}
+ ],
+ strokeOpacity: [
+ {
+ test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
+ value: 0.25
+ },
+ {value: null}
+ ],
+ x: {
+ signal: 'thr_ee_x[0]'
+ },
+ y: {
+ signal: 'thr_ee_y[0]'
+ },
+ x2: {
+ signal: 'thr_ee_x[1]'
+ },
+ y2: {
+ signal: 'thr_ee_y[1]'
+ }
+ }
}
- ]
- });
+ }
+ ]);
});
- it('namespaces signals when encoding/fields collide', () => {
- const model2 = parseUnitModel({
+ it('should be robust to same channel/field names', () => {
+ const nameModel = parseUnitModel({
mark: 'circle',
encoding: {
x: {field: 'x', type: 'quantitative'},
y: {field: 'y', type: 'quantitative'}
}
});
+ nameModel.parseScale();
- model2.parseScale();
-
- const selCmpts2 = (model2.component.selection = parseUnitSelection(model2, [
+ const nameSelCmpts = (nameModel.component.selection = parseUnitSelection(nameModel, [
{
- name: 'one',
- select: {
- type: 'interval',
- encodings: ['x'],
- translate: false,
- zoom: false
- }
+ name: 'brush',
+ select: 'interval'
}
]));
- const sg = interval.signals(model, selCmpts2['one'], []);
- expect(sg[0].name).toBe('one_x_1');
- expect(sg[1].name).toBe('one_x');
- });
- });
+ const signals = interval.signals(nameModel, nameSelCmpts['brush'], []);
+ const names = signals.map(s => s.name);
+ expect(names).toEqual(expect.arrayContaining(['brush_x_1', 'brush_x', 'brush_y_1', 'brush_y']));
- it('builds modify signals', () => {
- const signals = assembleUnitSelectionSignals(model, []);
- expect(signals).toEqual(
- expect.arrayContaining([
- {
- name: 'one_modify',
- on: [
- {
- events: {signal: 'one_tuple'},
- update: `modify("one_store", one_tuple, true)`
- }
- ]
- },
+ const marks: any[] = [{hello: 'world'}];
+ expect(interval.marks(nameModel, nameSelCmpts['brush'], marks)).toEqual([
{
- name: 'two_modify',
- on: [
- {
- events: {signal: 'two_tuple'},
- update: `modify("two_store", two_tuple, true)`
+ name: 'brush_brush_bg',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {fill: {value: '#333'}, fillOpacity: {value: 0.125}},
+ update: {
+ x: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_x_1[0]'
+ },
+ {value: 0}
+ ],
+ y: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_y_1[0]'
+ },
+ {value: 0}
+ ],
+ x2: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_x_1[1]'
+ },
+ {value: 0}
+ ],
+ y2: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_y_1[1]'
+ },
+ {value: 0}
+ ]
}
- ]
+ }
},
+ {hello: 'world'},
{
- name: 'thr_ee_modify',
- on: [
- {
- events: {signal: 'thr_ee_tuple'},
- update: `modify("thr_ee_store", thr_ee_tuple, {unit: ""})`
+ name: 'brush_brush',
+ type: 'rect',
+ clip: true,
+ encode: {
+ enter: {fill: {value: 'transparent'}},
+ update: {
+ x: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_x_1[0]'
+ },
+ {value: 0}
+ ],
+ y: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_y_1[0]'
+ },
+ {value: 0}
+ ],
+ x2: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_x_1[1]'
+ },
+ {value: 0}
+ ],
+ y2: [
+ {
+ test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
+ signal: 'brush_y_1[1]'
+ },
+ {value: 0}
+ ],
+ stroke: [
+ {
+ test: 'brush_x_1[0] !== brush_x_1[1] && brush_y_1[0] !== brush_y_1[1]',
+ value: 'white'
+ },
+ {value: null}
+ ]
}
- ]
+ }
}
- ])
- );
+ ]);
+ });
});
- it('builds brush mark', () => {
- const marks: any[] = [{hello: 'world'}];
- expect(interval.marks(model, selCmpts['one'], marks)).toEqual([
- {
- name: 'one_brush_bg',
- type: 'rect',
- clip: true,
- encode: {
- enter: {
- fill: {value: '#333'},
- fillOpacity: {value: 0.125}
- },
- update: {
- x: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- signal: 'one_x[0]'
- },
- {
- value: 0
- }
- ],
- y: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- value: 0
- },
- {
- value: 0
- }
- ],
- x2: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- signal: 'one_x[1]'
- },
- {
- value: 0
- }
- ],
- y2: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- field: {
- group: 'height'
- }
- },
- {
- value: 0
- }
- ]
- }
+ describe('Geo intervals', () => {
+ const model = parseUnitModelWithScaleAndLayoutSize({
+ data: {
+ url: 'data/airports.csv',
+ format: {
+ type: 'csv'
}
},
- {hello: 'world'},
- {
- name: 'one_brush',
- type: 'rect',
- clip: true,
- encode: {
- enter: {
- fill: {value: 'transparent'}
- },
- update: {
- stroke: [
- {
- test: 'one_x[0] !== one_x[1]',
- value: 'white'
- },
- {
- value: null
- }
- ],
- x: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- signal: 'one_x[0]'
- },
- {
- value: 0
- }
- ],
- y: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- value: 0
- },
- {
- value: 0
- }
- ],
- x2: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- signal: 'one_x[1]'
- },
- {
- value: 0
- }
- ],
- y2: [
- {
- test: 'data("one_store").length && data("one_store")[0].unit === ""',
- field: {
- group: 'height'
- }
- },
- {
- value: 0
- }
- ]
- }
+ mark: 'circle',
+ projection: {
+ type: 'albersUsa'
+ },
+ encoding: {
+ longitude: {
+ field: 'longitude',
+ type: 'quantitative'
+ },
+ latitude: {
+ field: 'latitude',
+ type: 'quantitative'
}
}
- ]);
-
- // Scale-bound interval selections should not add a brush mark.
- expect(interval.marks(model, selCmpts['two'], marks)).toEqual(marks);
+ });
+ model.parseProjection();
- expect(interval.marks(model, selCmpts['thr_ee'], marks)).toEqual([
+ const selCmpts = (model.component.selection = parseUnitSelection(model, [
{
- name: 'thr_ee_brush_bg',
- type: 'rect',
- clip: true,
- encode: {
- enter: {
- fill: {value: 'red'},
- fillOpacity: {value: 0.75}
- },
- update: {
- x: {
- signal: 'thr_ee_x[0]'
- },
- y: {
- signal: 'thr_ee_y[0]'
- },
- x2: {
- signal: 'thr_ee_x[1]'
- },
- y2: {
- signal: 'thr_ee_y[1]'
- }
- }
+ name: 'one',
+ select: {type: 'interval', clear: false, translate: false, zoom: false}
+ },
+ {
+ name: 'two',
+ select: {type: 'interval', encodings: ['longitude'], clear: false, translate: false, zoom: false}
+ },
+ {
+ name: 'three',
+ select: {type: 'interval', clear: false, translate: false, zoom: false},
+ value: {
+ latitude: [30, 40],
+ longitude: [-86, -118]
}
},
- {hello: 'world'},
{
- name: 'thr_ee_brush',
- type: 'rect',
- clip: true,
- encode: {
- enter: {
- fill: {value: 'transparent'}
- },
- update: {
- stroke: [
- {
- test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
- value: 'black'
- },
- {value: null}
- ],
- strokeWidth: [
- {
- test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
- value: 4
- },
- {value: null}
- ],
- strokeDash: [
- {
- test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
- value: [10, 5]
- },
- {value: null}
- ],
- strokeDashOffset: [
- {
- test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
- value: 3
- },
- {value: null}
- ],
- strokeOpacity: [
- {
- test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]',
- value: 0.25
- },
- {value: null}
- ],
- x: {
- signal: 'thr_ee_x[0]'
+ name: 'four',
+ select: {type: 'interval', clear: false, translate: false, zoom: false},
+ value: {latitude: [30, 40]}
+ }
+ ]));
+
+ describe('Tuple Signals', () => {
+ it('builds projection signals', () => {
+ const oneSg = interval.signals(model, selCmpts['one'], []);
+ expect(oneSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'one_latitude_1',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[one_latitude_1[0], clamp(y(unit), 0, height)]'
+ }
+ ]
+ },
+ {
+ name: 'one_longitude_1',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[one_longitude_1[0], clamp(x(unit), 0, width)]'
+ }
+ ]
+ }
+ ])
+ );
+
+ const twoSg = interval.signals(model, selCmpts['two'], []);
+ expect(twoSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'two_longitude_1',
+ value: [],
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[two_longitude_1[0], clamp(x(unit), 0, width)]'
+ }
+ ]
+ }
+ ])
+ );
+
+ const threeSg = interval.signals(model, selCmpts['three'], []);
+ expect(threeSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'three_init',
+ init: '[scale("projection", [-86, 30]), scale("projection", [-118, 40])]'
+ },
+ {
+ name: 'three_latitude_1',
+ init: '[three_init[0][1], three_init[1][1]]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[three_latitude_1[0], clamp(y(unit), 0, height)]'
+ }
+ ]
},
- y: {
- signal: 'thr_ee_y[0]'
+ {
+ name: 'three_longitude_1',
+ init: '[three_init[0][0], three_init[1][0]]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[x(unit), x(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[three_longitude_1[0], clamp(x(unit), 0, width)]'
+ }
+ ]
+ }
+ ])
+ );
+
+ const fourSg = interval.signals(model, selCmpts['four'], []);
+ expect(fourSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'projection_center',
+ update: 'invert("projection", [width/2, height/2])'
},
- x2: {
- signal: 'thr_ee_x[1]'
+ {
+ name: 'four_init',
+ init: '[scale("projection", [projection_center[0], 30]), scale("projection", [projection_center[0], 40])]'
},
- y2: {
- signal: 'thr_ee_y[1]'
+ {
+ name: 'four_latitude_1',
+ init: '[four_init[0][1], four_init[1][1]]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[y(unit), y(unit)]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: '[four_latitude_1[0], clamp(y(unit), 0, height)]'
+ }
+ ]
}
- }
- }
- }
- ]);
- });
+ ])
+ );
+ });
- it('should be robust to same channel/field names', () => {
- const nameModel = parseUnitModel({
- mark: 'circle',
- encoding: {
- x: {field: 'x', type: 'quantitative'},
- y: {field: 'y', type: 'quantitative'}
- }
- });
- nameModel.parseScale();
+ it('builds trigger signals', () => {
+ const oneSg = interval.signals(model, selCmpts['one'], []);
+ expect(oneSg).toContainEqual({
+ name: 'one_tuple',
+ on: [
+ {
+ events: [{signal: 'one_longitude_1 || one_latitude_1'}],
+ update:
+ 'vlSelectionTuples(intersect([[one_longitude_1[0], one_latitude_1[0]],[one_longitude_1[1], one_latitude_1[1]]], {markname: "marks"}, unit.mark), {unit: ""})'
+ }
+ ]
+ });
- const nameSelCmpts = (nameModel.component.selection = parseUnitSelection(nameModel, [
- {
- name: 'brush',
- select: 'interval'
- }
- ]));
+ const twoSg = interval.signals(model, selCmpts['two'], []);
+ expect(twoSg).toContainEqual({
+ name: 'two_tuple',
+ on: [
+ {
+ events: [{signal: 'two_longitude_1'}],
+ update:
+ 'vlSelectionTuples(intersect([[two_longitude_1[0], 0],[two_longitude_1[1], height]], {markname: "marks"}, unit.mark), {unit: ""})'
+ }
+ ]
+ });
- const signals = interval.signals(nameModel, nameSelCmpts['brush'], []);
- const names = signals.map(s => s.name);
- expect(names).toEqual(expect.arrayContaining(['brush_x_1', 'brush_x', 'brush_y_1', 'brush_y']));
+ const threeSg = interval.signals(model, selCmpts['three'], []);
+ let update =
+ 'vlSelectionTuples(intersect([[three_longitude_1[0], three_latitude_1[0]],[three_longitude_1[1], three_latitude_1[1]]], {markname: "marks"}, unit.mark), {unit: ""})';
+ expect(threeSg).toContainEqual({
+ name: 'three_tuple',
+ on: [{events: [{signal: 'three_latitude_1 || three_longitude_1'}, {signal: GEO_INIT_TICK}], update}]
+ });
- const marks: any[] = [{hello: 'world'}];
- expect(interval.marks(nameModel, nameSelCmpts['brush'], marks)).toEqual([
- {
- name: 'brush_brush_bg',
- type: 'rect',
- clip: true,
- encode: {
- enter: {fill: {value: '#333'}, fillOpacity: {value: 0.125}},
- update: {
- x: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_x_1[0]'
- },
- {value: 0}
- ],
- y: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_y_1[0]'
- },
- {value: 0}
- ],
- x2: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_x_1[1]'
- },
- {value: 0}
- ],
- y2: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_y_1[1]'
- },
- {value: 0}
- ]
- }
- }
- },
- {hello: 'world'},
- {
- name: 'brush_brush',
- type: 'rect',
- clip: true,
- encode: {
- enter: {fill: {value: 'transparent'}},
- update: {
- x: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_x_1[0]'
- },
- {value: 0}
- ],
- y: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_y_1[0]'
- },
- {value: 0}
- ],
- x2: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_x_1[1]'
- },
- {value: 0}
- ],
- y2: [
- {
- test: 'data("brush_store").length && data("brush_store")[0].unit === ""',
- signal: 'brush_y_1[1]'
- },
- {value: 0}
- ],
- stroke: [
- {
- test: 'brush_x_1[0] !== brush_x_1[1] && brush_y_1[0] !== brush_y_1[1]',
- value: 'white'
- },
- {value: null}
- ]
- }
- }
- }
- ]);
+ const fourSg = interval.signals(model, selCmpts['four'], []);
+ update =
+ 'vlSelectionTuples(intersect([[0, four_latitude_1[0]],[width, four_latitude_1[1]]], {markname: "marks"}, unit.mark), {unit: ""})';
+ expect(fourSg).toContainEqual({
+ name: 'four_tuple',
+ on: [{events: [{signal: 'four_latitude_1'}, {signal: GEO_INIT_TICK}], update}]
+ });
+ });
+ });
});
});
diff --git a/test/compile/selection/parse.test.ts b/test/compile/selection/parse.test.ts
index 0e04759ad8..d5b8233752 100644
--- a/test/compile/selection/parse.test.ts
+++ b/test/compile/selection/parse.test.ts
@@ -29,7 +29,7 @@ describe('Selection', () => {
expect(component.one.name).toBe('one');
expect(component.one.type).toBe('point');
expect(component['one'].project.items).toEqual(
- expect.arrayContaining([{field: '_vgsid_', type: 'E', signals: {data: 'one__vgsid_'}}])
+ expect.arrayContaining([{field: '_vgsid_', index: 0, type: 'E', signals: {data: 'one__vgsid_'}}])
);
expect(component['one'].events).toEqual(parseSelector('click', 'scope'));
@@ -39,16 +39,28 @@ describe('Selection', () => {
expect(component.two.zoom).toBe('wheel!');
expect(component['two'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Horsepower', channel: 'x', type: 'R', signals: {data: 'two_Horsepower', visual: 'two_x'}},
+ {
+ field: 'Horsepower',
+ channel: 'x',
+ index: 0,
+ type: 'R',
+ signals: {data: 'two_Horsepower', visual: 'two_x'}
+ },
{
field: 'Miles_per_Gallon',
channel: 'y',
+ index: 1,
type: 'R',
signals: {data: 'two_Miles_per_Gallon', visual: 'two_y'}
}
])
);
- expect(component['two'].events).toEqual(parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope'));
+ expect(component['two'].events).toEqual(
+ parseSelector(
+ '[mousedown[!event.item || event.item.mark.name !== "two_brush"], window:mouseup] > window:mousemove!',
+ 'scope'
+ )
+ );
});
it('supports inline default overrides', () => {
@@ -87,7 +99,7 @@ describe('Selection', () => {
expect(component.one.name).toBe('one');
expect(component.one.type).toBe('point');
expect(component['one'].project.items).toEqual(
- expect.arrayContaining([{field: 'Cylinders', type: 'E', signals: {data: 'one_Cylinders'}}])
+ expect.arrayContaining([{field: 'Cylinders', index: 0, type: 'E', signals: {data: 'one_Cylinders'}}])
);
expect(component['one'].events).toEqual(parseSelector('dblclick', 'scope'));
@@ -96,7 +108,7 @@ describe('Selection', () => {
expect(component.two.toggle).toBe('event.ctrlKey');
expect(component['two'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Origin', channel: 'color', type: 'E', signals: {data: 'two_Origin', visual: 'two_color'}}
+ {field: 'Origin', channel: 'color', index: 0, type: 'E', signals: {data: 'two_Origin', visual: 'two_color'}}
])
);
expect(component['two'].events).toEqual(parseSelector('mouseover', 'scope'));
@@ -110,6 +122,7 @@ describe('Selection', () => {
{
field: 'Miles_per_Gallon',
channel: 'y',
+ index: 0,
type: 'R',
signals: {data: 'three_Miles_per_Gallon', visual: 'three_y'}
}
@@ -142,7 +155,7 @@ describe('Selection', () => {
expect(component.one.toggle).toBe('event.ctrlKey');
expect(component['one'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Origin', channel: 'color', type: 'E', signals: {data: 'one_Origin', visual: 'one_color'}}
+ {field: 'Origin', channel: 'color', index: 0, type: 'E', signals: {data: 'one_Origin', visual: 'one_color'}}
])
);
expect(component['one'].events).toEqual(parseSelector('mouseover', 'scope'));
@@ -156,6 +169,7 @@ describe('Selection', () => {
{
field: 'Miles_per_Gallon',
channel: 'y',
+ index: 0,
type: 'R',
signals: {data: 'two_Miles_per_Gallon', visual: 'two_y'}
}
@@ -182,7 +196,7 @@ describe('Selection', () => {
expect(c['one'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Origin', channel: 'x', type: 'E', signals: {data: 'one_Origin', visual: 'one_x'}}
+ {field: 'Origin', channel: 'x', index: 0, type: 'E', signals: {data: 'one_Origin', visual: 'one_x'}}
])
);
@@ -200,7 +214,7 @@ describe('Selection', () => {
expect(c['one'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Origin', channel: 'x', type: 'E', signals: {data: 'one_Origin', visual: 'one_x'}}
+ {field: 'Origin', channel: 'x', index: 0, type: 'E', signals: {data: 'one_Origin', visual: 'one_x'}}
])
);
});
@@ -218,7 +232,13 @@ describe('Selection', () => {
expect(c['one'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Acceleration', channel: 'x', type: 'R-RE', signals: {data: 'one_Acceleration', visual: 'one_x'}}
+ {
+ field: 'Acceleration',
+ channel: 'x',
+ index: 0,
+ type: 'R-RE',
+ signals: {data: 'one_Acceleration', visual: 'one_x'}
+ }
])
);
});
@@ -243,18 +263,24 @@ describe('Selection', () => {
]);
expect(component['one'].project.items).toEqual(
- expect.arrayContaining([{field: 'Origin', type: 'E', signals: {data: 'one_Origin'}}])
+ expect.arrayContaining([{field: 'Origin', index: 0, type: 'E', signals: {data: 'one_Origin'}}])
);
expect(component['two'].project.items).toEqual(
expect.arrayContaining([
- {channel: 'color', field: 'Origin', type: 'E', signals: {data: 'two_Origin', visual: 'two_color'}}
+ {channel: 'color', field: 'Origin', index: 0, type: 'E', signals: {data: 'two_Origin', visual: 'two_color'}}
])
);
expect(component['three'].project.items).toEqual(
expect.arrayContaining([
- {field: 'Horsepower', channel: 'x', type: 'R', signals: {data: 'three_Horsepower', visual: 'three_x'}}
+ {
+ field: 'Horsepower',
+ channel: 'x',
+ index: 0,
+ type: 'R',
+ signals: {data: 'three_Horsepower', visual: 'three_x'}
+ }
])
);
});
@@ -268,8 +294,8 @@ describe('Selection', () => {
]);
expect(component['one'].project.items).toEqual([
- {field: 'nested.a', type: 'E', signals: {data: 'one_nested_a'}},
- {field: 'nested.b.aa', type: 'E', signals: {data: 'one_nested_b_aa'}}
+ {field: 'nested.a', index: 0, type: 'E', signals: {data: 'one_nested_a'}},
+ {field: 'nested.b.aa', index: 1, type: 'E', signals: {data: 'one_nested_b_aa'}}
]);
expect(project.signals(null, component['one'], [])).toEqual([
@@ -292,8 +318,14 @@ describe('Selection', () => {
]);
expect(component['one'].project.items).toEqual([
- {field: 'Horsepower', channel: 'x', type: 'E', signals: {data: 'one_Horsepower', visual: 'one_x'}},
- {field: 'Miles_per_Gallon', channel: 'y', type: 'E', signals: {data: 'one_Miles_per_Gallon', visual: 'one_y'}}
+ {field: 'Horsepower', channel: 'x', index: 0, type: 'E', signals: {data: 'one_Horsepower', visual: 'one_x'}},
+ {
+ field: 'Miles_per_Gallon',
+ channel: 'y',
+ index: 1,
+ type: 'E',
+ signals: {data: 'one_Miles_per_Gallon', visual: 'one_y'}
+ }
]);
});
});