diff --git a/packages/inputs/src/Menu.js b/packages/inputs/src/Menu.js index 521ee6df..95e8a087 100644 --- a/packages/inputs/src/Menu.js +++ b/packages/inputs/src/Menu.js @@ -23,7 +23,7 @@ export class Menu extends MosaicClient { super(filterBy); this.from = from; this.column = column; - this.selection = as; + this.selections = (as === undefined) ? [] : Array.isArray(as) ? as : [as]; this.format = format; this.element = element ?? document.createElement('div'); @@ -39,22 +39,25 @@ export class Menu extends MosaicClient { this.data = options.map(value => isObject(value) ? value : { value }); this.update(); } - value = value ?? this.selection?.value ?? this.data?.[0]?.value; - if (this.selection?.value === undefined) this.publish(value); + this.selections.forEach(s => { value = value ?? s?.value}); + value = value ?? this.data?.[0]?.value; + if (this.selections.some(s => s?.value === undefined)) { + this.publish(value); + } this.element.appendChild(this.select); - if (this.selection) { + this.selections.forEach(selection => { this.select.addEventListener('input', () => { this.publish(this.selectedValue() ?? null); }); - if (!isSelection(this.selection)) { - this.selection.addEventListener('value', value => { + if (!isSelection(selection)) { + selection.addEventListener('value', value => { if (value !== this.select.value) { this.selectedValue(value); } }); } - } + }); } selectedValue(value) { @@ -76,17 +79,19 @@ export class Menu extends MosaicClient { } publish(value) { - const { selection, column } = this; - if (isSelection(selection)) { - selection.update({ + const { selections, column } = this; + selections.forEach(selection => { + if (isSelection(selection)) { + selection.update({ source: this, schema: { type: 'point' }, value, predicate: value ? eq(column, literal(value)) : null }); - } else if (isParam(selection)) { + } else if (isParam(selection)) { selection.update(value); - } + } + }); } query(filter = []) { @@ -114,9 +119,9 @@ export class Menu extends MosaicClient { opt.innerText = label ?? format(value); this.select.appendChild(opt); } - if (this.selection) { - this.selectedValue(this.selection?.value ?? ''); - } + this.selections.forEach(selection => { + this.selectedValue(selection?.value ?? ''); + }); return this; } } diff --git a/packages/vgplot/src/interactors/Nearest.js b/packages/vgplot/src/interactors/Nearest.js index 519a4b07..7a657dbf 100644 --- a/packages/vgplot/src/interactors/Nearest.js +++ b/packages/vgplot/src/interactors/Nearest.js @@ -10,7 +10,11 @@ export class Nearest { field }) { this.mark = mark; - this.selection = selection; + if (Array.isArray(selection)) { + this.selections = selection; + } else { + this.selections = [selection]; + } this.clients = new Set().add(mark); this.channel = channel; this.field = field || getField(mark, [channel]); @@ -30,24 +34,28 @@ export class Nearest { init(svg) { const that = this; - const { mark, channel, selection } = this; + const { mark, channel, selections } = this; const { data } = mark; const key = mark.channelField(channel).as; const facets = select(svg).selectAll('g[aria-label="facet"]'); const root = facets.size() ? facets : select(svg); const scale = svg.scale(channel); - const param = !isSelection(selection); root.on('pointerdown pointermove', function(evt) { const [x, y] = pointer(evt, this); const z = findNearest(data, key, scale.invert(channel === 'x' ? x : y)); - selection.update(param ? z : that.clause(z)); + selections.forEach(selection => { + const param = !isSelection(selection); + selection.update(param ? z : that.clause(z)); + }); }); - if (param) return; - svg.addEventListener('pointerenter', () => { - this.selection.activate(this.clause(0)); + selections.forEach(selection => { + if (!isSelection(selection)) return; + svg.addEventListener('pointerenter', () => { + selection.activate(this.clause(0)); + }); }); } } diff --git a/packages/vgplot/src/spec/parse-spec.js b/packages/vgplot/src/spec/parse-spec.js index 71af0c9e..b796e35e 100644 --- a/packages/vgplot/src/spec/parse-spec.js +++ b/packages/vgplot/src/spec/parse-spec.js @@ -153,6 +153,13 @@ export class ParseContext { return this.maybeParam(value, () => Selection.intersect()); } + maybeSelections(value) { + if (Array.isArray(value)) { + return value.map(v => this.maybeSelection(v)); + } + return this.maybeSelection(value); + } + maybeTransform(value) { if (isObject(value)) { return value.expr @@ -260,7 +267,7 @@ function parseInput(spec, ctx) { error(`Unrecognized input: ${input}`, spec); } for (const key in options) { - options[key] = ctx.maybeSelection(options[key]); + options[key] = ctx.maybeSelections(options[key]); } return fn(options); } @@ -358,7 +365,7 @@ function parseInteractor(spec, ctx) { error(`Unrecognized interactor type: ${select}`, spec); } for (const key in options) { - options[key] = ctx.maybeSelection(options[key]); + options[key] = ctx.maybeSelections(options[key]); } return fn(options); } diff --git a/packages/vgplot/src/spec/to-module.js b/packages/vgplot/src/spec/to-module.js index 0e772d83..cb4bdde2 100644 --- a/packages/vgplot/src/spec/to-module.js +++ b/packages/vgplot/src/spec/to-module.js @@ -141,6 +141,13 @@ class CodegenContext extends ParseContext { return this.maybeParam(value, 'vg.Selection.intersect()'); } + maybeSelections(value) { + if (Array.isArray(value)) { + return `[${value.map(v => this.maybeSelection(v)).join(',')}]`; + } + return this.maybeSelection(value); + } + maybeTransform(value) { if (isObject(value)) { return value.expr @@ -337,7 +344,7 @@ function parseInput(spec, ctx) { } const opt = []; for (const key in options) { - opt.push(`${key}: ${ctx.maybeSelection(options[key])}`); + opt.push(`${key}: ${ctx.maybeSelections(options[key])}`); } return `${ctx.tab()}vg.${input}({ ${opt.join(', ')} })`; } @@ -458,7 +465,7 @@ function parseInteractor(spec, ctx) { } const opt = []; for (const key in options) { - opt.push(`${key}: ${ctx.maybeSelection(options[key])}`); + opt.push(`${key}: ${ctx.maybeSelections(options[key])}`); } return `${ctx.tab()}vg.${select}({ ${opt.join(', ')} })`; }