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

Add interactor mark selection to vgplot #158

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
16 changes: 14 additions & 2 deletions dev/yaml/highlight-toggle.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions docs/api/vgplot/interactors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)`
Expand All @@ -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.
28 changes: 18 additions & 10 deletions packages/vgplot/src/directives/interactors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

This approach has some possible leakage: the index is used to find the mark, but the index is also passed as an option to the interactor class itself. The index is not currently an option that is formally accepted by those classes; if it were added in the future it would have to conform exactly with how it is being used here.

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 }) {
Expand Down
4 changes: 2 additions & 2 deletions packages/vgplot/src/directives/marks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions packages/vgplot/src/interactors/Toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand All @@ -26,7 +27,7 @@ export class Toggle {
}

clause(value) {
const { channels, clients } = this;
const { channels, mark } = this;
let predicate = null;

if (value) {
Expand All @@ -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
};
Expand Down
12 changes: 9 additions & 3 deletions packages/vgplot/src/plot-renderer.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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];
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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]);
}
Expand Down