From 9679a969e7c0d683d407d1ef9471e55e704c6997 Mon Sep 17 00:00:00 2001 From: Arvind Satyanarayan Date: Tue, 5 Jan 2021 11:39:51 -0500 Subject: [PATCH] Preliminary geo intervals init. --- src/compile/selection/interval.ts | 28 +- src/compile/selection/project.ts | 34 +- src/log/message.ts | 6 +- test/compile/selection/interval.test.ts | 1529 ++++++++++++----------- 4 files changed, 816 insertions(+), 781 deletions(-) diff --git a/src/compile/selection/interval.ts b/src/compile/selection/interval.ts index 51d815b1e2..399dfcca09 100644 --- a/src/compile/selection/interval.ts +++ b/src/compile/selection/interval.ts @@ -13,6 +13,7 @@ import scales from './scales'; export const BRUSH = '_brush'; export const SCALE_TRIGGER = '_scale_trigger'; +const INIT = '_init'; export interface IntervalSelectionComponent { type: 'id' | 'scaled'; @@ -66,7 +67,7 @@ const interval: SelectionCompiler<'interval'> = { const init = selCmpt.init ? selCmpt.init[0] : null; signals.push( - ...channels.reduce((arr, proj, i) => arr.concat(channelSignals(model, selCmpt, proj, init && init[i])), []) + ...channels.reduce((arr, proj) => arr.concat(channelSignals(model, selCmpt, proj, init && init[proj.index])), []) ); if (selCmpt.interval.type === 'scaled') { @@ -115,9 +116,12 @@ const interval: SelectionCompiler<'interval'> = { ] }); } else { + const projection = stringValue(model.projectionName()); const {x, y} = selCmpt.project.hasChannel; const xvname = x && x.signals.visual; const yvname = y && y.signals.visual; + const xinit = init && init[x.index]; + const yinit = init && init[y.index]; const bbox = `[` + `[${xvname ? xvname + '[0]' : '0'}, ${yvname ? yvname + '[0]' : '0'}],` + @@ -127,10 +131,19 @@ const interval: SelectionCompiler<'interval'> = { const intersect = `intersect(${bbox}, {markname: ${stringValue(model.getName('marks'))}}, unit.mark)`; const base = `{unit: ${unitName(model)}, fields: [${name + TUPLE_FIELDS}[${selCmpt.project.selectionIdIdx}]]}`; - return signals.concat({ - name: tupleSg, - update: `vlSelectionTuples(${intersect}, ${base})` - }); + return [ + { + name: name + INIT, + init: init + ? `[scale(${projection}, [${xinit[0]}, ${yinit[0]}]), scale(${projection}, [${xinit[1]}, ${yinit[1]}])]` + : null + }, + ...signals, + { + name: tupleSg, + update: `vlSelectionTuples(${intersect}, ${base})` + } + ]; } }, @@ -232,7 +245,6 @@ function channelSignals( const scaleName = stringValue(scaledInterval ? model.scaleName(channel) : model.projectionName()); const scaled = (str: string) => `scale(${scaleName}, ${str})`; - const vinit: SignalValue = init ? {init: assembleInit(init, true, scaled)} : {value: []}; const size = model.getSizeSignalRef(channel === X ? 'width' : 'height').signal; const coord = `${channel}(unit)`; @@ -249,6 +261,7 @@ function channelSignals( const hasScales = scales.defined(selCmpt); const scale = model.getScaleComponent(channel as ScaleChannel); const scaleType = scale ? scale.get('type') : undefined; + const vinit: SignalValue = init ? {init: assembleInit(init, true, scaled)} : {value: []}; // React to pan/zooms of continuous scales. Non-continuous scales // (band, point) cannot be pan/zoomed and any other changes @@ -274,6 +287,9 @@ function channelSignals( } ]; } else { + const initIdx = channel === X ? 0 : 1; + const initSg = selCmpt.name + INIT; + const vinit: SignalValue = init ? {init: `[${initSg}[0][${initIdx}], ${initSg}[1][${initIdx}]]`} : {value: []}; return [{name: vname, ...vinit, on: von}]; } } diff --git a/src/compile/selection/project.ts b/src/compile/selection/project.ts index a3d05983d3..236ab095a6 100644 --- a/src/compile/selection/project.ts +++ b/src/compile/selection/project.ts @@ -1,5 +1,6 @@ import {array, isObject} from 'vega-util'; import { + GeoPositionChannel, getPositionChannelFromLatLong, isGeoPositionChannel, isScaleChannel, @@ -28,7 +29,9 @@ export type TupleStoreType = export interface SelectionProjection { type: TupleStoreType; field: string; + index: number; channel?: SingleDefUnitChannel; + geoChannel?: GeoPositionChannel; signals?: {data?: string; visual?: string}; hasLegend?: boolean; } @@ -81,6 +84,10 @@ const project: SelectionCompiler = { ? (array(selDef.value as any) as SelectionInitMapping[] | SelectionInitIntervalMapping[]) : null; + if (init && selCmpt.type === 'interval' && model.hasProjection && init[0].length !== 2) { + log.warn(log.message.INITIALIZE_GEO_INTERVAL); + } + // If no explicit projection (either fields or encodings) is specified, set some defaults. // If an initial value is set, try to infer projections. let {fields, encodings} = isObject(selDef.select) ? selDef.select : ({} as BaseSelectionConfig); @@ -96,10 +103,10 @@ const project: SelectionCompiler = { (encodings || (encodings = [])).push(key as SingleDefUnitChannel); } else { if (type === 'interval') { - log.warn(log.message.INTERVAL_INITIALIZED_WITH_X_Y); + log.warn(log.message.INTERVAL_INITIALIZED_WITH_POS); encodings = cfg.encodings; } else { - (fields || (fields = [])).push(key); + (fields ??= []).push(key); } } } @@ -157,15 +164,18 @@ const project: SelectionCompiler = { ? 'R-RE' : 'E'; - const posChannel = isGeoPositionChannel(channel) ? getPositionChannelFromLatLong(channel) : channel; - const p: SelectionProjection = { - field, - channel: posChannel, - type: tplType - }; + const p: SelectionProjection = {field, channel, type: tplType, index: proj.items.length}; p.signals = {...signalName(p, 'data'), ...signalName(p, 'visual')}; proj.items.push((parsed[field] = p)); - proj.hasField[field] = proj.hasChannel[posChannel] = parsed[field]; + proj.hasField[field] = parsed[field]; + + if (isGeoPositionChannel(channel)) { + p.geoChannel = channel; + p.channel = getPositionChannelFromLatLong(channel); + proj.hasChannel[p.channel] = parsed[field]; + } else { + proj.hasChannel[channel] = parsed[field]; + } } } else { log.warn(log.message.cannotProjectOnChannelWithoutField(channel)); @@ -175,7 +185,7 @@ const project: SelectionCompiler = { // TODO: find a possible channel mapping for these fields. for (const field of fields ?? []) { if (proj.hasField[field]) continue; - const p: SelectionProjection = {type: 'E', field}; + const p: SelectionProjection = {type: 'E', field, index: proj.items.length}; p.signals = {...signalName(p, 'data')}; proj.items.push(p); proj.hasField[field] = p; @@ -186,7 +196,9 @@ const project: SelectionCompiler = { selCmpt.init = (init as any).map((v: SelectionInitMapping | SelectionInitIntervalMapping) => { // Selections can be initialized either with a full object that maps projections to values // or scalar values to smoothen the abstraction gradient from variable params to point selections. - return proj.items.map(p => (isObject(v) ? (v[p.channel] !== undefined ? v[p.channel] : v[p.field]) : v)); + return proj.items.map(p => + isObject(v) ? (v[p.geoChannel || p.channel] !== undefined ? v[p.geoChannel || p.channel] : v[p.field]) : v + ); }); } diff --git a/src/log/message.ts b/src/log/message.ts index 5f377aad7b..9b52c5f887 100644 --- a/src/log/message.ts +++ b/src/log/message.ts @@ -89,7 +89,11 @@ export function noSameUnitLookup(name: string) { export const NEEDS_SAME_SELECTION = 'The same selection must be used to override scale domains in a layered view.'; -export const INTERVAL_INITIALIZED_WITH_X_Y = 'Interval selections should be initialized using "x" and/or "y" keys.'; +export const INTERVAL_INITIALIZED_WITH_POS = + 'Interval selections should be initialized using "x", "y", "longitude", or "latitude" keys.'; + +export const INITIALIZE_GEO_INTERVAL = + 'Interval selections over cartographic projections must be initialized with both longitude and latitude.'; // REPEAT export function noSuchRepeatedValue(field: string) { diff --git a/test/compile/selection/interval.test.ts b/test/compile/selection/interval.test.ts index 3fb38f8b0d..83d88b74b1 100644 --- a/test/compile/selection/interval.test.ts +++ b/test/compile/selection/interval.test.ts @@ -5,387 +5,514 @@ import {parseUnitSelection} from '../../../src/compile/selection/parse'; import {parseUnitModel} from '../../util'; describe('Interval Selections', () => { - const model = parseUnitModel({ - mark: 'circle', - encoding: { - x: {field: 'Horsepower', type: 'quantitative'}, - y: {field: 'Miles-per-Gallon', type: 'quantitative'}, - color: {field: 'Origin', type: 'nominal'} - } - }); - model.parseScale(); + describe('Scaled intervals', () => { + const model = parseUnitModel({ + mark: 'circle', + encoding: { + x: {field: 'Horsepower', type: 'quantitative'}, + y: {field: 'Miles-per-Gallon', type: 'quantitative'}, + color: {field: 'Origin', type: 'nominal'} + } + }); + model.parseScale(); - const selCmpts = (model.component.selection = parseUnitSelection(model, [ - { - name: 'one', - select: {type: 'interval', encodings: ['x'], clear: false, translate: false, zoom: false} - }, - { - name: 'two', - select: { - type: 'interval', - encodings: ['y'], - clear: false, - translate: false, - zoom: false + const selCmpts = (model.component.selection = parseUnitSelection(model, [ + { + name: 'one', + select: {type: 'interval', encodings: ['x'], clear: false, translate: false, zoom: false} + }, + { + name: 'two', + select: { + type: 'interval', + encodings: ['y'], + clear: false, + translate: false, + zoom: false + }, + bind: 'scales' }, - bind: 'scales' - }, - { - name: 'thr-ee', - select: { - type: 'interval', - on: '[mousedown, mouseup] > mousemove, [keydown, keyup] > keypress', - clear: false, - translate: false, - zoom: false, - resolve: 'intersect', - mark: { - fill: 'red', - fillOpacity: 0.75, - stroke: 'black', - strokeWidth: 4, - strokeDash: [10, 5], - strokeDashOffset: 3, - strokeOpacity: 0.25 + { + name: 'thr-ee', + select: { + type: 'interval', + on: '[mousedown, mouseup] > mousemove, [keydown, keyup] > keypress', + clear: false, + translate: false, + zoom: false, + resolve: 'intersect', + mark: { + fill: 'red', + fillOpacity: 0.75, + stroke: 'black', + strokeWidth: 4, + strokeDash: [10, 5], + strokeDashOffset: 3, + strokeOpacity: 0.25 + } } - } - }, - { - name: 'four', - value: {x: [50, 70]}, - select: { - type: 'interval', - encodings: ['x'], - clear: false, - translate: false, - zoom: false - } - }, - { - name: 'five', - value: {x: [50, 60], y: [23, 54]}, - select: { - type: 'interval', - clear: false, - translate: false, - zoom: false - } - }, - { - name: 'six', - value: { - x: [ - {year: 2000, month: 10, date: 5}, - {year: 2001, month: 1, date: 13} - ] }, - select: { - type: 'interval', - clear: false, - translate: false, - zoom: false, - encodings: ['x'] + { + name: 'four', + value: {x: [50, 70]}, + select: { + type: 'interval', + encodings: ['x'], + clear: false, + translate: false, + zoom: false + } + }, + { + name: 'five', + value: {x: [50, 60], y: [23, 54]}, + select: { + type: 'interval', + clear: false, + translate: false, + zoom: false + } + }, + { + name: 'six', + value: { + x: [ + {year: 2000, month: 10, date: 5}, + {year: 2001, month: 1, date: 13} + ] + }, + select: { + type: 'interval', + clear: false, + translate: false, + zoom: false, + encodings: ['x'] + } } - } - ])); + ])); - describe('Tuple Signals', () => { - it('builds projection signals', () => { - const oneSg = interval.signals(model, selCmpts['one'], []); - expect(oneSg).toEqual( - expect.arrayContaining([ - { - name: 'one_x', - value: [], - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], - update: '[one_x[0], clamp(x(unit), 0, width)]' - }, - { - events: {signal: 'one_scale_trigger'}, - update: '[scale("x", one_Horsepower[0]), scale("x", one_Horsepower[1])]' - } - ] - }, - { - name: 'one_Horsepower', - on: [ - { - events: {signal: 'one_x'}, - update: 'one_x[0] === one_x[1] ? null : invert("x", one_x)' - } - ] - }, - { - name: 'one_scale_trigger', - value: {}, - on: [ - { - events: [{scale: 'x'}], - update: - '(!isArray(one_Horsepower) || (+invert("x", one_x)[0] === +one_Horsepower[0] && +invert("x", one_x)[1] === +one_Horsepower[1])) ? one_scale_trigger : {}' - } - ] - } - ]) - ); + describe('Tuple Signals', () => { + it('builds projection signals', () => { + const oneSg = interval.signals(model, selCmpts['one'], []); + expect(oneSg).toEqual( + expect.arrayContaining([ + { + name: 'one_x', + value: [], + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], + update: '[one_x[0], clamp(x(unit), 0, width)]' + }, + { + events: {signal: 'one_scale_trigger'}, + update: '[scale("x", one_Horsepower[0]), scale("x", one_Horsepower[1])]' + } + ] + }, + { + name: 'one_Horsepower', + on: [ + { + events: {signal: 'one_x'}, + update: 'one_x[0] === one_x[1] ? null : invert("x", one_x)' + } + ] + }, + { + name: 'one_scale_trigger', + value: {}, + on: [ + { + events: [{scale: 'x'}], + update: + '(!isArray(one_Horsepower) || (+invert("x", one_x)[0] === +one_Horsepower[0] && +invert("x", one_x)[1] === +one_Horsepower[1])) ? one_scale_trigger : {}' + } + ] + } + ]) + ); + + const twoSg = interval.signals(model, selCmpts['two'], []); + expect(twoSg).toContainEqual({ + name: 'two_Miles_per_Gallon', + on: [] + }); + + const threeSg = interval.signals(model, selCmpts['thr_ee'], []); + expect(threeSg).toEqual( + expect.arrayContaining([ + { + name: 'thr_ee_x', + value: [], + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0], + update: '[thr_ee_x[0], clamp(x(unit), 0, width)]' + }, + { + events: parseSelector('keydown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[keydown, keyup] > keypress', 'scope')[0], + update: '[thr_ee_x[0], clamp(x(unit), 0, width)]' + }, + { + events: {signal: 'thr_ee_scale_trigger'}, + update: '[scale("x", thr_ee_Horsepower[0]), scale("x", thr_ee_Horsepower[1])]' + } + ] + }, + { + name: 'thr_ee_Horsepower', + on: [ + { + events: {signal: 'thr_ee_x'}, + update: 'thr_ee_x[0] === thr_ee_x[1] ? null : invert("x", thr_ee_x)' + } + ] + }, + { + name: 'thr_ee_y', + value: [], + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[y(unit), y(unit)]' + }, + { + events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0], + update: '[thr_ee_y[0], clamp(y(unit), 0, height)]' + }, + { + events: parseSelector('keydown', 'scope')[0], + update: '[y(unit), y(unit)]' + }, + { + events: parseSelector('[keydown, keyup] > keypress', 'scope')[0], + update: '[thr_ee_y[0], clamp(y(unit), 0, height)]' + }, + { + events: {signal: 'thr_ee_scale_trigger'}, + update: '[scale("y", thr_ee_Miles_per_Gallon[0]), scale("y", thr_ee_Miles_per_Gallon[1])]' + } + ] + }, + { + name: 'thr_ee_Miles_per_Gallon', + on: [ + { + events: {signal: 'thr_ee_y'}, + update: 'thr_ee_y[0] === thr_ee_y[1] ? null : invert("y", thr_ee_y)' + } + ] + }, + { + name: 'thr_ee_scale_trigger', + value: {}, + on: [ + { + events: [{scale: 'x'}, {scale: 'y'}], + update: + '(!isArray(thr_ee_Horsepower) || (+invert("x", thr_ee_x)[0] === +thr_ee_Horsepower[0] && +invert("x", thr_ee_x)[1] === +thr_ee_Horsepower[1])) && (!isArray(thr_ee_Miles_per_Gallon) || (+invert("y", thr_ee_y)[0] === +thr_ee_Miles_per_Gallon[0] && +invert("y", thr_ee_y)[1] === +thr_ee_Miles_per_Gallon[1])) ? thr_ee_scale_trigger : {}' + } + ] + } + ]) + ); - const twoSg = interval.signals(model, selCmpts['two'], []); - expect(twoSg).toContainEqual({ - name: 'two_Miles_per_Gallon', - on: [] + const fourSg = interval.signals(model, selCmpts['four'], []); + expect(fourSg).toEqual( + expect.arrayContaining([ + { + name: 'four_x', + init: '[scale("x", 50), scale("x", 70)]', + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], + update: '[four_x[0], clamp(x(unit), 0, width)]' + }, + { + events: {signal: 'four_scale_trigger'}, + update: '[scale("x", four_Horsepower[0]), scale("x", four_Horsepower[1])]' + } + ] + }, + { + name: 'four_Horsepower', + init: '[50, 70]', + on: [ + { + events: {signal: 'four_x'}, + update: 'four_x[0] === four_x[1] ? null : invert("x", four_x)' + } + ] + }, + { + name: 'four_scale_trigger', + value: {}, + on: [ + { + events: [{scale: 'x'}], + update: + '(!isArray(four_Horsepower) || (+invert("x", four_x)[0] === +four_Horsepower[0] && +invert("x", four_x)[1] === +four_Horsepower[1])) ? four_scale_trigger : {}' + } + ] + } + ]) + ); + + const fiveSg = interval.signals(model, selCmpts['five'], []); + expect(fiveSg).toEqual( + expect.arrayContaining([ + { + name: 'five_x', + init: '[scale("x", 50), scale("x", 60)]', + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], + update: '[five_x[0], clamp(x(unit), 0, width)]' + }, + { + events: {signal: 'five_scale_trigger'}, + update: '[scale("x", five_Horsepower[0]), scale("x", five_Horsepower[1])]' + } + ] + }, + { + name: 'five_Horsepower', + init: '[50, 60]', + on: [ + { + events: {signal: 'five_x'}, + update: 'five_x[0] === five_x[1] ? null : invert("x", five_x)' + } + ] + }, + { + name: 'five_y', + init: '[scale("y", 23), scale("y", 54)]', + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[y(unit), y(unit)]' + }, + { + events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], + update: '[five_y[0], clamp(y(unit), 0, height)]' + }, + { + events: {signal: 'five_scale_trigger'}, + update: '[scale("y", five_Miles_per_Gallon[0]), scale("y", five_Miles_per_Gallon[1])]' + } + ] + }, + { + name: 'five_Miles_per_Gallon', + init: '[23, 54]', + on: [ + { + events: {signal: 'five_y'}, + update: 'five_y[0] === five_y[1] ? null : invert("y", five_y)' + } + ] + }, + { + name: 'five_scale_trigger', + value: {}, + on: [ + { + events: [{scale: 'x'}, {scale: 'y'}], + update: + '(!isArray(five_Horsepower) || (+invert("x", five_x)[0] === +five_Horsepower[0] && +invert("x", five_x)[1] === +five_Horsepower[1])) && (!isArray(five_Miles_per_Gallon) || (+invert("y", five_y)[0] === +five_Miles_per_Gallon[0] && +invert("y", five_y)[1] === +five_Miles_per_Gallon[1])) ? five_scale_trigger : {}' + } + ] + } + ]) + ); + + const sixSg = interval.signals(model, selCmpts['six'], []); + expect(sixSg).toEqual( + expect.arrayContaining([ + { + name: 'six_x', + init: '[scale("x", datetime(2000, 9, 5, 0, 0, 0, 0)), scale("x", datetime(2001, 0, 13, 0, 0, 0, 0))]', + on: [ + { + events: parseSelector('mousedown', 'scope')[0], + update: '[x(unit), x(unit)]' + }, + { + events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], + update: '[six_x[0], clamp(x(unit), 0, width)]' + }, + { + events: {signal: 'six_scale_trigger'}, + update: '[scale("x", six_Horsepower[0]), scale("x", six_Horsepower[1])]' + } + ] + }, + { + name: 'six_Horsepower', + init: '[datetime(2000, 9, 5, 0, 0, 0, 0), datetime(2001, 0, 13, 0, 0, 0, 0)]', + on: [ + { + events: {signal: 'six_x'}, + update: 'six_x[0] === six_x[1] ? null : invert("x", six_x)' + } + ] + }, + { + name: 'six_scale_trigger', + value: {}, + on: [ + { + events: [{scale: 'x'}], + update: + '(!isArray(six_Horsepower) || (+invert("x", six_x)[0] === +six_Horsepower[0] && +invert("x", six_x)[1] === +six_Horsepower[1])) ? six_scale_trigger : {}' + } + ] + } + ]) + ); }); - const threeSg = interval.signals(model, selCmpts['thr_ee'], []); - expect(threeSg).toEqual( - expect.arrayContaining([ - { - name: 'thr_ee_x', - value: [], - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0], - update: '[thr_ee_x[0], clamp(x(unit), 0, width)]' - }, - { - events: parseSelector('keydown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[keydown, keyup] > keypress', 'scope')[0], - update: '[thr_ee_x[0], clamp(x(unit), 0, width)]' - }, - { - events: {signal: 'thr_ee_scale_trigger'}, - update: '[scale("x", thr_ee_Horsepower[0]), scale("x", thr_ee_Horsepower[1])]' - } - ] - }, - { - name: 'thr_ee_Horsepower', - on: [ - { - events: {signal: 'thr_ee_x'}, - update: 'thr_ee_x[0] === thr_ee_x[1] ? null : invert("x", thr_ee_x)' - } - ] - }, - { - name: 'thr_ee_y', - value: [], - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[y(unit), y(unit)]' - }, - { - events: parseSelector('[mousedown, mouseup] > mousemove', 'scope')[0], - update: '[thr_ee_y[0], clamp(y(unit), 0, height)]' - }, - { - events: parseSelector('keydown', 'scope')[0], - update: '[y(unit), y(unit)]' - }, - { - events: parseSelector('[keydown, keyup] > keypress', 'scope')[0], - update: '[thr_ee_y[0], clamp(y(unit), 0, height)]' - }, - { - events: {signal: 'thr_ee_scale_trigger'}, - update: '[scale("y", thr_ee_Miles_per_Gallon[0]), scale("y", thr_ee_Miles_per_Gallon[1])]' - } - ] - }, - { - name: 'thr_ee_Miles_per_Gallon', - on: [ - { - events: {signal: 'thr_ee_y'}, - update: 'thr_ee_y[0] === thr_ee_y[1] ? null : invert("y", thr_ee_y)' - } - ] - }, - { - name: 'thr_ee_scale_trigger', - value: {}, - on: [ - { - events: [{scale: 'x'}, {scale: 'y'}], - update: - '(!isArray(thr_ee_Horsepower) || (+invert("x", thr_ee_x)[0] === +thr_ee_Horsepower[0] && +invert("x", thr_ee_x)[1] === +thr_ee_Horsepower[1])) && (!isArray(thr_ee_Miles_per_Gallon) || (+invert("y", thr_ee_y)[0] === +thr_ee_Miles_per_Gallon[0] && +invert("y", thr_ee_y)[1] === +thr_ee_Miles_per_Gallon[1])) ? thr_ee_scale_trigger : {}' - } - ] - } - ]) - ); + it('builds trigger signals', () => { + const oneSg = interval.signals(model, selCmpts['one'], []); + expect(oneSg).toContainEqual({ + name: 'one_tuple', + on: [ + { + events: [{signal: 'one_Horsepower'}], + update: 'one_Horsepower ? {unit: "", fields: one_tuple_fields, values: [one_Horsepower]} : null' + } + ] + }); - const fourSg = interval.signals(model, selCmpts['four'], []); - expect(fourSg).toEqual( - expect.arrayContaining([ - { - name: 'four_x', - init: '[scale("x", 50), scale("x", 70)]', - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], - update: '[four_x[0], clamp(x(unit), 0, width)]' - }, - { - events: {signal: 'four_scale_trigger'}, - update: '[scale("x", four_Horsepower[0]), scale("x", four_Horsepower[1])]' - } - ] - }, - { - name: 'four_Horsepower', - init: '[50, 70]', - on: [ - { - events: {signal: 'four_x'}, - update: 'four_x[0] === four_x[1] ? null : invert("x", four_x)' - } - ] - }, - { - name: 'four_scale_trigger', - value: {}, - on: [ - { - events: [{scale: 'x'}], - update: - '(!isArray(four_Horsepower) || (+invert("x", four_x)[0] === +four_Horsepower[0] && +invert("x", four_x)[1] === +four_Horsepower[1])) ? four_scale_trigger : {}' - } - ] + const twoSg = interval.signals(model, selCmpts['two'], []); + expect(twoSg).toContainEqual({ + name: 'two_tuple', + on: [ + { + events: [{signal: 'two_Miles_per_Gallon'}], + update: + 'two_Miles_per_Gallon ? {unit: "", fields: two_tuple_fields, values: [two_Miles_per_Gallon]} : null' + } + ] + }); + + const threeSg = interval.signals(model, selCmpts['thr_ee'], []); + expect(threeSg).toContainEqual({ + name: 'thr_ee_tuple', + on: [ + { + events: [{signal: 'thr_ee_Horsepower || thr_ee_Miles_per_Gallon'}], + update: + 'thr_ee_Horsepower && thr_ee_Miles_per_Gallon ? {unit: "", fields: thr_ee_tuple_fields, values: [thr_ee_Horsepower,thr_ee_Miles_per_Gallon]} : null' + } + ] + }); + + const fourSg = interval.signals(model, selCmpts['four'], []); + expect(fourSg).toContainEqual({ + name: 'four_tuple', + init: '{unit: "", fields: four_tuple_fields, values: [[50, 70]]}', + on: [ + { + events: [{signal: 'four_Horsepower'}], + update: 'four_Horsepower ? {unit: "", fields: four_tuple_fields, values: [four_Horsepower]} : null' + } + ] + }); + + const fiveSg = interval.signals(model, selCmpts['five'], []); + expect(fiveSg).toContainEqual({ + name: 'five_tuple', + init: '{unit: "", fields: five_tuple_fields, values: [[50, 60], [23, 54]]}', + on: [ + { + events: [{signal: 'five_Horsepower || five_Miles_per_Gallon'}], + update: + 'five_Horsepower && five_Miles_per_Gallon ? {unit: "", fields: five_tuple_fields, values: [five_Horsepower,five_Miles_per_Gallon]} : null' + } + ] + }); + }); + + it('namespaces signals when encoding/fields collide', () => { + const model2 = parseUnitModel({ + mark: 'circle', + encoding: { + x: {field: 'x', type: 'quantitative'}, + y: {field: 'y', type: 'quantitative'} } - ]) - ); + }); - const fiveSg = interval.signals(model, selCmpts['five'], []); - expect(fiveSg).toEqual( - expect.arrayContaining([ - { - name: 'five_x', - init: '[scale("x", 50), scale("x", 60)]', - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], - update: '[five_x[0], clamp(x(unit), 0, width)]' - }, - { - events: {signal: 'five_scale_trigger'}, - update: '[scale("x", five_Horsepower[0]), scale("x", five_Horsepower[1])]' - } - ] - }, - { - name: 'five_Horsepower', - init: '[50, 60]', - on: [ - { - events: {signal: 'five_x'}, - update: 'five_x[0] === five_x[1] ? null : invert("x", five_x)' - } - ] - }, - { - name: 'five_y', - init: '[scale("y", 23), scale("y", 54)]', - on: [ - { - events: parseSelector('mousedown', 'scope')[0], - update: '[y(unit), y(unit)]' - }, - { - events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], - update: '[five_y[0], clamp(y(unit), 0, height)]' - }, - { - events: {signal: 'five_scale_trigger'}, - update: '[scale("y", five_Miles_per_Gallon[0]), scale("y", five_Miles_per_Gallon[1])]' - } - ] - }, - { - name: 'five_Miles_per_Gallon', - init: '[23, 54]', - on: [ - { - events: {signal: 'five_y'}, - update: 'five_y[0] === five_y[1] ? null : invert("y", five_y)' - } - ] - }, + model2.parseScale(); + + const selCmpts2 = (model2.component.selection = parseUnitSelection(model2, [ { - name: 'five_scale_trigger', - value: {}, - on: [ - { - events: [{scale: 'x'}, {scale: 'y'}], - update: - '(!isArray(five_Horsepower) || (+invert("x", five_x)[0] === +five_Horsepower[0] && +invert("x", five_x)[1] === +five_Horsepower[1])) && (!isArray(five_Miles_per_Gallon) || (+invert("y", five_y)[0] === +five_Miles_per_Gallon[0] && +invert("y", five_y)[1] === +five_Miles_per_Gallon[1])) ? five_scale_trigger : {}' - } - ] + name: 'one', + select: { + type: 'interval', + encodings: ['x'], + translate: false, + zoom: false + } } - ]) - ); + ])); - const sixSg = interval.signals(model, selCmpts['six'], []); - expect(sixSg).toEqual( + const sg = interval.signals(model, selCmpts2['one'], []); + expect(sg[0].name).toBe('one_x_1'); + expect(sg[1].name).toBe('one_x'); + }); + }); + + it('builds modify signals', () => { + const signals = assembleUnitSelectionSignals(model, []); + expect(signals).toEqual( expect.arrayContaining([ { - name: 'six_x', - init: '[scale("x", datetime(2000, 9, 5, 0, 0, 0, 0)), scale("x", datetime(2001, 0, 13, 0, 0, 0, 0))]', + name: 'one_modify', on: [ { - events: parseSelector('mousedown', 'scope')[0], - update: '[x(unit), x(unit)]' - }, - { - events: parseSelector('[mousedown, window:mouseup] > window:mousemove!', 'scope')[0], - update: '[six_x[0], clamp(x(unit), 0, width)]' - }, - { - events: {signal: 'six_scale_trigger'}, - update: '[scale("x", six_Horsepower[0]), scale("x", six_Horsepower[1])]' + events: {signal: 'one_tuple'}, + update: `modify("one_store", one_tuple, true)` } ] }, { - name: 'six_Horsepower', - init: '[datetime(2000, 9, 5, 0, 0, 0, 0), datetime(2001, 0, 13, 0, 0, 0, 0)]', + name: 'two_modify', on: [ { - events: {signal: 'six_x'}, - update: 'six_x[0] === six_x[1] ? null : invert("x", six_x)' + events: {signal: 'two_tuple'}, + update: `modify("two_store", two_tuple, true)` } ] }, { - name: 'six_scale_trigger', - value: {}, + name: 'thr_ee_modify', on: [ { - events: [{scale: 'x'}], - update: - '(!isArray(six_Horsepower) || (+invert("x", six_x)[0] === +six_Horsepower[0] && +invert("x", six_x)[1] === +six_Horsepower[1])) ? six_scale_trigger : {}' + events: {signal: 'thr_ee_tuple'}, + update: `modify("thr_ee_store", thr_ee_tuple, {unit: ""})` } ] } @@ -393,446 +520,322 @@ describe('Interval Selections', () => { ); }); - it('builds trigger signals', () => { - const oneSg = interval.signals(model, selCmpts['one'], []); - expect(oneSg).toContainEqual({ - name: 'one_tuple', - on: [ - { - events: [{signal: 'one_Horsepower'}], - update: 'one_Horsepower ? {unit: "", fields: one_tuple_fields, values: [one_Horsepower]} : null' + it('builds brush mark', () => { + const marks: any[] = [{hello: 'world'}]; + expect(interval.marks(model, selCmpts['one'], marks)).toEqual([ + { + name: 'one_brush_bg', + type: 'rect', + clip: true, + encode: { + enter: { + fill: {value: '#333'}, + fillOpacity: {value: 0.125} + }, + update: { + x: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + signal: 'one_x[0]' + }, + { + value: 0 + } + ], + y: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + value: 0 + }, + { + value: 0 + } + ], + x2: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + signal: 'one_x[1]' + }, + { + value: 0 + } + ], + y2: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + field: { + group: 'height' + } + }, + { + value: 0 + } + ] + } } - ] - }); - - const twoSg = interval.signals(model, selCmpts['two'], []); - expect(twoSg).toContainEqual({ - name: 'two_tuple', - on: [ - { - events: [{signal: 'two_Miles_per_Gallon'}], - update: 'two_Miles_per_Gallon ? {unit: "", fields: two_tuple_fields, values: [two_Miles_per_Gallon]} : null' + }, + {hello: 'world'}, + { + name: 'one_brush', + type: 'rect', + clip: true, + encode: { + enter: { + fill: {value: 'transparent'} + }, + update: { + stroke: [ + { + test: 'one_x[0] !== one_x[1]', + value: 'white' + }, + { + value: null + } + ], + x: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + signal: 'one_x[0]' + }, + { + value: 0 + } + ], + y: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + value: 0 + }, + { + value: 0 + } + ], + x2: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + signal: 'one_x[1]' + }, + { + value: 0 + } + ], + y2: [ + { + test: 'data("one_store").length && data("one_store")[0].unit === ""', + field: { + group: 'height' + } + }, + { + value: 0 + } + ] + } } - ] - }); + } + ]); - const threeSg = interval.signals(model, selCmpts['thr_ee'], []); - expect(threeSg).toContainEqual({ - name: 'thr_ee_tuple', - on: [ - { - events: [{signal: 'thr_ee_Horsepower || thr_ee_Miles_per_Gallon'}], - update: - 'thr_ee_Horsepower && thr_ee_Miles_per_Gallon ? {unit: "", fields: thr_ee_tuple_fields, values: [thr_ee_Horsepower,thr_ee_Miles_per_Gallon]} : null' - } - ] - }); + // Scale-bound interval selections should not add a brush mark. + expect(interval.marks(model, selCmpts['two'], marks)).toEqual(marks); - const fourSg = interval.signals(model, selCmpts['four'], []); - expect(fourSg).toContainEqual({ - name: 'four_tuple', - init: '{unit: "", fields: four_tuple_fields, values: [[50, 70]]}', - on: [ - { - events: [{signal: 'four_Horsepower'}], - update: 'four_Horsepower ? {unit: "", fields: four_tuple_fields, values: [four_Horsepower]} : null' + expect(interval.marks(model, selCmpts['thr_ee'], marks)).toEqual([ + { + name: 'thr_ee_brush_bg', + type: 'rect', + clip: true, + encode: { + enter: { + fill: {value: 'red'}, + fillOpacity: {value: 0.75} + }, + update: { + x: { + signal: 'thr_ee_x[0]' + }, + y: { + signal: 'thr_ee_y[0]' + }, + x2: { + signal: 'thr_ee_x[1]' + }, + y2: { + signal: 'thr_ee_y[1]' + } + } } - ] - }); - - const fiveSg = interval.signals(model, selCmpts['five'], []); - expect(fiveSg).toContainEqual({ - name: 'five_tuple', - init: '{unit: "", fields: five_tuple_fields, values: [[50, 60], [23, 54]]}', - on: [ - { - events: [{signal: 'five_Horsepower || five_Miles_per_Gallon'}], - update: - 'five_Horsepower && five_Miles_per_Gallon ? {unit: "", fields: five_tuple_fields, values: [five_Horsepower,five_Miles_per_Gallon]} : null' + }, + {hello: 'world'}, + { + name: 'thr_ee_brush', + type: 'rect', + clip: true, + encode: { + enter: { + fill: {value: 'transparent'} + }, + update: { + stroke: [ + { + test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', + value: 'black' + }, + {value: null} + ], + strokeWidth: [ + { + test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', + value: 4 + }, + {value: null} + ], + strokeDash: [ + { + test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', + value: [10, 5] + }, + {value: null} + ], + strokeDashOffset: [ + { + test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', + value: 3 + }, + {value: null} + ], + strokeOpacity: [ + { + test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', + value: 0.25 + }, + {value: null} + ], + x: { + signal: 'thr_ee_x[0]' + }, + y: { + signal: 'thr_ee_y[0]' + }, + x2: { + signal: 'thr_ee_x[1]' + }, + y2: { + signal: 'thr_ee_y[1]' + } + } } - ] - }); + } + ]); }); - it('namespaces signals when encoding/fields collide', () => { - const model2 = parseUnitModel({ + it('should be robust to same channel/field names', () => { + const nameModel = parseUnitModel({ mark: 'circle', encoding: { x: {field: 'x', type: 'quantitative'}, y: {field: 'y', type: 'quantitative'} } }); + nameModel.parseScale(); - model2.parseScale(); - - const selCmpts2 = (model2.component.selection = parseUnitSelection(model2, [ + const nameSelCmpts = (nameModel.component.selection = parseUnitSelection(nameModel, [ { - name: 'one', - select: { - type: 'interval', - encodings: ['x'], - translate: false, - zoom: false - } + name: 'brush', + select: 'interval' } ])); - const sg = interval.signals(model, selCmpts2['one'], []); - expect(sg[0].name).toBe('one_x_1'); - expect(sg[1].name).toBe('one_x'); - }); - }); + const signals = interval.signals(nameModel, nameSelCmpts['brush'], []); + const names = signals.map(s => s.name); + expect(names).toEqual(expect.arrayContaining(['brush_x_1', 'brush_x', 'brush_y_1', 'brush_y'])); - it('builds modify signals', () => { - const signals = assembleUnitSelectionSignals(model, []); - expect(signals).toEqual( - expect.arrayContaining([ - { - name: 'one_modify', - on: [ - { - events: {signal: 'one_tuple'}, - update: `modify("one_store", one_tuple, true)` - } - ] - }, + const marks: any[] = [{hello: 'world'}]; + expect(interval.marks(nameModel, nameSelCmpts['brush'], marks)).toEqual([ { - name: 'two_modify', - on: [ - { - events: {signal: 'two_tuple'}, - update: `modify("two_store", two_tuple, true)` + name: 'brush_brush_bg', + type: 'rect', + clip: true, + encode: { + enter: {fill: {value: '#333'}, fillOpacity: {value: 0.125}}, + update: { + x: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_x_1[0]' + }, + {value: 0} + ], + y: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_y_1[0]' + }, + {value: 0} + ], + x2: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_x_1[1]' + }, + {value: 0} + ], + y2: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_y_1[1]' + }, + {value: 0} + ] } - ] + } }, + {hello: 'world'}, { - name: 'thr_ee_modify', - on: [ - { - events: {signal: 'thr_ee_tuple'}, - update: `modify("thr_ee_store", thr_ee_tuple, {unit: ""})` - } - ] - } - ]) - ); - }); - - it('builds brush mark', () => { - const marks: any[] = [{hello: 'world'}]; - expect(interval.marks(model, selCmpts['one'], marks)).toEqual([ - { - name: 'one_brush_bg', - type: 'rect', - clip: true, - encode: { - enter: { - fill: {value: '#333'}, - fillOpacity: {value: 0.125} - }, - update: { - x: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - signal: 'one_x[0]' - }, - { - value: 0 - } - ], - y: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - value: 0 - }, - { - value: 0 - } - ], - x2: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - signal: 'one_x[1]' - }, - { - value: 0 - } - ], - y2: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - field: { - group: 'height' - } - }, - { - value: 0 - } - ] - } - } - }, - {hello: 'world'}, - { - name: 'one_brush', - type: 'rect', - clip: true, - encode: { - enter: { - fill: {value: 'transparent'} - }, - update: { - stroke: [ - { - test: 'one_x[0] !== one_x[1]', - value: 'white' - }, - { - value: null - } - ], - x: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - signal: 'one_x[0]' - }, - { - value: 0 - } - ], - y: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - value: 0 - }, - { - value: 0 - } - ], - x2: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - signal: 'one_x[1]' - }, - { - value: 0 - } - ], - y2: [ - { - test: 'data("one_store").length && data("one_store")[0].unit === ""', - field: { - group: 'height' - } - }, - { - value: 0 - } - ] - } - } - } - ]); - - // Scale-bound interval selections should not add a brush mark. - expect(interval.marks(model, selCmpts['two'], marks)).toEqual(marks); - - expect(interval.marks(model, selCmpts['thr_ee'], marks)).toEqual([ - { - name: 'thr_ee_brush_bg', - type: 'rect', - clip: true, - encode: { - enter: { - fill: {value: 'red'}, - fillOpacity: {value: 0.75} - }, - update: { - x: { - signal: 'thr_ee_x[0]' - }, - y: { - signal: 'thr_ee_y[0]' - }, - x2: { - signal: 'thr_ee_x[1]' - }, - y2: { - signal: 'thr_ee_y[1]' + name: 'brush_brush', + type: 'rect', + clip: true, + encode: { + enter: {fill: {value: 'transparent'}}, + update: { + x: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_x_1[0]' + }, + {value: 0} + ], + y: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_y_1[0]' + }, + {value: 0} + ], + x2: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_x_1[1]' + }, + {value: 0} + ], + y2: [ + { + test: 'data("brush_store").length && data("brush_store")[0].unit === ""', + signal: 'brush_y_1[1]' + }, + {value: 0} + ], + stroke: [ + { + test: 'brush_x_1[0] !== brush_x_1[1] && brush_y_1[0] !== brush_y_1[1]', + value: 'white' + }, + {value: null} + ] } } } - }, - {hello: 'world'}, - { - name: 'thr_ee_brush', - type: 'rect', - clip: true, - encode: { - enter: { - fill: {value: 'transparent'} - }, - update: { - stroke: [ - { - test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', - value: 'black' - }, - {value: null} - ], - strokeWidth: [ - { - test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', - value: 4 - }, - {value: null} - ], - strokeDash: [ - { - test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', - value: [10, 5] - }, - {value: null} - ], - strokeDashOffset: [ - { - test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', - value: 3 - }, - {value: null} - ], - strokeOpacity: [ - { - test: 'thr_ee_x[0] !== thr_ee_x[1] && thr_ee_y[0] !== thr_ee_y[1]', - value: 0.25 - }, - {value: null} - ], - x: { - signal: 'thr_ee_x[0]' - }, - y: { - signal: 'thr_ee_y[0]' - }, - x2: { - signal: 'thr_ee_x[1]' - }, - y2: { - signal: 'thr_ee_y[1]' - } - } - } - } - ]); - }); - - it('should be robust to same channel/field names', () => { - const nameModel = parseUnitModel({ - mark: 'circle', - encoding: { - x: {field: 'x', type: 'quantitative'}, - y: {field: 'y', type: 'quantitative'} - } + ]); }); - nameModel.parseScale(); - - const nameSelCmpts = (nameModel.component.selection = parseUnitSelection(nameModel, [ - { - name: 'brush', - select: 'interval' - } - ])); - - const signals = interval.signals(nameModel, nameSelCmpts['brush'], []); - const names = signals.map(s => s.name); - expect(names).toEqual(expect.arrayContaining(['brush_x_1', 'brush_x', 'brush_y_1', 'brush_y'])); - - const marks: any[] = [{hello: 'world'}]; - expect(interval.marks(nameModel, nameSelCmpts['brush'], marks)).toEqual([ - { - name: 'brush_brush_bg', - type: 'rect', - clip: true, - encode: { - enter: {fill: {value: '#333'}, fillOpacity: {value: 0.125}}, - update: { - x: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_x_1[0]' - }, - {value: 0} - ], - y: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_y_1[0]' - }, - {value: 0} - ], - x2: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_x_1[1]' - }, - {value: 0} - ], - y2: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_y_1[1]' - }, - {value: 0} - ] - } - } - }, - {hello: 'world'}, - { - name: 'brush_brush', - type: 'rect', - clip: true, - encode: { - enter: {fill: {value: 'transparent'}}, - update: { - x: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_x_1[0]' - }, - {value: 0} - ], - y: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_y_1[0]' - }, - {value: 0} - ], - x2: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_x_1[1]' - }, - {value: 0} - ], - y2: [ - { - test: 'data("brush_store").length && data("brush_store")[0].unit === ""', - signal: 'brush_y_1[1]' - }, - {value: 0} - ], - stroke: [ - { - test: 'brush_x_1[0] !== brush_x_1[1] && brush_y_1[0] !== brush_y_1[1]', - value: 'white' - }, - {value: null} - ] - } - } - } - ]); }); });