diff --git a/examples/specs/interactive_brush.vl.json b/examples/specs/interactive_brush.vl.json index 04246ebaf6..80b3e08007 100644 --- a/examples/specs/interactive_brush.vl.json +++ b/examples/specs/interactive_brush.vl.json @@ -4,7 +4,8 @@ "data": {"url": "data/cars.json"}, "selection": { "brush": { - "type": "interval" + "type": "interval", + "init": {"x": [55, 160], "y": [13, 37]} } }, "mark": "point", diff --git a/src/compile/selection/interval.ts b/src/compile/selection/interval.ts index 6aa56e1e2c..29a8febe7b 100644 --- a/src/compile/selection/interval.ts +++ b/src/compile/selection/interval.ts @@ -39,14 +39,14 @@ const interval: SelectionCompiler = { }); } - for (const p of selCmpt.project) { + selCmpt.project.forEach((p, i) => { const channel = p.channel; if (channel !== X && channel !== Y) { warn('Interval selections only support x and y encoding channels.'); - continue; + return; } - const cs = channelSignals(model, selCmpt, channel); + const cs = channelSignals(model, selCmpt, channel, i); const dname = channelSignalName(selCmpt, channel, 'data'); const vname = channelSignalName(selCmpt, channel, 'visual'); const scaleStr = stringValue(model.scaleName(channel)); @@ -63,7 +63,7 @@ const interval: SelectionCompiler = { `(${toNum}invert(${scaleStr}, ${vname})[0] === ${toNum}${dname}[0] && ` + `${toNum}invert(${scaleStr}, ${vname})[1] === ${toNum}${dname}[1]))` }); - } + }); // Proxy scale reactions to ensure that an infinite loop doesn't occur // when an interval selection filter touches the scale. @@ -77,15 +77,20 @@ const interval: SelectionCompiler = { // Only add an interval to the store if it has valid data extents. Data extents // are set to null if pixel extents are equal to account for intervals over // ordinal/nominal domains which, when inverted, will still produce a valid datum. + const init = selCmpt.init; + const update = `unit: ${unitName(model)}, fields: ${fieldsSg}, values`; return signals.concat({ name: name + TUPLE, + ...(init + ? { + update: `{${update}: ${JSON.stringify(init)}}`, + react: false + } + : {}), on: [ { events: dataSignals.map(t => ({signal: t})), - update: - dataSignals.join(' && ') + - ` ? {unit: ${unitName(model)}, fields: ${fieldsSg}, ` + - `values: [${dataSignals.join(', ')}]} : null` + update: dataSignals.join(' && ') + ` ? {${update}: [${dataSignals}]} : null` } ] }); @@ -177,17 +182,19 @@ export default interval; /** * Returns the visual and data signals for an interval selection. */ -function channelSignals(model: UnitModel, selCmpt: SelectionComponent, channel: 'x' | 'y'): any { +function channelSignals(model: UnitModel, selCmpt: SelectionComponent, channel: 'x' | 'y', idx: number): any { const vname = channelSignalName(selCmpt, channel, 'visual'); const dname = channelSignalName(selCmpt, channel, 'data'); + const init = selCmpt.init && (selCmpt.init[idx] as number[] | string[]); const hasScales = scales.has(selCmpt); - const scaleName = model.scaleName(channel); - const scaleStr = stringValue(scaleName); + const scaleName = stringValue(model.scaleName(channel)); const scale = model.getScaleComponent(channel); const scaleType = scale ? scale.get('type') : undefined; const size = model.getSizeSignalRef(channel === X ? 'width' : 'height').signal; const coord = `${channel}(unit)`; + const scaleStr = (arr: string[]) => '[' + arr.map(s => `scale(${scaleName}, ${s})`) + ']'; + const on = events(selCmpt, (def: any[], evt: VgEventStream) => { return def.concat( {events: evt.between[0], update: `[${coord}, ${coord}]`}, // Brush Start @@ -201,9 +208,7 @@ function channelSignals(model: UnitModel, selCmpt: SelectionComponent, channel: on.push({ events: {signal: selCmpt.name + SCALE_TRIGGER}, update: - hasContinuousDomain(scaleType) && !isBinScale(scaleType) - ? `[scale(${scaleStr}, ${dname}[0]), scale(${scaleStr}, ${dname}[1])]` - : `[0, 0]` + hasContinuousDomain(scaleType) && !isBinScale(scaleType) ? scaleStr([`${dname}[0]`, `${dname}[1]`]) : `[0, 0]` }); return hasScales @@ -211,12 +216,23 @@ function channelSignals(model: UnitModel, selCmpt: SelectionComponent, channel: : [ { name: vname, - value: [], + ...(init + ? { + update: scaleStr([init[0], init[init.length - 1]].map(x => JSON.stringify(x))), + react: false + } + : {value: []}), on: on }, { name: dname, - on: [{events: {signal: vname}, update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleStr}, ${vname})`}] + ...(init ? {value: init} : {}), + on: [ + { + events: {signal: vname}, + update: `${vname}[0] === ${vname}[1] ? null : invert(${scaleName}, ${vname})` + } + ] } ]; } diff --git a/src/compile/selection/multi.ts b/src/compile/selection/multi.ts index 7486da129a..a82f966a83 100644 --- a/src/compile/selection/multi.ts +++ b/src/compile/selection/multi.ts @@ -28,16 +28,20 @@ export function signals(model: UnitModel, selCmpt: SelectionComponent) { // 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`; return [ { name: name + TUPLE, - update: init ? `{unit: ${unitName(model)}, fields: ${fieldsSg}, values: ${JSON.stringify(selCmpt.init)}}` : '', + ...(init + ? { + update: `{${update}: ${JSON.stringify(init)}}`, + react: false + } + : {value: []}), on: [ { events: selCmpt.events, - update: - `datum && item().mark.marktype !== 'group' ? ` + - `{unit: ${unitName(model)}, fields: ${fieldsSg}, values: [${values}]} : null`, + update: `datum && item().mark.marktype !== 'group' ? {${update}: [${values}]} : null`, force: true } ] diff --git a/src/compile/selection/selection.ts b/src/compile/selection/selection.ts index dbcd3d55da..8aa76aed76 100644 --- a/src/compile/selection/selection.ts +++ b/src/compile/selection/selection.ts @@ -28,7 +28,7 @@ export const VL_SELECTION_RESOLVE = 'vlSelectionResolve'; export interface SelectionComponent { name: string; type: SelectionType; - init?: (number | string)[]; + init?: (number | string | number[] | string[])[]; events: VgEventStream; // predicate?: string; bind?: 'scales' | VgBinding | Dict; diff --git a/src/selection.ts b/src/selection.ts index 202d782358..1a5577eb82 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -42,7 +42,7 @@ export interface BaseSelectionDef { * When set to `none`, empty selections contain no data values. */ empty?: 'all' | 'none'; - init?: {[key: string]: number | string}; + init?: {[key: string]: number | string | number[] | string[]}; } export interface SingleSelectionConfig extends BaseSelectionDef {