@uwdata/mosaic-plot
Version:
A Mosaic-powered plotting framework based on Observable Plot.
139 lines (116 loc) • 4.63 kB
JavaScript
import { toDataColumns } from '@uwdata/mosaic-core';
import { binLinear1d, isBetween } from '@uwdata/mosaic-sql';
import { max, sum } from 'd3';
import { Transient } from '../symbols.js';
import { binExpr } from './util/bin-expr.js';
import { dericheConfig, dericheConv1d } from './util/density.js';
import { extentX, extentY, xext, yext } from './util/extent.js';
import { grid1d } from './util/grid.js';
import { handleParam } from './util/handle-param.js';
import { Mark, channelOption, markQuery } from './Mark.js';
const GROUPBY = { fill: 1, stroke: 1, z: 1 };
export class Density1DMark extends Mark {
constructor(type, source, options) {
const {
bins = 1024,
bandwidth = 20,
normalize = false,
stack = false,
...channels
} = options;
const dim = type.endsWith('X') ? 'y' : 'x';
super(type, source, channels, dim === 'x' ? xext : yext);
this.dim = dim;
/** @type {number} */
this.bins = handleParam(bins, value => {
return (this.bins = value, this.requestUpdate());
});
/** @type {number} */
this.bandwidth = handleParam(bandwidth, value => {
this.bandwidth = value;
return this.grids ? this.convolve().update() : null;
});
/** @type {string | boolean} */
this.normalize = handleParam(normalize, value => {
return (this.normalize = value, this.convolve().update());
});
/** @type {boolean} */
this.stack = handleParam(stack, value => {
return (this.stack = value, this.update());
});
}
get filterStable() {
const name = this.dim === 'x' ? 'xDomain' : 'yDomain';
const dom = this.plot.getAttribute(name);
return dom && !dom[Transient];
}
query(filter = []) {
if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
const { bins, channels, dim } = this;
const extent = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
const [x, bx] = binExpr(this, dim, bins, extent);
const q = markQuery(channels, this.sourceTable(), [dim])
.where(filter.concat(isBetween(bx, extent)));
const v = this.channelField('weight') ? 'weight' : null;
const g = this.groupby = channels.flatMap(c => {
return (GROUPBY[c.channel] && c.field) ? c.as : [];
});
return binLinear1d(q, x, v, g);
}
queryResult(data) {
// @ts-expect-error Correct the data column type
const c = toDataColumns(data).columns;
this.grids = grid1d(this.bins, c.index, c.density, c, this.groupby);
return this.convolve();
}
convolve() {
const {
bins, bandwidth, normalize, dim, grids, groupby, plot, extent: [lo, hi]
} = this;
const cols = grids.columns;
const numGrids = grids.numRows;
const b = this.channelField(dim).as;
const v = dim === 'x' ? 'y' : 'x';
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
const neg = cols._grid.some(grid => grid.some(v => v < 0));
const config = dericheConfig(bandwidth * (bins - 1) / size, neg);
const b0 = +lo;
const delta = (hi - b0) / (bins - 1);
const numRows = bins * numGrids;
const _b = new Float64Array(numRows);
const _v = new Float64Array(numRows);
const _g = groupby.reduce((m, name) => (m[name] = Array(numRows), m), {});
for (let k = 0, g = 0; g < numGrids; ++g) {
// fill in groupby values
groupby.forEach(name => _g[name].fill(cols[name][g], k, k + bins));
// perform smoothing, map smoothed grid values to sample data points
const grid = cols._grid[g];
const result = dericheConv1d(config, grid, bins);
const scale = 1 / norm(grid, result, delta, normalize);
for (let i = 0; i < bins; ++i, ++k) {
_b[k] = b0 + i * delta;
_v[k] = result[i] * scale;
}
}
this.data = { numRows, columns: { [b]: _b, [v]: _v, ..._g } };
return this;
}
plotSpecs() {
// @ts-expect-error Correct the data column type
const { type, data: { numRows: length, columns }, channels, dim, stack } = this;
// control if Plot's implicit stack transform is applied
// no stacking is done if x2/y2 are used instead of x/y
const _ = type.startsWith('area') && !stack ? '2' : '';
const options = dim === 'x' ? { [`y${_}`]: columns.y } : { [`x${_}`]: columns.x };
for (const c of channels) {
options[c.channel] = channelOption(c, columns);
}
return [{ type, data: { length }, options }];
}
}
function norm(grid, smoothed, delta, type) {
const value = type === true || type === 'sum' ? sum(grid)
: type === 'max' ? max(smoothed)
: delta;
return value || 1;
}