diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json
index 0e99779025..a936969728 100644
--- a/build/vega-lite-schema.json
+++ b/build/vega-lite-schema.json
@@ -20729,6 +20729,106 @@
},
"type": "object"
},
+ "RegionSelectionConfig": {
+ "additionalProperties": false,
+ "properties": {
+ "clear": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Stream"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ }
+ ],
+ "description": "Clears the selection, emptying it of all values. This property can be a [Event Stream](https://vega.github.io/vega/docs/event-streams/) or `false` to disable clear.\n\n__Default value:__ `dblclick`.\n\n__See also:__ [`clear` examples ](https://vega.github.io/vega-lite/docs/selection.html#clear) in the documentation."
+ },
+ "encodings": {
+ "description": "An array of encoding channels. The corresponding data field values must match for a data tuple to fall within the selection.\n\n__See also:__ The [projection with `encodings` and `fields` section](https://vega.github.io/vega-lite/docs/selection.html#project) in the documentation.",
+ "items": {
+ "$ref": "#/definitions/SingleDefUnitChannel"
+ },
+ "type": "array"
+ },
+ "mark": {
+ "$ref": "#/definitions/BrushConfig",
+ "description": "A region selection also adds a path mark to depict the shape of the region. The `mark` property can be used to customize the appearance of the mark.\n\n__See also:__ [`mark` examples](https://vega.github.io/vega-lite/docs/selection.html#mark) in the documentation."
+ },
+ "on": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Stream"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "A [Vega event stream](https://vega.github.io/vega/docs/event-streams/) (object or selector) that triggers the selection. For interval selections, the event stream must specify a [start and end](https://vega.github.io/vega/docs/event-streams/#between-filters).\n\n__See also:__ [`on` examples](https://vega.github.io/vega-lite/docs/selection.html#on) in the documentation."
+ },
+ "resolve": {
+ "$ref": "#/definitions/SelectionResolution",
+ "description": "With layered and multi-view displays, a strategy that determines how selections' data queries are resolved when applied in a filter transform, conditional encoding rule, or scale domain.\n\nOne of:\n- `\"global\"` -- only one brush exists for the entire SPLOM. When the user begins to drag, any previous brushes are cleared, and a new one is constructed.\n- `\"union\"` -- each cell contains its own brush, and points are highlighted if they lie within _any_ of these individual brushes.\n- `\"intersect\"` -- each cell contains its own brush, and points are highlighted only if they fall within _all_ of these individual brushes.\n\n__Default value:__ `global`.\n\n__See also:__ [`resolve` examples](https://vega.github.io/vega-lite/docs/selection.html#resolve) in the documentation."
+ },
+ "type": {
+ "const": "region",
+ "description": "Determines the default event processing and data query for the selection. Vega-Lite currently supports two selection types:\n\n- `\"point\"` -- to select multiple discrete data values; the first value is selected on `click` and additional values toggled on shift-click.\n- `\"interval\"` -- to select a continuous range of data values on `drag`.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "type"
+ ],
+ "type": "object"
+ },
+ "RegionSelectionConfigWithoutType": {
+ "additionalProperties": false,
+ "properties": {
+ "clear": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Stream"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "boolean"
+ }
+ ],
+ "description": "Clears the selection, emptying it of all values. This property can be a [Event Stream](https://vega.github.io/vega/docs/event-streams/) or `false` to disable clear.\n\n__Default value:__ `dblclick`.\n\n__See also:__ [`clear` examples ](https://vega.github.io/vega-lite/docs/selection.html#clear) in the documentation."
+ },
+ "encodings": {
+ "description": "An array of encoding channels. The corresponding data field values must match for a data tuple to fall within the selection.\n\n__See also:__ The [projection with `encodings` and `fields` section](https://vega.github.io/vega-lite/docs/selection.html#project) in the documentation.",
+ "items": {
+ "$ref": "#/definitions/SingleDefUnitChannel"
+ },
+ "type": "array"
+ },
+ "mark": {
+ "$ref": "#/definitions/BrushConfig",
+ "description": "A region selection also adds a path mark to depict the shape of the region. The `mark` property can be used to customize the appearance of the mark.\n\n__See also:__ [`mark` examples](https://vega.github.io/vega-lite/docs/selection.html#mark) in the documentation."
+ },
+ "on": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/Stream"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "description": "A [Vega event stream](https://vega.github.io/vega/docs/event-streams/) (object or selector) that triggers the selection. For interval selections, the event stream must specify a [start and end](https://vega.github.io/vega/docs/event-streams/#between-filters).\n\n__See also:__ [`on` examples](https://vega.github.io/vega-lite/docs/selection.html#on) in the documentation."
+ },
+ "resolve": {
+ "$ref": "#/definitions/SelectionResolution",
+ "description": "With layered and multi-view displays, a strategy that determines how selections' data queries are resolved when applied in a filter transform, conditional encoding rule, or scale domain.\n\nOne of:\n- `\"global\"` -- only one brush exists for the entire SPLOM. When the user begins to drag, any previous brushes are cleared, and a new one is constructed.\n- `\"union\"` -- each cell contains its own brush, and points are highlighted if they lie within _any_ of these individual brushes.\n- `\"intersect\"` -- each cell contains its own brush, and points are highlighted only if they fall within _all_ of these individual brushes.\n\n__Default value:__ `global`.\n\n__See also:__ [`resolve` examples](https://vega.github.io/vega-lite/docs/selection.html#resolve) in the documentation."
+ }
+ },
+ "type": "object"
+ },
"RegressionTransform": {
"additionalProperties": false,
"properties": {
@@ -21943,6 +22043,10 @@
"point": {
"$ref": "#/definitions/PointSelectionConfigWithoutType",
"description": "The default definition for a [`point`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations for a point selection definition (except `type`) may be specified here.\n\nFor instance, setting `point` to `{\"on\": \"dblclick\"}` populates point selections on double-click by default."
+ },
+ "region": {
+ "$ref": "#/definitions/RegionSelectionConfigWithoutType",
+ "description": "The default definition for an [`region`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations for an region selection definition (except `type`) may be specified here."
}
},
"type": "object"
@@ -22017,6 +22121,9 @@
},
{
"$ref": "#/definitions/IntervalSelectionConfig"
+ },
+ {
+ "$ref": "#/definitions/RegionSelectionConfig"
}
],
"description": "Determines the default event processing and data query for the selection. Vega-Lite currently supports two selection types:\n\n- `\"point\"` -- to select multiple discrete data values; the first value is selected on `click` and additional values toggled on shift-click.\n- `\"interval\"` -- to select a continuous range of data values on `drag`."
@@ -22056,7 +22163,8 @@
"SelectionType": {
"enum": [
"point",
- "interval"
+ "interval",
+ "region"
],
"type": "string"
},
@@ -29516,6 +29624,9 @@
},
{
"$ref": "#/definitions/IntervalSelectionConfig"
+ },
+ {
+ "$ref": "#/definitions/RegionSelectionConfig"
}
],
"description": "Determines the default event processing and data query for the selection. Vega-Lite currently supports two selection types:\n\n- `\"point\"` -- to select multiple discrete data values; the first value is selected on `click` and additional values toggled on shift-click.\n- `\"interval\"` -- to select a continuous range of data values on `drag`."
diff --git a/examples/compiled/selection_type_region.png b/examples/compiled/selection_type_region.png
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/compiled/selection_type_region.svg b/examples/compiled/selection_type_region.svg
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/examples/compiled/selection_type_region.vg.json b/examples/compiled/selection_type_region.vg.json
new file mode 100644
index 0000000000..8c8188605c
--- /dev/null
+++ b/examples/compiled/selection_type_region.vg.json
@@ -0,0 +1,209 @@
+{
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
+ "background": "white",
+ "padding": 5,
+ "width": 200,
+ "height": 200,
+ "style": "cell",
+ "data": [
+ {"name": "brush_store"},
+ {
+ "name": "source_0",
+ "url": "data/cars.json",
+ "format": {"type": "json"},
+ "transform": [
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"Horsepower\"]) && isFinite(+datum[\"Horsepower\"]) && isValid(datum[\"Miles_per_Gallon\"]) && isFinite(+datum[\"Miles_per_Gallon\"])"
+ }
+ ]
+ }
+ ],
+ "signals": [
+ {
+ "name": "unit",
+ "value": {},
+ "on": [
+ {"events": "mousemove", "update": "isTuple(group()) ? group() : unit"}
+ ]
+ },
+ {
+ "name": "brush",
+ "update": "vlSelectionResolve(\"brush_store\", \"union\")"
+ },
+ {
+ "name": "brush_tuple",
+ "on": [
+ {
+ "events": [{"signal": "brush_screen_path"}],
+ "update": "vlSelectionTuples(intersectLasso(\"marks\", brush_screen_path, unit), {unit: \"\"})"
+ },
+ {"events": [{"source": "view", "type": "dblclick"}], "update": "null"}
+ ]
+ },
+ {
+ "name": "brush_screen_path",
+ "init": "[]",
+ "on": [
+ {
+ "events": {"source": "scope", "type": "mousedown"},
+ "update": "[[x(unit), y(unit)]]"
+ },
+ {
+ "events": {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {"source": "scope", "type": "mousedown"},
+ {"source": "window", "type": "mouseup"}
+ ]
+ },
+ "update": "lassoAppend(brush_screen_path, clamp(x(unit), 0, width), clamp(y(unit), 0, height))"
+ }
+ ]
+ },
+ {"name": "brush_tuple_fields", "value": []},
+ {
+ "name": "brush_modify",
+ "on": [
+ {
+ "events": {"signal": "brush_tuple"},
+ "update": "modify(\"brush_store\", brush_tuple, true)"
+ }
+ ]
+ }
+ ],
+ "marks": [
+ {
+ "name": "brush_brush",
+ "type": "path",
+ "encode": {
+ "enter": {
+ "fill": {"value": "#333"},
+ "fillOpacity": {"value": 0.125},
+ "stroke": {"value": "gray"},
+ "strokeWidth": {"value": 2},
+ "strokeDash": {"value": [8, 5]}
+ },
+ "update": {"path": {"signal": "lassoPath(brush_screen_path)"}}
+ }
+ },
+ {
+ "name": "marks",
+ "type": "symbol",
+ "style": ["point"],
+ "interactive": true,
+ "from": {"data": "source_0"},
+ "encode": {
+ "update": {
+ "opacity": {"value": 0.7},
+ "fill": {"value": "transparent"},
+ "stroke": [
+ {
+ "test": "!length(data(\"brush_store\")) || vlSelectionTest(\"brush_store\", datum)",
+ "scale": "color",
+ "field": "Cylinders"
+ },
+ {"value": "grey"}
+ ],
+ "ariaRoleDescription": {"value": "point"},
+ "description": {
+ "signal": "\"Horsepower: \" + (format(datum[\"Horsepower\"], \"\")) + \"; Miles_per_Gallon: \" + (format(datum[\"Miles_per_Gallon\"], \"\")) + \"; Cylinders: \" + (isValid(datum[\"Cylinders\"]) ? datum[\"Cylinders\"] : \"\"+datum[\"Cylinders\"])"
+ },
+ "x": {"scale": "x", "field": "Horsepower"},
+ "y": {"scale": "y", "field": "Miles_per_Gallon"}
+ }
+ }
+ }
+ ],
+ "scales": [
+ {
+ "name": "x",
+ "type": "linear",
+ "domain": {"data": "source_0", "field": "Horsepower"},
+ "range": [0, {"signal": "width"}],
+ "nice": true,
+ "zero": true
+ },
+ {
+ "name": "y",
+ "type": "linear",
+ "domain": {"data": "source_0", "field": "Miles_per_Gallon"},
+ "range": [{"signal": "height"}, 0],
+ "nice": true,
+ "zero": true
+ },
+ {
+ "name": "color",
+ "type": "ordinal",
+ "domain": {"data": "source_0", "field": "Cylinders", "sort": true},
+ "range": "ordinal",
+ "interpolate": "hcl"
+ }
+ ],
+ "axes": [
+ {
+ "scale": "x",
+ "orient": "bottom",
+ "gridScale": "y",
+ "grid": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "y",
+ "orient": "left",
+ "gridScale": "x",
+ "grid": true,
+ "tickCount": {"signal": "ceil(height/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "x",
+ "orient": "bottom",
+ "grid": false,
+ "title": "Horsepower",
+ "labelFlush": true,
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "zindex": 0
+ },
+ {
+ "scale": "y",
+ "orient": "left",
+ "grid": false,
+ "title": "Miles_per_Gallon",
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(height/40)"},
+ "zindex": 0
+ }
+ ],
+ "legends": [
+ {
+ "stroke": "color",
+ "symbolType": "circle",
+ "title": "Cylinders",
+ "encode": {
+ "symbols": {
+ "update": {
+ "fill": {"value": "transparent"},
+ "opacity": {"value": 0.7}
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/examples/compiled/selection_type_region_concat.png b/examples/compiled/selection_type_region_concat.png
new file mode 100644
index 0000000000..aee9b59f58
Binary files /dev/null and b/examples/compiled/selection_type_region_concat.png differ
diff --git a/examples/compiled/selection_type_region_concat.svg b/examples/compiled/selection_type_region_concat.svg
new file mode 100644
index 0000000000..da5d98415d
--- /dev/null
+++ b/examples/compiled/selection_type_region_concat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/compiled/selection_type_region_concat.vg.json b/examples/compiled/selection_type_region_concat.vg.json
new file mode 100644
index 0000000000..a52c2b7eb4
--- /dev/null
+++ b/examples/compiled/selection_type_region_concat.vg.json
@@ -0,0 +1,306 @@
+{
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
+ "description": "Two vertically concatenated charts that show a histogram of precipitation in Seattle and the relationship between min and max temperature.",
+ "background": "white",
+ "padding": 5,
+ "width": 200,
+ "data": [
+ {"name": "brush_store"},
+ {"name": "source_0", "url": "data/cars.json", "format": {"type": "json"}},
+ {
+ "name": "data_0",
+ "source": "source_0",
+ "transform": [
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"Horsepower\"]) && isFinite(+datum[\"Horsepower\"]) && isValid(datum[\"Miles_per_Gallon\"]) && isFinite(+datum[\"Miles_per_Gallon\"])"
+ }
+ ]
+ },
+ {
+ "name": "data_1",
+ "source": "source_0",
+ "transform": [
+ {"type": "filter", "expr": "brush"},
+ {
+ "type": "filter",
+ "expr": "isValid(datum[\"Acceleration\"]) && isFinite(+datum[\"Acceleration\"]) && isValid(datum[\"Displacement\"]) && isFinite(+datum[\"Displacement\"])"
+ }
+ ]
+ }
+ ],
+ "signals": [
+ {"name": "childHeight", "value": 200},
+ {
+ "name": "unit",
+ "value": {},
+ "on": [
+ {"events": "mousemove", "update": "isTuple(group()) ? group() : unit"}
+ ]
+ },
+ {
+ "name": "brush",
+ "update": "vlSelectionResolve(\"brush_store\", \"union\")"
+ }
+ ],
+ "layout": {"padding": 20, "columns": 1, "bounds": "full", "align": "each"},
+ "marks": [
+ {
+ "type": "group",
+ "name": "concat_0_group",
+ "style": "cell",
+ "encode": {
+ "update": {
+ "width": {"signal": "width"},
+ "height": {"signal": "childHeight"}
+ }
+ },
+ "signals": [
+ {
+ "name": "brush_tuple",
+ "on": [
+ {
+ "events": [{"signal": "brush_screen_path"}],
+ "update": "vlSelectionTuples(intersectLasso(\"concat_0_marks\", brush_screen_path, unit), {unit: \"concat_0\"})"
+ },
+ {
+ "events": [{"source": "view", "type": "dblclick"}],
+ "update": "null"
+ }
+ ]
+ },
+ {
+ "name": "brush_screen_path",
+ "init": "[]",
+ "on": [
+ {
+ "events": {"source": "scope", "type": "mousedown"},
+ "update": "[[x(unit), y(unit)]]"
+ },
+ {
+ "events": {
+ "source": "window",
+ "type": "mousemove",
+ "consume": true,
+ "between": [
+ {"source": "scope", "type": "mousedown"},
+ {"source": "window", "type": "mouseup"}
+ ]
+ },
+ "update": "lassoAppend(brush_screen_path, clamp(x(unit), 0, width), clamp(y(unit), 0, childHeight))"
+ }
+ ]
+ },
+ {"name": "brush_tuple_fields", "value": []},
+ {
+ "name": "brush_modify",
+ "on": [
+ {
+ "events": {"signal": "brush_tuple"},
+ "update": "modify(\"brush_store\", brush_tuple, true)"
+ }
+ ]
+ }
+ ],
+ "marks": [
+ {
+ "name": "brush_brush",
+ "type": "path",
+ "encode": {
+ "enter": {
+ "fill": {"value": "#333"},
+ "fillOpacity": {"value": 0.125},
+ "stroke": {"value": "#d95f02"},
+ "strokeWidth": {"value": 2},
+ "strokeDash": {"value": [2, 8]}
+ },
+ "update": {"path": {"signal": "lassoPath(brush_screen_path)"}}
+ }
+ },
+ {
+ "name": "concat_0_marks",
+ "type": "symbol",
+ "style": ["point"],
+ "interactive": true,
+ "from": {"data": "data_0"},
+ "encode": {
+ "update": {
+ "opacity": {"value": 0.7},
+ "fill": {"value": "transparent"},
+ "stroke": {"value": "#4c78a8"},
+ "ariaRoleDescription": {"value": "point"},
+ "description": {
+ "signal": "\"Horsepower: \" + (format(datum[\"Horsepower\"], \"\")) + \"; Miles_per_Gallon: \" + (format(datum[\"Miles_per_Gallon\"], \"\"))"
+ },
+ "x": {"scale": "concat_0_x", "field": "Horsepower"},
+ "y": {"scale": "concat_0_y", "field": "Miles_per_Gallon"}
+ }
+ }
+ }
+ ],
+ "axes": [
+ {
+ "scale": "concat_0_x",
+ "orient": "bottom",
+ "gridScale": "concat_0_y",
+ "grid": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "concat_0_y",
+ "orient": "left",
+ "gridScale": "concat_0_x",
+ "grid": true,
+ "tickCount": {"signal": "ceil(childHeight/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "concat_0_x",
+ "orient": "bottom",
+ "grid": false,
+ "title": "Horsepower",
+ "labelFlush": true,
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "zindex": 0
+ },
+ {
+ "scale": "concat_0_y",
+ "orient": "left",
+ "grid": false,
+ "title": "Miles_per_Gallon",
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(childHeight/40)"},
+ "zindex": 0
+ }
+ ]
+ },
+ {
+ "type": "group",
+ "name": "concat_1_group",
+ "style": "cell",
+ "encode": {
+ "update": {
+ "width": {"signal": "width"},
+ "height": {"signal": "childHeight"}
+ }
+ },
+ "marks": [
+ {
+ "name": "concat_1_marks",
+ "type": "symbol",
+ "style": ["point"],
+ "interactive": false,
+ "from": {"data": "data_1"},
+ "encode": {
+ "update": {
+ "opacity": {"value": 0.7},
+ "fill": {"value": "transparent"},
+ "stroke": {"value": "#4c78a8"},
+ "ariaRoleDescription": {"value": "point"},
+ "description": {
+ "signal": "\"Acceleration: \" + (format(datum[\"Acceleration\"], \"\")) + \"; Displacement: \" + (format(datum[\"Displacement\"], \"\"))"
+ },
+ "x": {"scale": "concat_1_x", "field": "Acceleration"},
+ "y": {"scale": "concat_1_y", "field": "Displacement"}
+ }
+ }
+ }
+ ],
+ "axes": [
+ {
+ "scale": "concat_1_x",
+ "orient": "bottom",
+ "gridScale": "concat_1_y",
+ "grid": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "concat_1_y",
+ "orient": "left",
+ "gridScale": "concat_1_x",
+ "grid": true,
+ "tickCount": {"signal": "ceil(childHeight/40)"},
+ "domain": false,
+ "labels": false,
+ "aria": false,
+ "maxExtent": 0,
+ "minExtent": 0,
+ "ticks": false,
+ "zindex": 0
+ },
+ {
+ "scale": "concat_1_x",
+ "orient": "bottom",
+ "grid": false,
+ "title": "Acceleration",
+ "labelFlush": true,
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(width/40)"},
+ "zindex": 0
+ },
+ {
+ "scale": "concat_1_y",
+ "orient": "left",
+ "grid": false,
+ "title": "Displacement",
+ "labelOverlap": true,
+ "tickCount": {"signal": "ceil(childHeight/40)"},
+ "zindex": 0
+ }
+ ]
+ }
+ ],
+ "scales": [
+ {
+ "name": "concat_0_x",
+ "type": "linear",
+ "domain": {"data": "data_0", "field": "Horsepower"},
+ "range": [0, {"signal": "width"}],
+ "nice": true,
+ "zero": true
+ },
+ {
+ "name": "concat_0_y",
+ "type": "linear",
+ "domain": {"data": "data_0", "field": "Miles_per_Gallon"},
+ "range": [{"signal": "childHeight"}, 0],
+ "nice": true,
+ "zero": true
+ },
+ {
+ "name": "concat_1_x",
+ "type": "linear",
+ "domain": [0, 25],
+ "range": [0, {"signal": "width"}],
+ "zero": true
+ },
+ {
+ "name": "concat_1_y",
+ "type": "linear",
+ "domain": [0, 500],
+ "range": [{"signal": "childHeight"}, 0],
+ "zero": true
+ }
+ ]
+}
diff --git a/examples/specs/selection_type_region.vl.json b/examples/specs/selection_type_region.vl.json
new file mode 100644
index 0000000000..2140a11a8f
--- /dev/null
+++ b/examples/specs/selection_type_region.vl.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "data": {"url": "data/cars.json"},
+ "params": [
+ {
+ "name": "brush",
+ "select": "region"
+ }
+ ],
+ "mark": "point",
+ "encoding": {
+ "x": {"field": "Horsepower", "type": "quantitative"},
+ "y": {"field": "Miles_per_Gallon", "type": "quantitative"},
+ "color": {
+ "condition": {"param": "brush", "field": "Cylinders", "type": "ordinal"},
+ "value": "grey"
+ }
+ }
+}
diff --git a/examples/specs/selection_type_region_concat.vl.json b/examples/specs/selection_type_region_concat.vl.json
new file mode 100644
index 0000000000..9c0084478f
--- /dev/null
+++ b/examples/specs/selection_type_region_concat.vl.json
@@ -0,0 +1,62 @@
+{
+ "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
+ "description": "Two vertically concatenated charts that show a histogram of precipitation in Seattle and the relationship between min and max temperature.",
+ "data": {"url": "data/cars.json", "format": {"type": "json"}},
+ "vconcat": [
+ {
+ "mark": "point",
+ "params": [{
+ "name": "brush",
+ "select": {
+ "type": "region",
+ "mark": {
+ "fill": "#333",
+ "stroke": "#d95f02",
+ "strokeDash": [2, 8],
+ "strokeWidth": 2
+ }
+ }
+ }],
+ "encoding": {
+ "x": {
+ "field": "Horsepower",
+ "type": "quantitative"
+ },
+ "y": {
+ "field": "Miles_per_Gallon",
+ "type": "quantitative"
+ }
+ }
+ },
+ {
+ "transform": [
+ {
+ "filter": "brush"
+ }
+ ],
+ "mark": "point",
+ "encoding": {
+ "x": {
+ "field": "Acceleration",
+ "type": "quantitative",
+ "scale": {
+ "domain": [
+ 0,
+ 25
+ ]
+ }
+ },
+ "y": {
+ "field": "Displacement",
+ "type": "quantitative",
+ "scale": {
+ "domain": [
+ 0,
+ 500
+ ]
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/site/_data/examples.json b/site/_data/examples.json
index 760e1aff13..2afa962841 100644
--- a/site/_data/examples.json
+++ b/site/_data/examples.json
@@ -821,6 +821,16 @@
"name": "selection_translate_scatterplot_drag",
"title": "Scatterplot Pan & Zoom"
},
+ {
+ "name": "selection_type_region",
+ "title": "Region (lasso) selection",
+ "description": "The plot below uses a region selection, which causes the chart to include an interactive brush."
+ },
+ {
+ "name": "selection_type_region_concat",
+ "title": "Customized region (lasso) selection",
+ "description": "The concatenated plot below uses a region selection, which causes the chart to include an interactive brush. The appearance can be customized."
+ },
{
"name": "interactive_query_widgets",
"title": "Query Widgets"
diff --git a/site/_includes/docs_toc.md b/site/_includes/docs_toc.md
index a71657fc06..8902f79e9c 100644
--- a/site/_includes/docs_toc.md
+++ b/site/_includes/docs_toc.md
@@ -287,6 +287,7 @@
- [Nominal]({{site.baseurl}}/docs/type.html#nominal)
- [GeoJSON]({{site.baseurl}}/docs/type.html#geojson)
- [Value]({{site.baseurl}}/docs/value.html)
+ - [Examples]({{site.baseurl}}/docs/value.html#examples)
- [Projection]({{site.baseurl}}/docs/projection.html)
- [Documentation Overview]({{site.baseurl}}/docs/projection.html#documentation-overview)
- [Projection Properties]({{site.baseurl}}/docs/projection.html#projection-properties)
@@ -329,6 +330,7 @@
- [Using Parameters]({{site.baseurl}}/docs/parameter.html#using-parameters)
- [Selection Configuration]({{site.baseurl}}/docs/parameter.html#config)
- [Value]({{site.baseurl}}/docs/value.html)
+ - [Examples]({{site.baseurl}}/docs/value.html#examples)
- [Expr]({{site.baseurl}}/docs/parameter.html)
- [Documentation Overview]({{site.baseurl}}/docs/parameter.html#documentation-overview)
- [Defining a Parameter]({{site.baseurl}}/docs/parameter.html#defining-a-parameter)
diff --git a/src/compile/selection/index.ts b/src/compile/selection/index.ts
index 604e84a637..9628cd1b63 100644
--- a/src/compile/selection/index.ts
+++ b/src/compile/selection/index.ts
@@ -28,6 +28,7 @@ import toggle from './toggle';
import translate from './translate';
import zoom from './zoom';
import {ParameterName} from '../../parameter';
+import region from './region';
export const STORE = '_store';
export const TUPLE = '_tuple';
@@ -69,6 +70,7 @@ export interface SelectionCompiler {
export const selectionCompilers: SelectionCompiler[] = [
point,
interval,
+ region,
project,
toggle,
diff --git a/src/compile/selection/region.ts b/src/compile/selection/region.ts
new file mode 100644
index 0000000000..6617621b2f
--- /dev/null
+++ b/src/compile/selection/region.ts
@@ -0,0 +1,90 @@
+import {OnEvent, Signal} from 'vega';
+import {stringValue} from 'vega-util';
+import {SelectionCompiler, TUPLE, unitName} from '.';
+import {warn} from '../../log';
+import {BRUSH} from './interval';
+import scales from './scales';
+export const SCREEN_PATH = '_screen_path';
+
+const region: SelectionCompiler<'region'> = {
+ defined: selCmpt => selCmpt.type === 'region',
+
+ signals: (model, selCmpt, signals) => {
+ const name = selCmpt.name;
+ const signalsToAdd: Signal[] = [];
+
+ const screenPathName = `${name}${SCREEN_PATH}`;
+
+ const w = model.getSizeSignalRef('width').signal;
+ const h = model.getSizeSignalRef('height').signal;
+
+ signalsToAdd.push({
+ name: `${name}${TUPLE}`,
+ on: [
+ {
+ events: [{signal: screenPathName}],
+ update: `vlSelectionTuples(intersectLasso(${stringValue(
+ model.getName('marks')
+ )}, ${screenPathName}, unit), {unit: ${unitName(model)}})`
+ }
+ ]
+ });
+
+ const regionEvents = selCmpt.events.reduce((on, evt) => {
+ if (!evt.between) {
+ warn(`${evt} is not an ordered event stream for region selections.`);
+ return on;
+ }
+
+ return [
+ ...on,
+ {events: evt.between[0], update: `[[x(unit), y(unit)]]`},
+ {events: evt, update: `lassoAppend(${screenPathName}, clamp(x(unit), 0, ${w}), clamp(y(unit), 0, ${h}))`}
+ ];
+ }, [] as OnEvent[]);
+
+ signalsToAdd.push({
+ name: screenPathName,
+ init: '[]',
+ on: regionEvents
+ });
+
+ return [...signals, ...signalsToAdd];
+ },
+
+ marks: (model, selCmpt, marks) => {
+ const name = selCmpt.name;
+ const {fill, fillOpacity, stroke, strokeDash, strokeWidth} = selCmpt.mark;
+
+ const screenPathName = `${name}${SCREEN_PATH}`;
+
+ // Do not add a brush if we're binding to scales.
+ if (scales.defined(selCmpt)) {
+ return marks;
+ }
+
+ return [
+ {
+ name: `${name + BRUSH}`,
+ type: 'path',
+ encode: {
+ enter: {
+ fill: {value: fill},
+ fillOpacity: {value: fillOpacity},
+ stroke: {value: stroke},
+ strokeWidth: {value: strokeWidth},
+ strokeDash: {value: strokeDash}
+ },
+ update: {
+ path: {
+ signal: `lassoPath(${screenPathName})`
+ }
+ }
+ }
+ },
+ ...marks
+ ];
+ }
+};
+
+export default region;
diff --git a/src/selection.ts b/src/selection.ts
index fa7b1b6239..9331d563a5 100644
--- a/src/selection.ts
+++ b/src/selection.ts
@@ -7,15 +7,13 @@ import {ParameterName} from './parameter';
import {Dict} from './util';
export const SELECTION_ID = '_vgsid_';
-export type SelectionType = 'point' | 'interval';
+export type SelectionType = 'point' | 'interval' | 'region';
export type SelectionResolution = 'global' | 'union' | 'intersect';
export type SelectionInit = PrimitiveValue | DateTime;
export type SelectionInitInterval = Vector2 | Vector2 | Vector2 | Vector2;
-
export type SelectionInitMapping = Dict;
export type SelectionInitIntervalMapping = Dict;
-
export type LegendStreamBinding = {legend: string | Stream};
export type LegendBinding = 'legend' | LegendStreamBinding;
@@ -200,6 +198,17 @@ export interface IntervalSelectionConfig extends BaseSelectionConfig<'interval'>
mark?: BrushConfig;
}
+export interface RegionSelectionConfig extends BaseSelectionConfig<'region'> {
+ /**
+ * A region selection also adds a path mark to depict the
+ * shape of the region. The `mark` property can be used to customize the
+ * appearance of the mark.
+ *
+ * __See also:__ [`mark` examples](https://vega.github.io/vega-lite/docs/selection.html#mark) in the documentation.
+ */
+ mark?: BrushConfig;
+}
+
export interface SelectionParameter {
/**
* Required. A unique name for the selection parameter. Selection names should be valid JavaScript identifiers: they should contain only alphanumeric characters (or "$", or "_") and may not start with a digit. Reserved keywords that may not be used as parameter names are "datum", "event", "item", and "parent".
@@ -212,7 +221,15 @@ export interface SelectionParameter {
* - `"point"` -- to select multiple discrete data values; the first value is selected on `click` and additional values toggled on shift-click.
* - `"interval"` -- to select a continuous range of data values on `drag`.
*/
- select: T | (T extends 'point' ? PointSelectionConfig : T extends 'interval' ? IntervalSelectionConfig : never);
+ select:
+ | T
+ | (T extends 'point'
+ ? PointSelectionConfig
+ : T extends 'interval'
+ ? IntervalSelectionConfig
+ : T extends 'region'
+ ? RegionSelectionConfig
+ : never);
/**
* Initialize the selection with a mapping between [projected channels or field names](https://vega.github.io/vega-lite/docs/selection.html#project) and initial values.
@@ -282,6 +299,8 @@ export type PointSelectionConfigWithoutType = Omit
export type IntervalSelectionConfigWithoutType = Omit;
+export type RegionSelectionConfigWithoutType = Omit;
+
export interface SelectionConfig {
/**
* The default definition for a [`point`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations
@@ -299,6 +318,12 @@ export interface SelectionConfig {
* interval selections by default.
*/
interval?: IntervalSelectionConfigWithoutType;
+
+ /**
+ * The default definition for an [`region`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations
+ * for an region selection definition (except `type`) may be specified here.
+ */
+ region?: RegionSelectionConfigWithoutType;
}
export const defaultConfig: SelectionConfig = {
@@ -317,6 +342,12 @@ export const defaultConfig: SelectionConfig = {
mark: {fill: '#333', fillOpacity: 0.125, stroke: 'white'},
resolve: 'global',
clear: 'dblclick'
+ },
+ region: {
+ on: '[mousedown, window:mouseup] > window:mousemove!',
+ resolve: 'global',
+ mark: {fill: '#333', fillOpacity: 0.125, stroke: 'gray', strokeWidth: 2, strokeDash: [8, 5]},
+ clear: 'dblclick'
}
};
diff --git a/test-runtime/index.html b/test-runtime/index.html
index b3f511838c..dec8ddefee 100644
--- a/test-runtime/index.html
+++ b/test-runtime/index.html
@@ -41,6 +41,22 @@
);
}
+ function pureMouseEvt(type, target, opts) {
+ opts.bubbles = true;
+ target = winSrc.indexOf(type) < 0 ? target : window;
+
+ target.dispatchEvent(type === 'wheel' ? new WheelEvent('wheel', opts) : new MouseEvent(type, opts));
+ }
+
+ function pureClear(id, parent, targetBrush) {
+ const el0 = mark(id, parent);
+ const [clientX, clientY] = coords(el0);
+
+ pureMouseEvt('mousedown', el0, {clientX, clientY});
+ pureMouseEvt('mouseup', window, {clientX, clientY});
+ pureMouseEvt('click', el0, {clientX, clientY});
+ }
+
function mark(id, parent) {
return document.querySelector((parent ? `g.${parent} ` : '') + `g.mark-symbol.role-mark path:nth-child(${id})`);
}
@@ -50,6 +66,10 @@
return [Math.ceil(rect.left + rect.width / 2), Math.ceil(rect.top + rect.height / 2)];
}
+ function pointOnCircle(point, radius, angle) {
+ return {clientX: point.clientX + radius * Math.cos(angle), clientY: point.clientY + radius * Math.sin(angle)}
+ }
+
function brushOrEl(el, parent, _) {
return !_ ? el : document.querySelector((parent ? `g.${parent} ` : '') + 'g.sel_brush > path');
}
@@ -70,6 +90,44 @@
return (await view.runAsync()).data('sel_store');
}
+ async function polygonRegion(parent, targetBrush, id, shape) {
+ const el0 = mark(id, parent);
+ const [mdX, mdY] = coords(el0);
+
+ shape.forEach((e, i) => {
+ const p = {clientX: mdX + e[0], clientY: mdY + e[1]}
+ if (i === 0) {
+ mouseEvt('mousedown', brushOrEl(el0, parent, targetBrush), p);
+ } else if (i === shape.length - 1) {
+ mouseEvt('mouseup', window, p);
+ } else {
+ pureMouseEvt('mousemove', brushOrEl(el0, parent, targetBrush), p);
+ }
+ })
+
+ return (await view.runAsync()).data('sel_store');
+ }
+
+ async function circleRegion(parent, targetBrush, id) {
+ const radius = 40;
+ const segments = 20;
+
+ const el0 = mark(id, parent);
+ const [mdX, mdY] = coords(el0);
+
+ for (let i = 0; i < segments; i++) {
+ if (i === 0) {
+ mouseEvt('mousedown', brushOrEl(el0, parent, targetBrush), pointOnCircle({clientX: mdX, clientY: mdY}, radius, 0));
+ } else if (i === segments - 1) {
+ mouseEvt('mouseup', window, pointOnCircle({clientX: mdX, clientY: mdY}, radius, (i / (segments / 2)) * Math.PI));
+ } else {
+ pureMouseEvt('mousemove', brushOrEl(el0, parent, targetBrush), pointOnCircle({clientX: mdX, clientY: mdY}, radius, (i / (segments / 2)) * Math.PI));
+ }
+ }
+
+ return (await view.runAsync()).data('sel_store');
+ }
+
async function pt(id, parent, shiftKey) {
const el = mark(id, parent);
const [clientX, clientY] = coords(el);
diff --git a/test-runtime/region.test.ts b/test-runtime/region.test.ts
new file mode 100644
index 0000000000..26f4aef984
--- /dev/null
+++ b/test-runtime/region.test.ts
@@ -0,0 +1,114 @@
+import {TopLevelSpec} from '../src';
+import {SelectionType} from '../src/selection';
+import {clear, embedFn, region, regionByPolygon, spec, testRenderFn} from './util';
+import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
+
+describe('interval selections at runtime in unit views', () => {
+ 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, `${type}/unit`);
+ await page.goto('http://0.0.0.0:8000/test-runtime/');
+ });
+
+ afterAll(async () => {
+ await page.close();
+ });
+
+ const type: SelectionType = 'region';
+
+ it('circle region test 1', async () => {
+ await embed(spec('unit', 0, {type}));
+ const store = await page.evaluate(region(14));
+
+ expect(store).toHaveLength(5);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+
+ await testRender(`circle_0`);
+ });
+
+ it('circle region test 2', async () => {
+ await embed(spec('unit', 0, {type}));
+ const store = await page.evaluate(region(3));
+
+ expect(store).toHaveLength(2);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+
+ await testRender(`cirlce_1`);
+ });
+
+ it('circle region test 3', async () => {
+ await embed(spec('unit', 0, {type}));
+ const store = await page.evaluate(region(6));
+
+ expect(store).toHaveLength(4);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+
+ await testRender(`circle_2`);
+ });
+
+ it('polygon region test 1', async () => {
+ await embed(spec('unit', 0, {type}));
+
+ const store = await page.evaluate(
+ regionByPolygon(6, [
+ [-30, -30],
+ [-30, 30],
+ [30, 30],
+ [30, -30]
+ ])
+ );
+
+ expect(store).toHaveLength(4);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+
+ await testRender(`polygon_0`);
+ });
+
+ it('polygon region test 2', async () => {
+ await embed(spec('unit', 0, {type}));
+
+ const l = 30;
+ const h = 15;
+
+ const store = await page.evaluate(
+ regionByPolygon(14, [
+ [-l, -l],
+ [-l, l],
+ [-h, h],
+ [-h, -h],
+ [h, -h],
+ [h, l],
+ [l, l],
+ [l, -l]
+ ])
+ );
+
+ expect(store).toHaveLength(2);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+
+ await testRender(`polygon_1`);
+ });
+
+ it('should clear out stored extents', async () => {
+ await embed(spec('unit', 0, {type}));
+ let store = await page.evaluate(region(14));
+
+ expect(store).toHaveLength(5);
+
+ store = await page.evaluate(clear(14));
+
+ expect(store).toBeUndefined();
+
+ await testRender(`clear_0`);
+ });
+});
diff --git a/test-runtime/resources/region/unit/circle_0.svg b/test-runtime/resources/region/unit/circle_0.svg
new file mode 100644
index 0000000000..03cc7a7946
--- /dev/null
+++ b/test-runtime/resources/region/unit/circle_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/unit/circle_2.svg b/test-runtime/resources/region/unit/circle_2.svg
new file mode 100644
index 0000000000..8a80857997
--- /dev/null
+++ b/test-runtime/resources/region/unit/circle_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/unit/cirlce_1.svg b/test-runtime/resources/region/unit/cirlce_1.svg
new file mode 100644
index 0000000000..0d64826005
--- /dev/null
+++ b/test-runtime/resources/region/unit/cirlce_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/unit/clear_0.svg b/test-runtime/resources/region/unit/clear_0.svg
new file mode 100644
index 0000000000..a442a6aea6
--- /dev/null
+++ b/test-runtime/resources/region/unit/clear_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/unit/polygon_0.svg b/test-runtime/resources/region/unit/polygon_0.svg
new file mode 100644
index 0000000000..be79a5a71e
--- /dev/null
+++ b/test-runtime/resources/region/unit/polygon_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/unit/polygon_1.svg b/test-runtime/resources/region/unit/polygon_1.svg
new file mode 100644
index 0000000000..de9472e38c
--- /dev/null
+++ b/test-runtime/resources/region/unit/polygon_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/util.ts b/test-runtime/util.ts
index 192974f3a5..67e0dba66c 100644
--- a/test-runtime/util.ts
+++ b/test-runtime/util.ts
@@ -69,7 +69,6 @@ export const hits = {
facet: [2, 6, 9],
facet_clear: [3, 4, 8]
},
-
interval: {
drag: [
[5, 14],
@@ -198,6 +197,18 @@ export function parentSelector(compositeType: ComposeType, index: number) {
return compositeType === 'facet' ? `cell > g:nth-child(${index + 1})` : `${UNIT_NAMES.repeat[index]}_group`;
}
+export function clear(id: number, parent?: string, targetBrush?: boolean) {
+ return `pureClear(${id}, ${stringValue(parent)}, ${!!targetBrush})`;
+}
+
+export function region(id: number, parent?: string, targetBrush?: boolean) {
+ return `circleRegion(${stringValue(parent)}, ${!!targetBrush}, ${id})`;
+}
+
+export function regionByPolygon(id: number, polygon: [number, number][], parent?: string, targetBrush?: boolean) {
+ return `polygonRegion(${stringValue(parent)}, ${!!targetBrush}, ${id}, ${JSON.stringify(polygon)})`;
+}
+
export function brush(key: string, idx: number, parent?: string, targetBrush?: boolean) {
const fn = key.match('_clear') ? 'clear' : 'brush';
return `${fn}(${hits.interval[key][idx].join(', ')}, ${stringValue(parent)}, ${!!targetBrush})`;
diff --git a/test/compile/selection/region.test.ts b/test/compile/selection/region.test.ts
new file mode 100644
index 0000000000..2ce39564e9
--- /dev/null
+++ b/test/compile/selection/region.test.ts
@@ -0,0 +1,107 @@
+import {assembleUnitSelectionSignals} from '../../../src/compile/selection/assemble';
+import region from '../../../src/compile/selection/region';
+import {parseUnitSelection} from '../../../src/compile/selection/parse';
+import {parseUnitModelWithScale} from '../../util';
+import {parseSelector} from 'vega';
+
+describe('Multi Selection', () => {
+ const model = parseUnitModelWithScale({
+ mark: 'circle',
+ encoding: {
+ x: {field: 'Horsepower', type: 'quantitative'},
+ y: {field: 'Miles_per_Gallon', type: 'quantitative', bin: true},
+ color: {field: 'Origin', type: 'nominal'}
+ }
+ });
+
+ const selCmpts2 = (model.component.selection = parseUnitSelection(model, [
+ {
+ name: 'one',
+ select: 'region'
+ },
+ {
+ name: 'two',
+ select: {
+ type: 'region',
+ mark: {
+ fill: 'red',
+ fillOpacity: 0.75,
+ stroke: 'black',
+ strokeWidth: 4,
+ strokeDash: [10, 5]
+ }
+ }
+ }
+ ]));
+
+ it('builds tuple signals', () => {
+ const oneSg = region.signals(model, selCmpts2['one'], []);
+
+ expect(oneSg).toEqual(
+ expect.arrayContaining([
+ {
+ name: 'one_tuple',
+ on: [
+ {
+ events: [{signal: 'one_screen_path'}],
+ update: 'vlSelectionTuples(intersectLasso("marks", one_screen_path, unit), {unit: ""})'
+ }
+ ]
+ },
+ {
+ name: 'one_screen_path',
+ init: '[]',
+ on: [
+ {
+ events: parseSelector('mousedown', 'scope')[0],
+ update: '[[x(unit), y(unit)]]'
+ },
+ {
+ events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0],
+ update: 'lassoAppend(one_screen_path, clamp(x(unit), 0, width), clamp(y(unit), 0, height))'
+ }
+ ]
+ }
+ ])
+ );
+ });
+
+ 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)`
+ }
+ ]
+ }
+ ])
+ );
+ });
+
+ it('builds brush mark', () => {
+ const marks: any[] = [];
+
+ expect(region.marks(model, selCmpts2['two'], marks)).toEqual([
+ {
+ name: 'two_brush',
+ type: 'path',
+ encode: {
+ enter: {
+ fill: {value: 'red'},
+ fillOpacity: {value: 0.75},
+ stroke: {value: 'black'},
+ strokeWidth: {value: 4},
+ strokeDash: {value: [10, 5]}
+ },
+ update: {path: {signal: 'lassoPath(two_screen_path)'}}
+ }
+ }
+ ]);
+ });
+});