UNPKG

@uwdata/mosaic-plot

Version:

A Mosaic-powered plotting framework based on Observable Plot.

97 lines (85 loc) 2.84 kB
import { clauseIntervals } from '@uwdata/mosaic-core'; import { ascending, min, max } from 'd3'; import { brush, brushGroups } from './util/brush.js'; import { closeTo } from './util/close-to.js'; import { getField } from './util/get-field.js'; import { invert } from './util/invert.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; /** * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class Interval2D { constructor(mark, { selection, xfield, yfield, pixelSize = 1, peers = true, brush: style }) { this.mark = mark; this.pixelSize = pixelSize || 1; this.selection = selection; this.peers = peers; this.xfield = xfield || getField(mark, 'x'); this.yfield = yfield || getField(mark, 'y'); this.style = style && sanitizeStyles(style); this.brush = brush(); this.brush.on('brush end', ({ selection }) => this.publish(selection)); } reset() { this.value = undefined; if (this.g) this.brush.reset(this.g); } activate() { this.selection.activate(this.clause(this.value || [[0, 1], [0, 1]])); } publish(extent) { const { value, pixelSize, xscale, yscale } = this; let xr = undefined; let yr = undefined; if (extent) { const [a, b] = extent; xr = [a[0], b[0]].map(v => invert(v, xscale, pixelSize)).sort(ascending); yr = [a[1], b[1]].map(v => invert(v, yscale, pixelSize)).sort(ascending); } if (!closeTo(xr, value?.[0]) || !closeTo(yr, value?.[1])) { this.value = extent ? [xr, yr] : undefined; this.g.call(this.brush.moveSilent, extent); this.selection.update(this.clause(this.value)); } } clause(value) { const { mark, pixelSize, xfield, yfield, xscale, yscale } = this; return clauseIntervals([xfield, yfield], value, { source: this, clients: this.peers ? mark.plot.markSet : new Set().add(mark), scales: [xscale, yscale], pixelSize }); } init(svg) { const { brush, style, value } = this; const xscale = this.xscale = svg.scale('x'); const yscale = this.yscale = svg.scale('y'); const rx = xscale.range; const ry = yscale.range; brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]); this.g = brushGroups(svg, null, min(rx), min(ry), 'interval-xy').call(brush); if (style) { const brushes = this.g.selectAll('rect.selection'); for (const name in style) { brushes.attr(name, style[name]); } } if (value) { const [x1, x2] = value[0].map(xscale.apply).sort(ascending); const [y1, y2] = value[1].map(yscale.apply).sort(ascending); this.g.call(brush.moveSilent, [[x1, y1], [x2, y2]]); } svg.addEventListener('pointerenter', evt => { if (!evt.buttons) this.activate(); }); } }