@uwdata/mosaic-plot
Version:
A Mosaic-powered plotting framework based on Observable Plot.
120 lines (110 loc) • 4.08 kB
JavaScript
import { Query, abs, add, and, bitAnd, cond, div, float64, gt, int32, isAggregateExpression, isNotNull, lt, mul, neq, pow, round, sub } from '@uwdata/mosaic-sql';
import { Transient } from '../symbols.js';
import { extentX, extentY, xyext } from './util/extent.js';
import { Mark } from './Mark.js';
import { handleParam } from './util/handle-param.js';
export class HexbinMark extends Mark {
constructor(source, options) {
const { type = 'hexagon', binWidth = 20, ...channels } = options;
super(type, source, { r: binWidth / 2, clip: true, ...channels }, xyext);
/** @type {number} */
this.binWidth = handleParam(binWidth, value => {
return (this.binWidth = value, this.requestUpdate());
});
}
get filterStable() {
const xdom = this.plot.getAttribute('xDomain');
const ydom = this.plot.getAttribute('yDomain');
return xdom && ydom && !xdom[Transient] && !ydom[Transient];
}
query(filter = []) {
if (this.hasOwnData()) return null;
const { plot, binWidth, channels } = this;
// Extract channel information, update top-level query
// and extract dependent columns for aggregates
let xc, yc;
const dims = new Set;
const cols = {};
for (const c of channels) {
if (c.channel === 'orderby') {
// ignore ordering, as we will aggregate
} else if (c.channel === 'x') {
xc = c;
} else if (c.channel === 'y') {
yc = c;
} else if (Object.hasOwn(c, 'field')) {
const { as, field } = c;
cols[as] = field;
if (!isAggregateExpression(field)) {
dims.add(as);
}
}
}
// get x / y extents, may update plot xDomain / yDomain
const [x1, x2] = extentX(this, filter);
const [y1, y2] = extentY(this, filter);
// Adjust screen-space coordinates by top/left
// margins as this is what Observable Plot does.
const ox = 0.5 - plot.getAttribute('marginLeft');
const oy = 0 - plot.getAttribute('marginTop');
const dx = float64(binWidth);
const dy = float64(binWidth * (1.5 / Math.sqrt(3)));
const xr = float64(plot.innerWidth() / (x2 - x1));
const yr = float64(plot.innerHeight() / (y2 - y1));
// column references
const x ='_x';
const y = '_y';
const px = '_px';
const py = '_py';
const pi = '_pi';
const pj = '_pj';
const tt = '_tt';
// Top-level query maps from screen space back to data values.
// Doing so ensures that Plot generates correct data-driven scales.
return Query.select({
[]: add(
float64(x1),
div(add(mul(add(x, mul(0.5, bitAnd(y, 1))), dx), ox), xr)
),
[]: sub(
float64(y2),
div(add(mul(y, dy), oy), yr)
),
...cols
})
.groupby(x, y, ...dims)
.from(
// Subquery performs hex binning in screen space and also passes
// original columns through (the DB should optimize this).
Query.select({
[]: div(sub(mul(yr, sub(y2, yc.field)), oy), dy),
[]: int32(round(py)),
[]: sub(
div(sub(mul(xr, sub(xc.field, x1)), ox), dx),
mul(0.5, bitAnd(pj, 1))
),
[]: int32(round(px)),
[]: and(
gt(mul(abs(sub(py, pj)), 3), 1),
gt(
add(pow(sub(px, pi), 2), pow(sub(py, pj), 2)),
add(
pow(sub(sub(px, pi), mul(0.5, cond(lt(px, pi), -1, 1))), 2),
pow(sub(sub(py, pj), cond(lt(py, pj), -1, 1)), 2)
)
)
),
[]: cond(tt,
int32(add(
add(pi, cond(lt(px, pi), -0.5, 0.5)),
cond(neq(bitAnd(pj, 1), 0), 0.5, -0.5)
)),
pi
),
[]: cond(tt, int32(add(pj, cond(lt(py, pj), -1, 1))), pj)
}, '*')
.from(this.sourceTable())
.where(isNotNull(xc.field), isNotNull(yc.field), filter)
);
}
}