@uwdata/mosaic-plot
Version:
A Mosaic-powered plotting framework based on Observable Plot.
95 lines (84 loc) • 2.68 kB
JavaScript
/** @import { ClauseSource } from '@uwdata/mosaic-core' */
import { clausePoints } from '@uwdata/mosaic-core';
import { getDatum } from './util/get-datum.js';
import { neq, neqSome } from './util/neq.js';
/**
* @import {Activatable} from '@uwdata/mosaic-core'
* @implements {Activatable}
*/
export class Toggle {
/**
* @param {*} mark The mark to interact with.
* @param {*} options The interactor options.
*/
constructor(mark, {
selection,
channels,
peers = true
}) {
this.mark = mark;
this.value = null;
this.selection = selection;
this.peers = peers;
const fields = this.fields = [];
const as = this.as = [];
channels.forEach(c => {
const q = c === 'color' ? ['color', 'fill', 'stroke']
: c === 'x' ? ['x', 'x1', 'x2']
: c === 'y' ? ['y', 'y1', 'y2']
: [c];
for (let i = 0; i < q.length; ++i) {
const f = mark.channelField(q[i], { exact: true });
if (f) {
fields.push(f.field?.basis || f.field);
as.push(f.as);
return;
}
}
throw new Error(`Missing channel: ${c}`);
});
}
clause(value) {
const { fields, mark } = this;
return clausePoints(fields, value, {
source: /** @type {ClauseSource} */(this),
clients: this.peers ? mark.plot.markSet : new Set().add(mark)
});
}
init(svg, selector, accessor) {
const { mark, as, selection } = this;
const { data: { columns = {} } = {} } = mark;
accessor ??= target => as.map(name => columns[name][getDatum(target)]);
selector ??= `[data-index="${mark.index}"]`;
const groups = Array.from(svg.querySelectorAll(selector));
svg.addEventListener('pointerdown', evt => {
const state = selection.single ? selection.value : this.value;
const target = evt.target;
let value = null;
if (isTargetElement(groups, target)) {
const point = accessor(target);
if ((evt.shiftKey || evt.metaKey) && state?.length) {
value = state.filter(s => neq(s, point));
if (value.length === state.length) value.push(point);
} else if (state?.length === 1 && !neq(state[0], point)) {
value = null;
} else {
value = [point];
}
}
this.value = value;
if (neqSome(state, value)) {
selection.update(this.clause(value));
}
});
svg.addEventListener('pointerenter', evt => {
if (!evt.buttons) this.activate();
});
}
activate() {
this.selection.activate(this.clause([this.fields.map(() => 0)]));
}
}
function isTargetElement(groups, node) {
return groups.some(g => g.contains(node));
}