Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Lasso Specification Compiler #8104

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion build/vega-lite-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12289,6 +12289,120 @@
}
]
},
"LassoSelectionConfig": {
"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 lasso selection also adds a path mark to depict the shape of the lasso. 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": "lasso",
"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"
},
"LassoSelectionConfigWithoutType": {
"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 lasso selection also adds a path mark to depict the shape of the lasso. 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"
},
"LatLongDef": {
"anyOf": [
{
Expand Down Expand Up @@ -21936,6 +22050,10 @@
"$ref": "#/definitions/IntervalSelectionConfigWithoutType",
"description": "The default definition for an [`interval`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations for an interval selection definition (except `type`) may be specified here.\n\nFor instance, setting `interval` to `{\"translate\": false}` disables the ability to move interval selections by default."
},
"lasso": {
"$ref": "#/definitions/LassoSelectionConfigWithoutType",
"description": "The default definition for an [`lasso`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations for an lasso selection definition (except `type`) may be specified here."
},
"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."
Expand Down Expand Up @@ -22013,6 +22131,9 @@
},
{
"$ref": "#/definitions/IntervalSelectionConfig"
},
{
"$ref": "#/definitions/LassoSelectionConfig"
}
],
"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`."
Expand Down Expand Up @@ -22052,7 +22173,8 @@
"SelectionType": {
"enum": [
"point",
"interval"
"interval",
"lasso"
],
"type": "string"
},
Expand Down Expand Up @@ -29512,6 +29634,9 @@
},
{
"$ref": "#/definitions/IntervalSelectionConfig"
},
{
"$ref": "#/definitions/LassoSelectionConfig"
}
],
"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`."
Expand Down
2 changes: 2 additions & 0 deletions src/compile/selection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import toggle from './toggle';
import translate from './translate';
import zoom from './zoom';
import {ParameterName} from '../../parameter';
import lasso from './lasso';

export const STORE = '_store';
export const TUPLE = '_tuple';
Expand Down Expand Up @@ -69,6 +70,7 @@ export interface SelectionCompiler<T extends SelectionType = SelectionType> {
export const selectionCompilers: SelectionCompiler[] = [
point,
interval,
lasso,
project,
toggle,

Expand Down
96 changes: 96 additions & 0 deletions src/compile/selection/lasso.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {OnEvent, Signal, Stream} from 'vega';
import {stringValue} from 'vega-util';
import {SelectionCompiler, SelectionComponent, TUPLE, unitName} from '.';
import {warn} from '../../log';
import {BRUSH} from './interval';
import scales from './scales';
export const SCREEN_PATH = '_screen_path';

const lasso: SelectionCompiler<'lasso'> = {
defined: selCmpt => selCmpt.type === 'lasso',

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 on = events(selCmpt, (def: OnEvent[], evt: Stream) => {
return [
...def,
{events: evt.between[0], update: `[[x(unit), y(unit)]]`},
{events: evt, update: `lassoAppend(${screenPathName}, clamp(x(unit), 0, ${w}), clamp(y(unit), 0, ${h}))`}
];
});

signalsToAdd.push({
name: screenPathName,
init: '[]',
on: on
});

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: {
test: true,
dvmoritzschoefl marked this conversation as resolved.
Show resolved Hide resolved
signal: `lassoPath(${screenPathName})`
}
}
}
},
...marks
];
}
};

function events(selCmpt: SelectionComponent<'lasso'>, cb: (def: OnEvent[], evt: Stream) => OnEvent[]): OnEvent[] {
dvmoritzschoefl marked this conversation as resolved.
Show resolved Hide resolved
return selCmpt.events.reduce((on, evt) => {
if (!evt.between) {
warn(`${evt} is not an ordered event stream for lasso selections.`);
return on;
}
return cb(on, evt);
}, [] as OnEvent[]);
}

export default lasso;
50 changes: 46 additions & 4 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import {ParameterName} from './parameter';
import {Dict} from './util';

export const SELECTION_ID = '_vgsid_';
export type SelectionType = 'point' | 'interval';
export type SelectionType = 'point' | 'interval' | 'lasso';
dvmoritzschoefl marked this conversation as resolved.
Show resolved Hide resolved
export type SelectionResolution = 'global' | 'union' | 'intersect';

export type SelectionInit = PrimitiveValue | DateTime;
export type SelectionInitInterval = Vector2<boolean> | Vector2<number> | Vector2<string> | Vector2<DateTime>;

export type SelectionInitLasso = number[];
export type SelectionInitMapping = Dict<SelectionInit>;
export type SelectionInitIntervalMapping = Dict<SelectionInitInterval>;

