From 4636034e8580f55f5c9b84fc7f2d772b3b972664 Mon Sep 17 00:00:00 2001 From: Torsten Sprenger Date: Fri, 25 Aug 2023 14:55:29 +0200 Subject: [PATCH] Add mark selection via indexes for interactors --- dev/yaml/highlight-toggle.yaml | 16 +++++++++-- docs/api/vgplot/interactors.md | 11 ++++++-- packages/vgplot/src/directives/interactors.js | 28 ++++++++++++------- packages/vgplot/src/directives/marks.js | 4 +-- packages/vgplot/src/interactors/Toggle.js | 9 +++--- packages/vgplot/src/plot-renderer.js | 12 ++++++-- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/dev/yaml/highlight-toggle.yaml b/dev/yaml/highlight-toggle.yaml index c676f22a..4202cd52 100644 --- a/dev/yaml/highlight-toggle.yaml +++ b/dev/yaml/highlight-toggle.yaml @@ -21,16 +21,28 @@ vconcat: - vspace: 20 - name: sex plot: + - mark: gridY - mark: barY data: { from: penguins, filterBy: $filter } x: sex y: { count: '' } - - select: highlight - by: $select + - mark: textY + data: { from: penguins, filterBy: $filter } + x: sex + y: { count: '' } + text: { count: '' } + fill: '#efefef' + dy: 10 - select: toggleX as: $select + index: 1 + - select: highlight + by: $select + index: 1 + opacity: 0.5 - select: toggleX as: $filter + index: 1 xLabel: '' yLabel: Total xDomain: Fixed diff --git a/docs/api/vgplot/interactors.md b/docs/api/vgplot/interactors.md index 4a99fdde..b6728fe4 100644 --- a/docs/api/vgplot/interactors.md +++ b/docs/api/vgplot/interactors.md @@ -3,7 +3,7 @@ Interactors imbue plots with interactive behavior, such as selecting or highlighting values, and panning or zooming the display. To determine which fields (database columns) an interactor should select, an interactor defaults to looking at the corresponding encoding channels for the most recently added mark. -Alternatively, interactors accept options that explicitly indicate which data fields should be selected. +Alternatively, interactors accept options that explicitly indicate which mark and data fields should be selected. ## toggle @@ -13,6 +13,8 @@ Select individual data values by clicking / shift-clicking points. The supported - _as_: The [Selection](../core/selection) to populate with filter predicates. - _channels_: An array of encoding channels (e.g., `"x"`, `"y"`, `"color"`) indicating the data values to select. +- _peers_: A Boolean-flag (default `true`) indicating if all marks in the current plot should be considered "peers" in the clients set used to perform cross-filtering. A peer mark will be exempt from filtering. Set this to false if you are using a cross-filtered selection but want to filter across marks within the same plot. +- _index_: An optional index of mark which should be used for interaction. ### toggleX @@ -45,6 +47,7 @@ Select the nearest value along the x dimension. The supported _options_ are: - _as_: The [Selection](../core/selection) to populate with filter predicates. - _field_: The field to select. If not specified, the field backing the `"x"` encoding channel of the most recently added mark is used. +- _index_: An optional index of mark which should be used for interaction. ### nearestY @@ -54,6 +57,7 @@ Select the nearest value along the y dimension. The supported _options_ are: - _as_: The [Selection](../core/selection) to populate with filter predicates. - _field_: The field to select. If not specified, the field backing the `"x"` encoding channel of the most recently added mark is used. +- _index_: An optional index of mark which should be used for interaction. ## interval @@ -70,6 +74,7 @@ Select a 1D interval range along the x dimension. The supported _options_ are: - _pixelSize_: The size of an interactive "pixel" (default 1). If set larger, the interval brush will "snap" to a grid larger than visible pixels. In some cases this can be helpful to improve scalability to large data by reducing interactive resolution. - _peers_: A Boolean-flag (default `true`) indicating if all marks in the current plot should be considered "peers" in the clients set used to perform cross-filtering. A peer mark will be exempt from filtering. Set this to false if you are using a cross-filtered selection but want to filter across marks within the same plot. - _brush_: An optional object that provides CSS styles for the visible brush. +- _index_: An optional index of mark which should be used for interaction. ### intervalY @@ -82,6 +87,7 @@ Select a 1D interval range along the y dimension. The supported _options_ are: - _pixelSize_: The size of an interactive "pixel" (default 1). If set larger, the interval brush will "snap" to a grid larger than visible pixels. In some cases this can be helpful to improve scalability to large data by reducing interactive resolution. - _peers_: A Boolean-flag (default `true`) indicating if all marks in the current plot should be considered "peers" in the clients set used to perform cross-filtering. A peer mark will be exempt from filtering. Set this to false if you are using a cross-filtered selection but want to filter across marks within the same plot. - _brush_: An optional object that provides CSS styles for the visible brush. +- _index_: An optional index of mark which should be used for interaction. ### intervalXY @@ -95,6 +101,7 @@ Select a 2D interval range along the x and y dimensions. The supported _options_ - _pixelSize_: The size of an interactive "pixel" (default 1). If set larger, the interval brush will "snap" to a grid larger than visible pixels. In some cases this can be helpful to improve scalability to large data by reducing interactive resolution. - _peers_: A Boolean-flag (default `true`) indicating if all marks in the current plot should be considered "peers" in the clients set used to perform cross-filtering. A peer mark will be exempt from filtering. Set this to false if you are using a cross-filtered selection but want to filter across marks within the same plot. - _brush_: An optional object that provides CSS styles for the visible brush. +- _index_: An optional index of mark which should be used for interaction. ## pan & zoom @@ -160,7 +167,6 @@ The supported _options_ are listed above. Pan or zoom the plot in the `y` dimension only. The supported _options_ are listed above. - ## highlight `highlight(options)` @@ -171,3 +177,4 @@ Unselected values are deemphasized. - _by_: The [Selection](../core/selection) driving the highlighting. - _channels_: An optional object of channel/value mappings that defines what CSS styles to apply to deemphasized items. The default value is to set the `opacity` channel to `0.2`. +- _index_: An optional index of mark which should be used for interaction. \ No newline at end of file diff --git a/packages/vgplot/src/directives/interactors.js b/packages/vgplot/src/directives/interactors.js index d51a9478..fef8b3cf 100644 --- a/packages/vgplot/src/directives/interactors.js +++ b/packages/vgplot/src/directives/interactors.js @@ -7,29 +7,37 @@ import { Nearest } from '../interactors/Nearest.js'; function interactor(InteractorClass, options) { return plot => { - const mark = plot.marks[plot.marks.length - 1]; + const mark = findMark(plot.marks, options.index); plot.addInteractor(new InteractorClass(mark, options)); }; } -export function highlight({ by, ...channels }) { - return interactor(Highlight, { selection: by, channels }); +function findMark(marks, index) { + const mark = marks[index ?? marks.length - 1]; + + if (!mark) throw new Error(`Mark not found with index ${index}.`); + + return mark; +} + +export function highlight({ by, index, ...channels }) { + return interactor(Highlight, { selection: by, index, channels }); } export function toggle({ as, ...rest }) { - return interactor(Toggle, { ...rest, selection: as }); + return interactor(Toggle, { selection: as, ...rest }); } -export function toggleX({ as }) { - return toggle({ as, channels: ['x'] }); +export function toggleX({ as, index }) { + return toggle({ as, index, channels: ['x'] }); } -export function toggleY({ as }) { - return toggle({ as, channels: ['y'] }); +export function toggleY({ as, index }) { + return toggle({ as, index, channels: ['y'] }); } -export function toggleColor({ as }) { - return toggle({ as, channels: ['color'] }); +export function toggleColor({ as, index }) { + return toggle({ as, index, channels: ['color'] }); } export function nearestX({ as, ...rest }) { diff --git a/packages/vgplot/src/directives/marks.js b/packages/vgplot/src/directives/marks.js index c2548263..a135b32a 100644 --- a/packages/vgplot/src/directives/marks.js +++ b/packages/vgplot/src/directives/marks.js @@ -9,7 +9,7 @@ import { RasterMark } from '../marks/RasterMark.js'; import { RasterTileMark } from '../marks/RasterTileMark.js'; import { RegressionMark } from '../marks/RegressionMark.js'; -const decorators = new Set([ +export const DECORATOR_MARKS = new Set([ 'frame', 'axisX', 'axisY', 'axisFx', 'axisFy', 'gridX', 'gridY', 'gridFx', 'gridFy', @@ -20,7 +20,7 @@ const decorators = new Set([ function mark(type, data, channels) { if (arguments.length === 2) { channels = data; - data = decorators.has(type) ? null : [{}]; + data = DECORATOR_MARKS.has(type) ? null : [{}]; } const MarkClass = type.startsWith('area') || type.startsWith('line') ? ConnectedMark diff --git a/packages/vgplot/src/interactors/Toggle.js b/packages/vgplot/src/interactors/Toggle.js index b62a5ef9..6011f7e3 100644 --- a/packages/vgplot/src/interactors/Toggle.js +++ b/packages/vgplot/src/interactors/Toggle.js @@ -3,12 +3,13 @@ import { and, or, isNotDistinct, literal } from '@uwdata/mosaic-sql'; export class Toggle { constructor(mark, { selection, - channels + channels, + peers = true, }) { this.value = null; this.mark = mark; this.selection = selection; - this.clients = new Set().add(mark); + this.peers = peers; this.channels = channels.map(c => { const q = c === 'color' ? ['fill', 'stroke'] : c === 'x' ? ['x', 'x1', 'x2'] @@ -26,7 +27,7 @@ export class Toggle { } clause(value) { - const { channels, clients } = this; + const { channels, mark } = this; let predicate = null; if (value) { @@ -42,7 +43,7 @@ export class Toggle { return { source: this, schema: { type: 'point' }, - clients, + clients: this.peers ? mark.plot.markSet : new Set().add(mark), value, predicate }; diff --git a/packages/vgplot/src/plot-renderer.js b/packages/vgplot/src/plot-renderer.js index b4a6774f..39407c80 100644 --- a/packages/vgplot/src/plot-renderer.js +++ b/packages/vgplot/src/plot-renderer.js @@ -1,6 +1,7 @@ import * as Plot from '@observablehq/plot'; import { attributeMap } from './plot-attributes.js'; import { Fixed } from './symbols.js'; +import { DECORATOR_MARKS } from './directives/marks.js'; const OPTIONS_ONLY_MARKS = new Set([ 'frame', @@ -9,6 +10,12 @@ const OPTIONS_ONLY_MARKS = new Set([ 'graticule' ]); +const DECORATOR_ARIA_LABELS = new Set( + Array + .from(DECORATOR_MARKS) + .map((type) => type.replace(/(X|Y|Fx|Fy)$/g, '')) +); + function setProperty(object, path, value) { for (let i = 0; i < path.length; ++i) { const key = path[i]; @@ -51,7 +58,7 @@ export async function plotRenderer(plot) { } else { spec.marks.push(Plot[type](data, options)); } - indices.push(mark.index); + if (!DECORATOR_MARKS.has(type)) indices.push(mark.index); } } @@ -162,8 +169,7 @@ function annotateMarks(svg, indices) { for (const child of svg.children) { const aria = child.getAttribute('aria-label') || ''; const skip = child.nodeName === 'style' - || aria.includes('-axis') - || aria.includes('-grid'); + || Array.from(DECORATOR_ARIA_LABELS).some((label) => aria.includes(label)); if (!skip) { child.setAttribute('data-index', indices[++index]); }