UNPKG

@uwdata/mosaic-plot

Version:

A Mosaic-powered plotting framework based on Observable Plot.

128 lines (112 loc) 3.54 kB
/** @import { ClauseSource } from '@uwdata/mosaic-core' */ import { Selection, clauseInterval } from '@uwdata/mosaic-core'; import { select, zoom, ZoomTransform } from 'd3'; import { getField } from './util/get-field.js'; const asc = (a, b) => a - b; /** * @import {Activatable} from '@uwdata/mosaic-core' * @implements {Activatable} */ export class PanZoom { constructor(mark, { x = new Selection(), y = new Selection(), xfield, yfield, zoom = true, panx = true, pany = true }) { this.mark = mark; this.xsel = x; this.ysel = y; this.xfield = xfield || getField(mark, 'x'); this.yfield = yfield || getField(mark, 'y'); this.zoom = extent(zoom, [0, Infinity], [1, 1]); this.panx = this.xsel && panx; this.pany = this.ysel && pany; const { plot } = mark; if (panx) { this.xsel.addEventListener('value', value => { if (plot.setAttribute('xDomain', value)) plot.update(); }); } if (pany) { this.ysel.addEventListener('value', value => { if (plot.setAttribute('yDomain', value)) plot.update(); }); } } publish(transform) { if (this.panx) { const xdom = rescaleX(transform, this.xscale); this.xsel.update(this.clause(xdom, this.xfield, this.xscale)); } if (this.pany) { const ydom = rescaleY(transform, this.yscale); this.ysel.update(this.clause(ydom, this.yfield, this.yscale)); } } clause(value, field, scale) { return clauseInterval(field, value, { source: /** @type {ClauseSource} */(this), clients: this.mark.plot.markSet, scale }); } init(svg) { this.svg = svg; if (this.initialized) return; else this.initialized = true; const { panx, pany, mark: { plot: { element } } } = this; this.xscale = svg.scale('x'); this.yscale = svg.scale('y'); const rx = this.xscale.range.slice().sort(asc); const ry = this.yscale.range.slice().sort(asc); const tx = extent(panx, [-Infinity, Infinity], rx); const ty = extent(pany, [-Infinity, Infinity], ry); const z = zoom() .extent([[rx[0], ry[0]], [rx[1], ry[1]]]) .scaleExtent(this.zoom) .translateExtent([[tx[0], ty[0]], [tx[1], ty[1]]]) .on('start', () => { this.xscale = this.svg.scale('x'); this.yscale = this.svg.scale('y'); }) .on('end', () => element.__zoom = new ZoomTransform(1, 0, 0)) .on('zoom', ({ transform }) => this.publish(transform)); select(element).call(z); if (panx || pany) { let enter = false; element.addEventListener('pointerenter', evt => { if (enter) return; else enter = true; if (!evt.buttons) this.activate(); // don't activate if mouse down }); element.addEventListener('pointerleave', () => enter = false); } } activate() { if (this.panx) { const { xscale, xfield } = this; this.xsel.activate(this.clause(xscale.domain, xfield, xscale)); } if (this.pany) { const { yscale, yfield } = this; this.ysel.activate(this.clause(yscale.domain, yfield, yscale)); } } } function extent(ext, defaultTrue, defaultFalse) { return ext ? (Array.isArray(ext) ? ext : defaultTrue) : defaultFalse; } function rescaleX(transform, scale) { return scale.range .map(transform.invertX, transform) .map(scale.invert, scale); } function rescaleY(transform, scale) { return scale.range .map(transform.invertY, transform) .map(scale.invert, scale); }