export type SelectionInitLassoMapping = Dict<SelectionInitLasso>;
dvmoritzschoefl marked this conversation as resolved.
Show resolved Hide resolved
export type LegendStreamBinding = {legend: string | Stream};
export type LegendBinding = 'legend' | LegendStreamBinding;

Expand Down Expand Up @@ -200,6 +200,25 @@ export interface IntervalSelectionConfig extends BaseSelectionConfig<'interval'>
mark?: BrushConfig;
}

export interface LassoSelectionConfig extends BaseSelectionConfig<'lasso'> {
/**
* A lasso selection also adds a path mark to depict the
* shape of the lasso. 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;

/**
* An array of field names whose values must match for a data tuple to
* fall within the selection.
*
* __See also:__ The [projection with `encodings` and `fields` section](https://vega.github.io/vega-lite/docs/selection.html#project) in the documentation.
*/
fields?: FieldName[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How should this property work? Shouldn't the fields always match the x and y encodings?

It would be good to add an example for this property if it should be supported, or otherwise we should remove them.

Copy link
Author

@dvmoritzschoefl dvmoritzschoefl Sep 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, looking at the vega counterpart for this PR
https://github.com/vega/vega/pull/3388/files#diff-547caeeef0ceae19c314c72edcd93ab9aac4a9462ada8fbae25ec7d37fad147b

the intersection test will always use x and y coordinates, if this is what you meant.
I think I took the interval selection config as a starting point, can I just remove the fields property if it is not needed?

EDIT:
Shouldn´t the fields property be always [SELECTION_ID] like in the default config for the point selection? The interval selection uses x/y channel bounds or ids as values, but for the region only ids are possible (like in the point case). And looking at line 83 the point selection also has a fields property.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn´t the fields property be always [SELECTION_ID] like in the default config for the point selection?

Point selection indeed uses [SELECTION_ID] by default. However, one can configure the fields to be different for point selection, thus having the fields property for users to customize makes sense.

So the question is whether customizing this for lasso ever make sense.
If it should always be a constant value and can't be customized, we shouldn't expose it in the specification.

If it can be customized, better add an example to demonstrate at least one practical use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dvmoritzschoefl: the typings here generate the JSON Schema and/or serve as the public interface for what Vega-Lite users believe can be specified. You're quite right that the lasso selection's predicate uses SELECTION_ID, but it does not make sense to customize it to use other fields (i.e., your predicate function is hard coded to vlSelectionIdTest) or for customizing the field to affect how lassoing works operationally/mechanically (i.e., how we can get a uni-dimensional brush by specifying "encodings": ["x"] for interval selections).

So I think @kanitw is quite right that this property should be dropped here — great catch @kanitw!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for the clarification that makes sense. So removing both this property and the initial config for it should do the trick?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kanitw It seems that the CLI is generating the faulty images after a new commit. Is there an easy way to test why it is generating empty images? I tested the examples locally and they seem correct. Also the other images are correctly created in the runtime tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kanitw @arvind Was I correct under the assumption that the 2 images were in the wrong folder? Is it resolved then or is there another thing to do before this can be merged?

}

export interface SelectionParameter<T extends SelectionType = SelectionType> {
/**
* 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".
Expand All @@ -212,7 +231,15 @@ export interface SelectionParameter<T extends SelectionType = SelectionType> {
* - `"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 'lasso'
? LassoSelectionConfig
: 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.
Expand Down Expand Up @@ -282,6 +309,8 @@ export type PointSelectionConfigWithoutType = Omit<PointSelectionConfig, 'type'>

export type IntervalSelectionConfigWithoutType = Omit<IntervalSelectionConfig, 'type'>;

export type LassoSelectionConfigWithoutType = Omit<LassoSelectionConfig, 'type'>;

export interface SelectionConfig {
/**
* The default definition for a [`point`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations
Expand All @@ -299,6 +328,12 @@ export interface SelectionConfig {
* interval selections by default.
*/
interval?: IntervalSelectionConfigWithoutType;

/**
* The default definition for an [`lasso`](https://vega.github.io/vega-lite/docs/parameter.html#select) selection. All properties and transformations
* for an lasso selection definition (except `type`) may be specified here.
*/
lasso?: LassoSelectionConfigWithoutType;
}

export const defaultConfig: SelectionConfig = {
Expand All @@ -317,6 +352,13 @@ export const defaultConfig: SelectionConfig = {
mark: {fill: '#333', fillOpacity: 0.125, stroke: 'white'},
resolve: 'global',
clear: 'dblclick'
},
lasso: {
on: '[mousedown, window:mouseup] > window:mousemove!',
resolve: 'global',
fields: [SELECTION_ID],
mark: {fill: '#333', fillOpacity: 0.125, stroke: 'gray', strokeWidth: 2, strokeDash: [8, 5]},
clear: 'dblclick'
}
};

Expand Down