UNPKG

@uwdata/mosaic-plot

Version:

A Mosaic-powered plotting framework based on Observable Plot.

107 lines (90 loc) 3.04 kB
import { throttle } from '@uwdata/mosaic-core'; import { and, isAggregateExpression } from '@uwdata/mosaic-sql'; import { getDatum } from './util/get-datum.js'; import { sanitizeStyles } from './util/sanitize-styles.js'; function configureMark(mark) { const { channels } = mark; const dims = new Set; let ordered = false; let aggregate = false; for (const c of channels) { const { channel, field, as } = c; if (channel === 'orderby') { ordered = true; } else if (field) { if (isAggregateExpression(field)) { aggregate = true; } else { if (dims.has(as)) continue; dims.add(as); } } } // if orderby is defined, we're ok: nothing to do // or, if there is no groupby aggregation, we're ok: nothing to do // grouping may result in optimizations that change result order // so we orderby the grouping dimensions to ensure stable indices if (!ordered && aggregate && dims.size) { mark.channels.push(({ channel: 'orderby', value: Array.from(dims) })); } return mark; } export class Highlight { constructor(mark, { selection, channels = {} }) { this.mark = configureMark(mark); this.selection = selection; const c = Object.entries(sanitizeStyles(channels)); this.channels = c.length ? c : [['opacity', 0.2]]; this.selection.addEventListener('value', throttle(() => this.update())); } init(svg) { this.svg = svg; const values = this.values = []; const index = this.mark.index; const g = `g[data-index="${index}"]`; const selector = `${g} > *:not(g), ${g} > g > *`; const nodes = this.nodes = svg.querySelectorAll(selector); const { channels } = this; for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; values.push(channels.map(c => node.getAttribute(c[0]))); } return this.update(); } async update() { const { svg, nodes, channels, values, mark, selection } = this; if (!svg) return; const test = await predicateFunction(mark, selection); for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; const base = values[i]; const t = test(getDatum(node)); // TODO? handle inherited values / remove attributes for (let j = 0; j < channels.length; ++j) { const [attr, value] = channels[j]; node.setAttribute(attr, t ? base[j] : value); } } } } async function predicateFunction(mark, selection) { const pred = selection?.predicate(mark); if (!pred || pred.length === 0) { return () => true; } // set flag so we do not skip cross-filtered sources const filter = mark.filterBy?.predicate(mark, true); const s = { __: and(pred) }; const q = mark.query(filter); (q.queries || [q]).forEach(q => { q._groupby.length ? q.select(s) : q.setSelect(s); }); const data = await mark.coordinator.query(q); const v = data.getChild?.('__'); return !(data.numRows || data.length) ? (() => false) : v ? (i => v.at(i)) : (i => data[i].__); }