diff --git a/src/compile/selection/assemble.ts b/src/compile/selection/assemble.ts index 57d11710ae..e3069c4795 100644 --- a/src/compile/selection/assemble.ts +++ b/src/compile/selection/assemble.ts @@ -4,7 +4,7 @@ import {identity, isArray, stringValue} from 'vega-util'; import {MODIFY, STORE, unitName, VL_SELECTION_RESOLVE, TUPLE, selectionCompilers} from '.'; import {dateTimeToExpr, isDateTime, dateTimeToTimestamp} from '../../datetime'; import {hasContinuousDomain} from '../../scale'; -import {SelectionInit, SelectionInitInterval, ParameterExtent} from '../../selection'; +import {SelectionInit, SelectionInitInterval, ParameterExtent, SELECTION_ID} from '../../selection'; import {keys, stringify, vals} from '../../util'; import {VgData, VgDomain} from '../../vega.schema'; import {FacetModel} from '../facet'; @@ -114,23 +114,29 @@ export function assembleTopLevelSignals(model: UnitModel, signals: Signal[]) { export function assembleUnitSelectionData(model: UnitModel, data: readonly VgData[]): VgData[] { const dataCopy = [...data]; + const unit = unitName(model, {escape: false}); + for (const selCmpt of vals(model.component.selection ?? {})) { - const init: VgData = {name: selCmpt.name + STORE}; + const store: VgData = {name: selCmpt.name + STORE}; + + if (selCmpt.project.hasSelectionId) { + store.transform = [{type: 'collect', sort: {field: SELECTION_ID}}]; + } + if (selCmpt.init) { const fields = selCmpt.project.items.map(proj => { const {signals, ...rest} = proj; return rest; }); - init.values = selCmpt.init.map(i => ({ - unit: unitName(model, {escape: false}), - fields, - values: assembleInit(i, false) - })); + store.values = selCmpt.project.hasSelectionId + ? selCmpt.init.map(i => ({unit, [SELECTION_ID]: assembleInit(i, false)[0]})) + : selCmpt.init.map(i => ({unit, fields, values: assembleInit(i, false)})); } + const contains = dataCopy.filter(d => d.name === selCmpt.name + STORE); if (!contains.length) { - dataCopy.push(init); + dataCopy.push(store); } } diff --git a/src/compile/selection/index.ts b/src/compile/selection/index.ts index 2e46ecf741..604e84a637 100644 --- a/src/compile/selection/index.ts +++ b/src/compile/selection/index.ts @@ -7,8 +7,7 @@ import { SelectionInit, SelectionInitInterval, SelectionResolution, - SelectionType, - SELECTION_ID + SelectionType } from '../../selection'; import {Dict, vals} from '../../util'; import {OutputNode} from '../data/dataflow'; @@ -110,7 +109,7 @@ export function unitName(model: Model, {escape} = {escape: true}) { export function requiresSelectionId(model: Model) { return vals(model.component.selection ?? {}).reduce((identifier, selCmpt) => { - return identifier || selCmpt.project.items.some(proj => proj.field === SELECTION_ID); + return identifier || selCmpt.project.hasSelectionId; }, false); } diff --git a/src/compile/selection/parse.ts b/src/compile/selection/parse.ts index 32708996b2..554ef8fa36 100644 --- a/src/compile/selection/parse.ts +++ b/src/compile/selection/parse.ts @@ -90,9 +90,9 @@ export function parseSelectionPredicate( } } - const test = `vlSelectionTest(${store}, ${datum}${ - selCmpt.resolve === 'global' ? ')' : `, ${stringValue(selCmpt.resolve)})` - }`; + const fn = selCmpt.project.hasSelectionId ? 'vlSelectionIdTest(' : 'vlSelectionTest('; + const resolve = selCmpt.resolve === 'global' ? ')' : `, ${stringValue(selCmpt.resolve)})`; + const test = `${fn}${store}, ${datum}${resolve}`; const length = `length(data(${store}))`; return pred.empty === false ? `${length} && ${test}` : `!${length} || ${test}`; diff --git a/src/compile/selection/point.ts b/src/compile/selection/point.ts index 8d9755aee7..137386cfcc 100644 --- a/src/compile/selection/point.ts +++ b/src/compile/selection/point.ts @@ -1,6 +1,7 @@ import {Stream} from 'vega'; import {stringValue} from 'vega-util'; import {SelectionCompiler, TUPLE, unitName} from '.'; +import {SELECTION_ID} from '../../selection'; import {vals} from '../../util'; import {BRUSH} from './interval'; import {TUPLE_FIELDS} from './project'; @@ -13,16 +14,6 @@ const point: SelectionCompiler<'point'> = { const fieldsSg = name + TUPLE_FIELDS; const project = selCmpt.project; const datum = '(item().isVoronoi ? datum.datum : datum)'; - const values = project.items - .map(p => { - const fieldDef = model.fieldDef(p.channel); - // Binned fields should capture extents, for a range test against the raw field. - return fieldDef?.bin - ? `[${datum}[${stringValue(model.vgField(p.channel, {}))}], ` + - `${datum}[${stringValue(model.vgField(p.channel, {binSuffix: 'end'}))}]]` - : `${datum}[${stringValue(p.field)}]`; - }) - .join(', '); // Only add a discrete selection to the store if a datum is present _and_ // the interaction isn't occurring on a group mark. This guards against @@ -31,10 +22,6 @@ const point: SelectionCompiler<'point'> = { // for constant null states but varying toggles (e.g., shift-click in // whitespace followed by a click in whitespace; the store should only // be cleared on the second click). - const update = `unit: ${unitName(model)}, fields: ${fieldsSg}, values`; - - const events: Stream[] = selCmpt.events; - const brushes = vals(model.component.selection ?? {}) .reduce((acc, cmpt) => { return cmpt.type === 'interval' ? acc.concat(cmpt.name + BRUSH) : acc; @@ -44,6 +31,26 @@ const point: SelectionCompiler<'point'> = { const test = `datum && item().mark.marktype !== 'group'${brushes ? ` && ${brushes}` : ''}`; + let update = `unit: ${unitName(model)}, `; + + if (selCmpt.project.hasSelectionId) { + update += `${SELECTION_ID}: ${datum}[${stringValue(SELECTION_ID)}]`; + } else { + const values = project.items + .map(p => { + const fieldDef = model.fieldDef(p.channel); + // Binned fields should capture extents, for a range test against the raw field. + return fieldDef?.bin + ? `[${datum}[${stringValue(model.vgField(p.channel, {}))}], ` + + `${datum}[${stringValue(model.vgField(p.channel, {binSuffix: 'end'}))}]]` + : `${datum}[${stringValue(p.field)}]`; + }) + .join(', '); + + update += `fields: ${fieldsSg}, values: [${values}]`; + } + + const events: Stream[] = selCmpt.events; return signals.concat([ { name: name + TUPLE, @@ -51,7 +58,7 @@ const point: SelectionCompiler<'point'> = { ? [ { events, - update: `${test} ? {${update}: [${values}]} : null`, + update: `${test} ? {${update}} : null`, force: true } ] diff --git a/src/compile/selection/project.ts b/src/compile/selection/project.ts index 6290d4162c..706e75ea16 100644 --- a/src/compile/selection/project.ts +++ b/src/compile/selection/project.ts @@ -2,7 +2,7 @@ import {array, isObject} from 'vega-util'; import {isSingleDefUnitChannel, ScaleChannel, SingleDefUnitChannel} from '../../channel'; import * as log from '../../log'; import {hasContinuousDomain} from '../../scale'; -import {PointSelectionConfig, SelectionInitIntervalMapping, SelectionInitMapping} from '../../selection'; +import {PointSelectionConfig, SelectionInitIntervalMapping, SelectionInitMapping, SELECTION_ID} from '../../selection'; import {Dict, hash, keys, replacePathInField, varName, isEmpty} from '../../util'; import {TimeUnitComponent, TimeUnitNode} from '../data/timeunit'; import {SelectionCompiler} from '.'; @@ -30,6 +30,7 @@ export interface SelectionProjection { export class SelectionProjectionComponent { public hasChannel: Partial>; public hasField: Record; + public hasSelectionId: boolean; public timeUnit?: TimeUnitNode; public items: SelectionProjection[]; @@ -37,6 +38,7 @@ export class SelectionProjectionComponent { this.items = items; this.hasChannel = {}; this.hasField = {}; + this.hasSelectionId = false; } } @@ -152,6 +154,7 @@ const project: SelectionCompiler = { p.signals = {...signalName(p, 'data'), ...signalName(p, 'visual')}; proj.items.push((parsed[field] = p)); proj.hasField[field] = proj.hasChannel[channel] = parsed[field]; + proj.hasSelectionId = proj.hasSelectionId || field === SELECTION_ID; } } else { log.warn(log.message.cannotProjectOnChannelWithoutField(channel)); @@ -164,6 +167,7 @@ const project: SelectionCompiler = { p.signals = {...signalName(p, 'data')}; proj.items.push(p); proj.hasField[field] = p; + proj.hasSelectionId = proj.hasSelectionId || field === SELECTION_ID; } if (init) { @@ -182,7 +186,7 @@ const project: SelectionCompiler = { signals: (model, selCmpt, allSignals) => { const name = selCmpt.name + TUPLE_FIELDS; const hasSignal = allSignals.filter(s => s.name === name); - return hasSignal.length > 0 + return hasSignal.length > 0 || selCmpt.project.hasSelectionId ? allSignals : allSignals.concat({ name,