diff --git a/build/vega-lite-schema.json b/build/vega-lite-schema.json
index 0ce9879b3a..3f009869f1 100644
--- a/build/vega-lite-schema.json
+++ b/build/vega-lite-schema.json
@@ -21346,6 +21346,120 @@
},
"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"
+ },
+ "fields": {
+ "description": "An array of field names whose 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/FieldName"
+ },
+ "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"
+ },
+ "fields": {
+ "description": "An array of field names whose 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/FieldName"
+ },
+ "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": {
@@ -23108,6 +23222,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"
@@ -23182,6 +23300,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`."
@@ -23221,7 +23342,8 @@
"SelectionType": {
"enum": [
"point",
- "interval"
+ "interval",
+ "region"
],
"type": "string"
},
@@ -30987,6 +31109,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..8583225ab5
Binary files /dev/null and b/examples/compiled/selection_type_region.png differ
diff --git a/examples/compiled/selection_type_region.svg b/examples/compiled/selection_type_region.svg
new file mode 100644
index 0000000000..c84dceb9e0
--- /dev/null
+++ b/examples/compiled/selection_type_region.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/compiled/selection_type_region.vg.json b/examples/compiled/selection_type_region.vg.json
new file mode 100644
index 0000000000..524cddd617
--- /dev/null
+++ b/examples/compiled/selection_type_region.vg.json
@@ -0,0 +1,220 @@
+{
+ "$schema": "https://vega.github.io/schema/vega/v5.json",
+ "background": "white",
+ "padding": 5,
+ "width": 200,
+ "height": 200,
+ "style": "cell",
+ "data": [
+ {
+ "name": "brush_store",
+ "transform": [{"type": "collect", "sort": {"field": "_vgsid_"}}]
+ },
+ {
+ "name": "source_0",
+ "url": "data/cars.json",
+ "format": {"type": "json"},
+ "transform": [
+ {"type": "identifier", "as": "_vgsid_"},
+ {
+ "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": "pointermove", "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_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": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"\"",
+ "signal": "lassoPath(brush_screen_path)"
+ },
+ {"value": "[]"}
+ ]
+ }
+ }
+ },
+ {
+ "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\")) || vlSelectionIdTest(\"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..7bb9c0852c
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..5e7f91704b
--- /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..a2e4fc230d
--- /dev/null
+++ b/examples/compiled/selection_type_region_concat.vg.json
@@ -0,0 +1,321 @@
+{
+ "$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",
+ "transform": [{"type": "collect", "sort": {"field": "_vgsid_"}}]
+ },
+ {
+ "name": "source_0",
+ "url": "data/cars.json",
+ "format": {"type": "json"},
+ "transform": [{"type": "identifier", "as": "_vgsid_"}]
+ },
+ {
+ "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": "pointermove", "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_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": [
+ {
+ "test": "data(\"brush_store\").length && data(\"brush_store\")[0].unit === \"concat_0\"",
+ "signal": "lassoPath(brush_screen_path)"
+ },
+ {"value": "[]"}
+ ]
+ }
+ }
+ },
+ {
+ "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 99568b67ea..d16ee804be 100644
--- a/site/_data/examples.json
+++ b/site/_data/examples.json
@@ -838,6 +838,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/src/compile/selection/index.ts b/src/compile/selection/index.ts
index 320541acd1..a1185bd705 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';
@@ -67,6 +68,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..bb0b4cd85b
--- /dev/null
+++ b/src/compile/selection/region.ts
@@ -0,0 +1,113 @@
+import {OnEvent, Signal} from 'vega';
+import {stringValue} from 'vega-util';
+import {SelectionCompiler, STORE, TUPLE, unitName} from '.';
+import {warn} from '../../log';
+import {BRUSH} from './interval';
+import scales from './scales';
+import {SELECTION_ID} from '../../selection';
+export const SCREEN_PATH = '_screen_path';
+
+const region: SelectionCompiler<'region'> = {
+ defined: selCmpt => selCmpt.type === 'region',
+
+ parse: (model, selCmpt, selDef) => {
+ // Region selections are only valid over the SELECTION_ID field.
+ // As a result, we don't expose "fields" as a valid property of the interface
+ // and instead hardwire it here during parsing.
+ selDef.select = {type: 'region', fields: [SELECTION_ID]} as any;
+ },
+
+ 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}`;
+ const store = `data(${stringValue(selCmpt.name + STORE)})`;
+
+ // Do not add a brush if we're binding to scales.
+ if (scales.defined(selCmpt)) {
+ return marks;
+ }
+
+ const path = {signal: `lassoPath(${screenPathName})`};
+
+ 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:
+ // If the selection is resolved to global, only a single region is in the store.
+ // We wrap a region mark's path encoding with a production rule to hide the mark
+ // if it corresponds to a unit different from the one in the store.
+ selCmpt.resolve === 'global'
+ ? [
+ {
+ test: `${store}.length && ${store}[0].unit === ${unitName(model)}`,
+ ...path
+ },
+ {
+ value: '[]'
+ }
+ ]
+ : path
+ }
+ }
+ },
+ ...marks
+ ];
+ }
+};
+
+export default region;
diff --git a/src/selection.ts b/src/selection.ts
index bf931b0055..f8a6586ed9 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 b58c958d70..d654db5ce1 100644
--- a/test-runtime/index.html
+++ b/test-runtime/index.html
@@ -38,6 +38,13 @@
);
}
+ 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 mark(id, parent) {
return document.querySelector((parent ? `g.${parent} ` : '') + `g.mark-symbol.role-mark path:nth-child(${id})`);
}
@@ -47,6 +54,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');
}
@@ -67,6 +78,53 @@
return (await view.runAsync()).data('sel_store');
}
+ async function polygonRegion(id, shape, parent, targetBrush) {
+ 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(id, radius, segments, parent, targetBrush) {
+ 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/interval.test.ts b/test-runtime/interval.test.ts
index f0b4690ff0..8654c04104 100644
--- a/test-runtime/interval.test.ts
+++ b/test-runtime/interval.test.ts
@@ -1,5 +1,5 @@
import {TopLevelSpec} from '../src';
-import {SelectionType} from '../src/selection';
+import {SELECTION_ID, SelectionType} from '../src/selection';
import {brush, embedFn, geoSpec, hits as hitsMaster, spec, testRenderFn, tuples} from './util';
import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
@@ -204,7 +204,7 @@ describe('interval selections at runtime in unit views', () => {
const store: any = await page.evaluate(brush('drag', 1));
expect(store).toHaveLength(13);
for (const t of store) {
- expect(t).toHaveProperty('_vgsid_');
+ expect(t).toHaveProperty(SELECTION_ID);
}
await testRender(`geo_1`);
});
@@ -214,7 +214,7 @@ describe('interval selections at runtime in unit views', () => {
const store: any = await page.evaluate(brush('drag', 0));
expect(store).toHaveLength(20);
for (const t of store) {
- expect(t).toHaveProperty('_vgsid_');
+ expect(t).toHaveProperty(SELECTION_ID);
}
await testRender(`geo_0`);
});
diff --git a/test-runtime/discrete.test.ts b/test-runtime/point.test.ts
similarity index 98%
rename from test-runtime/discrete.test.ts
rename to test-runtime/point.test.ts
index f30dc81978..a06a26c307 100644
--- a/test-runtime/discrete.test.ts
+++ b/test-runtime/point.test.ts
@@ -20,7 +20,7 @@ describe(`point selections at runtime in unit views`, () => {
});
const type: SelectionType = 'point';
- const hits = hitsMaster.discrete;
+ const hits = hitsMaster.point;
it('should add values to the store', async () => {
for (let i = 0; i < hits.qq.length; i++) {
diff --git a/test-runtime/region.test.ts b/test-runtime/region.test.ts
new file mode 100644
index 0000000000..2ea652ed48
--- /dev/null
+++ b/test-runtime/region.test.ts
@@ -0,0 +1,68 @@
+import {TopLevelSpec} from '../src';
+import {SELECTION_ID, SelectionType} from '../src/selection';
+import {clearRegion, embedFn, circleRegion, polygonRegion, spec, testRenderFn, hits} from './util';
+import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
+
+describe('region 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('should add values to the store for circle regions', async () => {
+ for (const [i, hit] of hits.region.circle.entries()) {
+ await embed(spec('unit', 0, {type}));
+ const store: any = await page.evaluate(circleRegion(hit.id));
+
+ expect(store).toHaveLength(hit.count);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+ for (const t of store) {
+ expect(t).toHaveProperty(SELECTION_ID);
+ }
+
+ await testRender(`circle_${i}`);
+ }
+ });
+
+ it('should add values to the store for complex polygons', async () => {
+ for (const [i, hit] of hits.region.polygon.entries()) {
+ await embed(spec('unit', 0, {type}));
+ const store: any = await page.evaluate(polygonRegion(hit.id, hit.coords));
+
+ expect(store).toHaveLength(hit.count);
+ expect(store[0].fields).toBeUndefined();
+ expect(store[0].values).toBeUndefined();
+ for (const t of store) {
+ expect(t).toHaveProperty(SELECTION_ID);
+ }
+
+ await testRender(`polygon_${i}`);
+ }
+ });
+
+ it('should clear out stored extents', async () => {
+ await embed(spec('unit', 0, {type}));
+
+ const hit = hits.region.circle[0];
+ let store = await page.evaluate(circleRegion(hit.id));
+ expect(store).toHaveLength(hit.count);
+
+ store = await page.evaluate(clearRegion(hits.region.circle_clear[0].id));
+ expect(store).toHaveLength(0);
+
+ await testRender(`clear_0`);
+ });
+});
diff --git a/test-runtime/resolve.test.ts b/test-runtime/resolve.test.ts
index d3045c4488..bd74e17b82 100644
--- a/test-runtime/resolve.test.ts
+++ b/test-runtime/resolve.test.ts
@@ -3,6 +3,7 @@ import {
compositeTypes,
embedFn,
hits as hitsMaster,
+ multiviewRegion,
parentSelector,
pt,
resolutions,
@@ -14,10 +15,15 @@ import {
import {Page} from 'puppeteer/lib/cjs/puppeteer/common/Page';
import {TopLevelSpec} from '../src';
+const fns = {
+ point: pt,
+ interval: brush,
+ region: multiviewRegion
+};
+
for (const type of selectionTypes) {
- const isInterval = type === 'interval';
- const hits: any = isInterval ? hitsMaster.interval : hitsMaster.discrete;
- const fn = isInterval ? brush : pt;
+ const hits = hitsMaster[type];
+ const fn = fns[type];
describe(`${type} selections at runtime`, () => {
let page: Page;
@@ -49,7 +55,7 @@ for (const type of selectionTypes) {
const selection = {
type,
resolve: 'global',
- ...(specType === 'facet' ? {encodings: ['y']} : {})
+ ...(specType === 'facet' && type !== 'region' ? {encodings: ['y']} : {})
};
for (let i = 0; i < hits[specType].length; i++) {
@@ -72,7 +78,7 @@ for (const type of selectionTypes) {
const selection = {
type,
resolve,
- ...(specType === 'facet' ? {encodings: ['x']} : {})
+ ...(specType === 'facet' && type !== 'region' ? {encodings: ['x']} : {})
};
/**
diff --git a/test-runtime/resources/region/facet/global_0.svg b/test-runtime/resources/region/facet/global_0.svg
new file mode 100644
index 0000000000..e9b60bbfb9
--- /dev/null
+++ b/test-runtime/resources/region/facet/global_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/global_1.svg b/test-runtime/resources/region/facet/global_1.svg
new file mode 100644
index 0000000000..9914adb83d
--- /dev/null
+++ b/test-runtime/resources/region/facet/global_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/global_2.svg b/test-runtime/resources/region/facet/global_2.svg
new file mode 100644
index 0000000000..fc5392f14c
--- /dev/null
+++ b/test-runtime/resources/region/facet/global_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/global_clear_2.svg b/test-runtime/resources/region/facet/global_clear_2.svg
new file mode 100644
index 0000000000..cb25b91108
--- /dev/null
+++ b/test-runtime/resources/region/facet/global_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_0.svg b/test-runtime/resources/region/facet/intersect_0.svg
new file mode 100644
index 0000000000..eb6da9e8ef
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_1.svg b/test-runtime/resources/region/facet/intersect_1.svg
new file mode 100644
index 0000000000..3312529937
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_2.svg b/test-runtime/resources/region/facet/intersect_2.svg
new file mode 100644
index 0000000000..9683dc2306
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_clear_0.svg b/test-runtime/resources/region/facet/intersect_clear_0.svg
new file mode 100644
index 0000000000..2884ee4b8e
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_clear_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_clear_1.svg b/test-runtime/resources/region/facet/intersect_clear_1.svg
new file mode 100644
index 0000000000..be479fff92
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_clear_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/intersect_clear_2.svg b/test-runtime/resources/region/facet/intersect_clear_2.svg
new file mode 100644
index 0000000000..b9bfcf6b8c
--- /dev/null
+++ b/test-runtime/resources/region/facet/intersect_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_0.svg b/test-runtime/resources/region/facet/union_0.svg
new file mode 100644
index 0000000000..eb6da9e8ef
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_1.svg b/test-runtime/resources/region/facet/union_1.svg
new file mode 100644
index 0000000000..5e1dc0206c
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_2.svg b/test-runtime/resources/region/facet/union_2.svg
new file mode 100644
index 0000000000..c60a631590
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_clear_0.svg b/test-runtime/resources/region/facet/union_clear_0.svg
new file mode 100644
index 0000000000..2884ee4b8e
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_clear_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_clear_1.svg b/test-runtime/resources/region/facet/union_clear_1.svg
new file mode 100644
index 0000000000..be479fff92
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_clear_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/facet/union_clear_2.svg b/test-runtime/resources/region/facet/union_clear_2.svg
new file mode 100644
index 0000000000..ed821edf5a
--- /dev/null
+++ b/test-runtime/resources/region/facet/union_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/global_0.svg b/test-runtime/resources/region/repeat/global_0.svg
new file mode 100644
index 0000000000..3aa27092c9
--- /dev/null
+++ b/test-runtime/resources/region/repeat/global_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/global_1.svg b/test-runtime/resources/region/repeat/global_1.svg
new file mode 100644
index 0000000000..79477098ba
--- /dev/null
+++ b/test-runtime/resources/region/repeat/global_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/global_2.svg b/test-runtime/resources/region/repeat/global_2.svg
new file mode 100644
index 0000000000..81dd340021
--- /dev/null
+++ b/test-runtime/resources/region/repeat/global_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/global_clear_2.svg b/test-runtime/resources/region/repeat/global_clear_2.svg
new file mode 100644
index 0000000000..35d7e51bbf
--- /dev/null
+++ b/test-runtime/resources/region/repeat/global_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_0.svg b/test-runtime/resources/region/repeat/intersect_0.svg
new file mode 100644
index 0000000000..18ba0ea442
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_1.svg b/test-runtime/resources/region/repeat/intersect_1.svg
new file mode 100644
index 0000000000..12d9e4d08e
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_2.svg b/test-runtime/resources/region/repeat/intersect_2.svg
new file mode 100644
index 0000000000..b5ed167f3b
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_clear_0.svg b/test-runtime/resources/region/repeat/intersect_clear_0.svg
new file mode 100644
index 0000000000..3574852192
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_clear_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_clear_1.svg b/test-runtime/resources/region/repeat/intersect_clear_1.svg
new file mode 100644
index 0000000000..3882d40c82
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_clear_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/intersect_clear_2.svg b/test-runtime/resources/region/repeat/intersect_clear_2.svg
new file mode 100644
index 0000000000..3d2e06adf7
--- /dev/null
+++ b/test-runtime/resources/region/repeat/intersect_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_0.svg b/test-runtime/resources/region/repeat/union_0.svg
new file mode 100644
index 0000000000..18ba0ea442
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_1.svg b/test-runtime/resources/region/repeat/union_1.svg
new file mode 100644
index 0000000000..76b0bb9287
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_2.svg b/test-runtime/resources/region/repeat/union_2.svg
new file mode 100644
index 0000000000..f01ab7a7e3
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_clear_0.svg b/test-runtime/resources/region/repeat/union_clear_0.svg
new file mode 100644
index 0000000000..3574852192
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_clear_0.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_clear_1.svg b/test-runtime/resources/region/repeat/union_clear_1.svg
new file mode 100644
index 0000000000..3882d40c82
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_clear_1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/test-runtime/resources/region/repeat/union_clear_2.svg b/test-runtime/resources/region/repeat/union_clear_2.svg
new file mode 100644
index 0000000000..edd1fa5361
--- /dev/null
+++ b/test-runtime/resources/region/repeat/union_clear_2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
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..b79c887b6e
--- /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_1.svg b/test-runtime/resources/region/unit/circle_1.svg
new file mode 100644
index 0000000000..33997b39c1
--- /dev/null
+++ b/test-runtime/resources/region/unit/circle_1.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..0992ffc4ac
--- /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/clear_0.svg b/test-runtime/resources/region/unit/clear_0.svg
new file mode 100644
index 0000000000..1b119829d8
--- /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..4a4baa010b
--- /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..674738c620
--- /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 c30f2d75fb..5d26ce62fb 100644
--- a/test-runtime/util.ts
+++ b/test-runtime/util.ts
@@ -10,8 +10,8 @@ const generate = process.env.VL_GENERATE_TESTS;
const output = 'test-runtime/resources';
export type ComposeType = 'unit' | 'repeat' | 'facet';
-export const selectionTypes: SelectionType[] = ['point', 'interval'];
-export const compositeTypes = ['repeat', 'facet'] as const;
+export const selectionTypes: SelectionType[] = ['point', 'interval', 'region'];
+export const compositeTypes: ComposeType[] = ['repeat', 'facet'];
export const resolutions: SelectionResolution[] = ['union', 'intersect'];
export const bound = 'bound';
@@ -56,7 +56,7 @@ const UNIT_NAMES = {
};
export const hits = {
- discrete: {
+ point: {
qq: [8, 19],
qq_clear: [5, 16],
@@ -104,6 +104,48 @@ export const hits = {
[4, 10]
],
facet_clear: [[3], [5], [7]]
+ },
+
+ region: {
+ circle: [
+ {id: 14, count: 5},
+ {id: 3, count: 2},
+ {id: 6, count: 4}
+ ],
+ circle_clear: [{id: 14}],
+
+ polygon: [
+ {
+ id: 6,
+ coords: [
+ [-30, -30],
+ [-30, 30],
+ [30, 30],
+ [30, -30]
+ ],
+ count: 4
+ },
+ {
+ id: 14,
+ coords: [
+ [-30, -30],
+ [-30, 30],
+ [-15, 15],
+ [-15, -15],
+ [15, -15],
+ [15, 30],
+ [30, 30],
+ [30, -30]
+ ],
+ count: 2
+ }
+ ],
+
+ facet: [2, 4, 7],
+ facet_clear: [3, 5, 8],
+
+ repeat: [5, 10, 16],
+ repeat_clear: [13, 14, 2]
}
};
@@ -259,14 +301,57 @@ export function parentSelector(compositeType: ComposeType, index: number) {
}
export type BrushKeys = keyof typeof hits.interval;
+
+export function clear(idx: number, parent?: string, targetBrush?: boolean) {
+ return `pureClear(${idx}, ${stringValue(parent)}, ${!!targetBrush})`;
+}
+
+export function clearRegion(
+ idx:
+ | number
+ | {
+ id: number;
+ count?: number;
+ },
+ parent?: string,
+ targetBrush?: boolean
+) {
+ return `clear(${idx}, ${stringValue(parent)}, ${!!targetBrush})`;
+}
+
+export function circleRegion(
+ idx:
+ | number
+ | {
+ id: number;
+ count?: number;
+ },
+ parent?: string,
+ targetBrush?: boolean,
+ radius = 40,
+ segments = 20
+) {
+ return `circleRegion(${idx}, ${radius}, ${segments}, ${stringValue(parent)}, ${!!targetBrush})`;
+}
+
+export function polygonRegion(idx: number, polygon: number[][], parent?: string, targetBrush?: boolean) {
+ return `polygonRegion(${idx}, ${JSON.stringify(polygon)}, ${stringValue(parent)}, ${!!targetBrush})`;
+}
+
+export function multiviewRegion(key: keyof typeof hits.region, idx: number, parent?: string, targetBrush?: boolean) {
+ return key.match('_clear')
+ ? clearRegion(hits.region[key][idx], parent, targetBrush)
+ : circleRegion(hits.region[key][idx], parent, targetBrush, 10);
+}
+
export function brush(key: BrushKeys, idx: number, parent?: string, targetBrush?: boolean) {
const fn = key.match('_clear') ? 'clear' : 'brush';
return `${fn}(${hits.interval[key][idx].join(', ')}, ${stringValue(parent)}, ${!!targetBrush})`;
}
-export function pt(key: keyof typeof hits.discrete, idx: number, parent?: string) {
+export function pt(key: keyof typeof hits.point, idx: number, parent?: string) {
const fn = key.match('_clear') ? 'clear' : 'pt';
- return `${fn}(${hits.discrete[key][idx]}, ${stringValue(parent)})`;
+ return `${fn}(${hits.point[key][idx]}, ${stringValue(parent)})`;
}
export function getState(signals: string[], data: string[]) {
diff --git a/test/compile/selection/region.test.ts b/test/compile/selection/region.test.ts
new file mode 100644
index 0000000000..bffdf38dc4
--- /dev/null
+++ b/test/compile/selection/region.test.ts
@@ -0,0 +1,115 @@
+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: [
+ {
+ test: 'data("two_store").length && data("two_store")[0].unit === ""',
+ signal: 'lassoPath(two_screen_path)'
+ },
+ {value: '[]'}
+ ]
+ }
+ }
+ }
+ ]);
+ });
+});