Skip to content

Commit

Permalink
Allow interval selections to be initialized.
Browse files Browse the repository at this point in the history
  • Loading branch information
arvind committed Oct 13, 2018
1 parent bb859cf commit 062c1f5
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 23 deletions.
3 changes: 2 additions & 1 deletion examples/specs/interactive_brush.vl.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"data": {"url": "data/cars.json"},
"selection": {
"brush": {
"type": "interval"
"type": "interval",
"init": {"x": [55, 160], "y": [13, 37]}
}
},
"mark": "point",
Expand Down
48 changes: 32 additions & 16 deletions src/compile/selection/interval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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.
Expand All @@ -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`
}
]
});
Expand Down Expand Up @@ -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})`) + ']';

This comment has been minimized.

Copy link
@domoritz

domoritz Oct 13, 2018

Member

Let's use a string template here.


const on = events(selCmpt, (def: any[], evt: VgEventStream) => {
return def.concat(
{events: evt.between[0], update: `[${coord}, ${coord}]`}, // Brush Start
Expand All @@ -201,22 +208,31 @@ 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
? [{name: dname, on: []}]
: [
{
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})`
}
]
}
];
}
Expand Down
12 changes: 8 additions & 4 deletions src/compile/selection/multi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
2 changes: 1 addition & 1 deletion src/compile/selection/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[])[];

This comment has been minimized.

Copy link
@domoritz

domoritz Oct 13, 2018

Member

Can you initialize selections with dates?

This comment has been minimized.

Copy link
@arvind

arvind Oct 13, 2018

Author Member

I assume you mean a Date object? In which case, nope, because those cannot be represented in JSON. They'd have to be passed in as a timestamp number.

This comment has been minimized.

Copy link
@domoritz

domoritz Oct 13, 2018

Member

I meant https://vega.github.io/vega-lite/docs/types.html#datetime.

Even if we don't add this for now, we should allow booleans, no?

This comment has been minimized.

Copy link
@arvind

arvind Oct 13, 2018

Author Member

Oh cool, didn't realize that type was available within Vega-Lite. Will update to support it. And yes, booleans should be supported too.

events: VgEventStream;
// predicate?: string;
bind?: 'scales' | VgBinding | Dict<VgBinding>;
Expand Down
2 changes: 1 addition & 1 deletion src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 062c1f5

Please sign in to comment.