UNPKG

@uwdata/mosaic-plot

Version:

A Mosaic-powered plotting framework based on Observable Plot.

116 lines (101 loc) 3.36 kB
import { contours } from 'd3'; import { gridDomainContinuous } from './util/grid.js'; import { handleParam } from './util/handle-param.js'; import { Grid2DMark } from './Grid2DMark.js'; import { channelOption } from './Mark.js'; export class ContourMark extends Grid2DMark { constructor(source, options) { const { thresholds = 10, ...channels } = options; super('geo', source, { bandwidth: 20, interpolate: 'linear', pixelSize: 2, ...channels }); /** @type {number|number[]} */ this.thresholds = handleParam(thresholds, value => { this.thresholds = value; return this.grids ? this.contours().update() : null; }); } convolve() { return super.convolve().contours(); } contours() { const { bins, densityMap, grids, thresholds, plot } = this; const { numRows, columns } = grids; let t = thresholds; let tz; if (Array.isArray(t)) { tz = t; } else { const [, hi] = gridDomainContinuous(columns.density); tz = Array.from({length: t - 1}, (_, i) => (hi * (i + 1)) / t); } if (densityMap.fill || densityMap.stroke) { if (this.plot.getAttribute('colorScale') !== 'log') { this.plot.setAttribute('colorZero', true); } } // transform contours into data space coordinates // so we play nice with scale domains & axes const [nx, ny] = bins; const [x0, x1] = plot.getAttribute('xDomain'); const [y0, y1] = plot.getAttribute('yDomain'); const sx = (x1 - x0) / nx; const sy = (y1 - y0) / ny; const xo = +x0; const yo = +y0; const x = v => xo + v * sx; const y = v => yo + v * sy; const contour = contours().size(bins); // generate contours const data = this.contourData = Array(numRows * tz.length); const { density, ...groupby } = columns; const groups = Object.entries(groupby); for (let i = 0, k = 0; i < numRows; ++i) { const grid = density[i]; const rest = groups.reduce((o, [name, col]) => (o[name] = col[i], o), {}); for (let j = 0; j < tz.length; ++j, ++k) { // annotate contour geojson with cell groupby fields // d3-contour already adds a threshold "value" property data[k] = Object.assign( transform(contour.contour(grid, tz[j]), x, y), rest ); } } return this; } plotSpecs() { const { type, channels, densityMap, contourData: data } = this; const options = {}; for (const c of channels) { const { channel } = c; if (channel !== 'x' && channel !== 'y') { options[channel] = channelOption(c); } } // d3-contour adds a threshold "value" property // here we ensure requested density values are encoded for (const channel in densityMap) { if (!densityMap[channel]) continue; options[channel] = channelOption({ channel, as: 'value' }); } return [{ type, data, options }]; } } function transform(geometry, x, y) { function transformPolygon(coordinates) { coordinates.forEach(transformRing); } function transformRing(coordinates) { coordinates.forEach(transformPoint); } function transformPoint(coordinates) { coordinates[0] = x(coordinates[0]); coordinates[1] = y(coordinates[1]); } geometry.coordinates.forEach(transformPolygon); return geometry; }