@uwdata/mosaic-plot 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +6 -0
- package/dist/mosaic-plot.js +42313 -0
- package/dist/mosaic-plot.min.js +69 -0
- package/package.json +38 -0
- package/src/index.js +30 -0
- package/src/interactors/Highlight.js +101 -0
- package/src/interactors/Interval1D.js +90 -0
- package/src/interactors/Interval2D.js +102 -0
- package/src/interactors/Nearest.js +66 -0
- package/src/interactors/PanZoom.js +121 -0
- package/src/interactors/Toggle.js +111 -0
- package/src/interactors/util/brush.js +45 -0
- package/src/interactors/util/close-to.js +9 -0
- package/src/interactors/util/get-field.js +4 -0
- package/src/interactors/util/invert.js +3 -0
- package/src/interactors/util/patchScreenCTM.js +13 -0
- package/src/interactors/util/sanitize-styles.js +9 -0
- package/src/interactors/util/to-kebab-case.js +9 -0
- package/src/legend.js +64 -0
- package/src/marks/ConnectedMark.js +66 -0
- package/src/marks/ContourMark.js +89 -0
- package/src/marks/DenseLineMark.js +146 -0
- package/src/marks/Density1DMark.js +104 -0
- package/src/marks/Density2DMark.js +69 -0
- package/src/marks/GeoMark.js +35 -0
- package/src/marks/Grid2DMark.js +191 -0
- package/src/marks/HexbinMark.js +88 -0
- package/src/marks/Mark.js +207 -0
- package/src/marks/RasterMark.js +121 -0
- package/src/marks/RasterTileMark.js +331 -0
- package/src/marks/RegressionMark.js +117 -0
- package/src/marks/util/bin-field.js +17 -0
- package/src/marks/util/density.js +226 -0
- package/src/marks/util/extent.js +56 -0
- package/src/marks/util/grid.js +57 -0
- package/src/marks/util/handle-param.js +14 -0
- package/src/marks/util/is-arrow-table.js +3 -0
- package/src/marks/util/is-color.js +18 -0
- package/src/marks/util/is-constant-option.js +41 -0
- package/src/marks/util/is-symbol.js +20 -0
- package/src/marks/util/raster.js +44 -0
- package/src/marks/util/stats.js +133 -0
- package/src/marks/util/to-data-array.js +70 -0
- package/src/plot-attributes.js +212 -0
- package/src/plot-renderer.js +161 -0
- package/src/plot.js +136 -0
- package/src/symbols.js +3 -0
- package/src/transforms/bin.js +81 -0
- package/src/transforms/index.js +3 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { coordinator } from '@uwdata/mosaic-core';
|
|
2
|
+
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
|
+
import { scale } from '@observablehq/plot';
|
|
4
|
+
import { extentX, extentY } from './util/extent.js';
|
|
5
|
+
import { isColor } from './util/is-color.js';
|
|
6
|
+
import { createCanvas, raster, opacityMap, palette } from './util/raster.js';
|
|
7
|
+
import { Grid2DMark } from './Grid2DMark.js';
|
|
8
|
+
import { binField } from './util/bin-field.js';
|
|
9
|
+
|
|
10
|
+
export class RasterTileMark extends Grid2DMark {
|
|
11
|
+
constructor(source, options) {
|
|
12
|
+
const { origin = [0, 0], dim = 'xy', ...markOptions } = options;
|
|
13
|
+
super('image', source, markOptions);
|
|
14
|
+
|
|
15
|
+
// TODO: make part of data source instead of options?
|
|
16
|
+
this.origin = origin;
|
|
17
|
+
this.tileX = dim.toLowerCase().includes('x');
|
|
18
|
+
this.tileY = dim.toLowerCase().includes('y');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setPlot(plot, index) {
|
|
22
|
+
const update = () => { if (this.stats) this.rasterize(); };
|
|
23
|
+
plot.addAttributeListener('schemeColor', update);
|
|
24
|
+
super.setPlot(plot, index);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
requestQuery() {
|
|
28
|
+
return this.requestTiles();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
query(filter = []) {
|
|
32
|
+
this._filter = filter;
|
|
33
|
+
// we will submit our own queries
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
tileQuery(extent) {
|
|
38
|
+
const { plot, binType, binPad, channels, densityMap, source } = this;
|
|
39
|
+
const [[x0, x1], [y0, y1]] = extent;
|
|
40
|
+
const [nx, ny] = this.bins;
|
|
41
|
+
const bx = binField(this, 'x');
|
|
42
|
+
const by = binField(this, 'y');
|
|
43
|
+
const rx = !!plot.getAttribute('xReverse');
|
|
44
|
+
const ry = !!plot.getAttribute('yReverse');
|
|
45
|
+
const x = bin1d(bx, x0, x1, nx, rx, binPad);
|
|
46
|
+
const y = bin1d(by, y0, y1, ny, ry, binPad);
|
|
47
|
+
|
|
48
|
+
// with padded bins, include the entire domain extent
|
|
49
|
+
// if the bins are flush, exclude the extent max
|
|
50
|
+
const bounds = binPad
|
|
51
|
+
? [isBetween(bx, [x0, x1]), isBetween(by, [y0, y1])]
|
|
52
|
+
: [lte(x0, bx), lt(bx, x1), lte(y0, by), lt(by, y1)];
|
|
53
|
+
|
|
54
|
+
const q = Query
|
|
55
|
+
.from(source.table)
|
|
56
|
+
.where(bounds);
|
|
57
|
+
|
|
58
|
+
const groupby = this.groupby = [];
|
|
59
|
+
let agg = count();
|
|
60
|
+
for (const c of channels) {
|
|
61
|
+
if (Object.hasOwn(c, 'field')) {
|
|
62
|
+
const { channel, field } = c;
|
|
63
|
+
if (field.aggregate) {
|
|
64
|
+
agg = field;
|
|
65
|
+
densityMap[channel] = true;
|
|
66
|
+
} else if (channel === 'weight') {
|
|
67
|
+
agg = sum(field);
|
|
68
|
+
} else if (channel !== 'x' && channel !== 'y') {
|
|
69
|
+
q.select({ [channel]: field });
|
|
70
|
+
groupby.push(channel);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return binType === 'linear'
|
|
76
|
+
? binLinear2d(q, x, y, agg, nx, groupby)
|
|
77
|
+
: bin2d(q, x, y, agg, nx, groupby);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async requestTiles() {
|
|
81
|
+
// get coordinator, cancel prior prefetch queries
|
|
82
|
+
const mc = coordinator();
|
|
83
|
+
if (this.prefetch) mc.cancel(this.prefetch);
|
|
84
|
+
|
|
85
|
+
// get view extent info
|
|
86
|
+
const { binPad, tileX, tileY, origin: [tx, ty] } = this;
|
|
87
|
+
const [m, n] = this.bins = this.binDimensions(this);
|
|
88
|
+
const [x0, x1] = extentX(this, this._filter);
|
|
89
|
+
const [y0, y1] = extentY(this, this._filter);
|
|
90
|
+
const xspan = x1 - x0;
|
|
91
|
+
const yspan = y1 - y0;
|
|
92
|
+
const xx = Math.floor((x0 - tx) * (m - binPad) / xspan);
|
|
93
|
+
const yy = Math.floor((y0 - ty) * (n - binPad) / yspan);
|
|
94
|
+
|
|
95
|
+
const tileExtent = (i, j) => [
|
|
96
|
+
[tx + i * xspan, tx + (i + 1) * xspan],
|
|
97
|
+
[ty + j * yspan, ty + (j + 1) * yspan]
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// get tile coords that overlap current view extent
|
|
101
|
+
const i0 = Math.floor((x0 - tx) / xspan);
|
|
102
|
+
const i1 = tileX ? tileFloor((x1 - tx) / xspan) : i0;
|
|
103
|
+
const j0 = Math.floor((y0 - ty) / yspan);
|
|
104
|
+
const j1 = tileY ? tileFloor((y1 - ty) / yspan) : j0;
|
|
105
|
+
|
|
106
|
+
// query for currently needed data tiles
|
|
107
|
+
const coords = [];
|
|
108
|
+
for (let i = i0; i <= i1; ++i) {
|
|
109
|
+
for (let j = j0; j <= j1; ++j) {
|
|
110
|
+
coords.push([i, j]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const queries = coords.map(
|
|
114
|
+
([i, j]) => mc.query(this.tileQuery(tileExtent(i, j)))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// prefetch tiles along periphery of current tiles
|
|
118
|
+
const prefetchCoords = [];
|
|
119
|
+
if (tileX) {
|
|
120
|
+
for (let j = j0; j <= j1; ++j) {
|
|
121
|
+
prefetchCoords.push([i1 + 1, j]);
|
|
122
|
+
prefetchCoords.push([i0 - 1, j]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (tileY) {
|
|
126
|
+
const x0 = tileX ? i0 - 1 : i0;
|
|
127
|
+
const x1 = tileX ? i1 + 1 : i1;
|
|
128
|
+
for (let i = x0; i <= x1; ++i) {
|
|
129
|
+
prefetchCoords.push([i, j1 + 1]);
|
|
130
|
+
prefetchCoords.push([i, j0 - 1]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.prefetch = prefetchCoords.map(
|
|
134
|
+
([i, j]) => mc.prefetch(this.tileQuery(tileExtent(i, j)))
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// wait for tile queries to complete, then update
|
|
138
|
+
const tiles = await Promise.all(queries);
|
|
139
|
+
this.grids = [{ grid: processTiles(m, n, xx, yy, coords, tiles) }];
|
|
140
|
+
this.convolve().update();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
convolve() {
|
|
144
|
+
return super.convolve().rasterize();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
rasterize() {
|
|
148
|
+
const { bins, kde, groupby } = this;
|
|
149
|
+
const [ w, h ] = bins;
|
|
150
|
+
|
|
151
|
+
// raster data
|
|
152
|
+
const { canvas, ctx, img } = imageData(this, w, h);
|
|
153
|
+
|
|
154
|
+
// scale function to map densities to [0, 1]
|
|
155
|
+
const s = imageScale(this);
|
|
156
|
+
|
|
157
|
+
// gather color domain as needed
|
|
158
|
+
const idx = groupby.indexOf(this.channelField('fill')?.as);
|
|
159
|
+
const domain = idx < 0 ? [] : kde.map(({ key }) => key[idx]);
|
|
160
|
+
|
|
161
|
+
// generate raster images
|
|
162
|
+
this.data = kde.map(grid => {
|
|
163
|
+
const palette = imagePalette(this, domain, grid.key?.[idx]);
|
|
164
|
+
raster(grid, img.data, w, h, s, palette);
|
|
165
|
+
ctx.putImageData(img, 0, 0);
|
|
166
|
+
return { src: canvas.toDataURL() };
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
plotSpecs() {
|
|
173
|
+
const { type, data, plot } = this;
|
|
174
|
+
const options = {
|
|
175
|
+
src: 'src',
|
|
176
|
+
width: plot.innerWidth(),
|
|
177
|
+
height: plot.innerHeight(),
|
|
178
|
+
preserveAspectRatio: 'none',
|
|
179
|
+
imageRendering: this.channel('imageRendering')?.value,
|
|
180
|
+
frameAnchor: 'middle'
|
|
181
|
+
};
|
|
182
|
+
return [{ type, data, options }];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function processTiles(m, n, x, y, coords, tiles) {
|
|
187
|
+
const grid = new Float64Array(m * n);
|
|
188
|
+
tiles.forEach((data, index) => {
|
|
189
|
+
const [i, j] = coords[index];
|
|
190
|
+
const tx = i * m - x;
|
|
191
|
+
const ty = j * n - y;
|
|
192
|
+
copy(m, n, grid, data, tx, ty);
|
|
193
|
+
});
|
|
194
|
+
return grid;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function copy(m, n, grid, values, tx, ty) {
|
|
198
|
+
// index = row + col * width
|
|
199
|
+
const num = values.numRows;
|
|
200
|
+
if (num === 0) return;
|
|
201
|
+
const index = values.getChild('index').toArray();
|
|
202
|
+
const value = values.getChild('value').toArray();
|
|
203
|
+
for (let row = 0; row < num; ++row) {
|
|
204
|
+
const idx = index[row];
|
|
205
|
+
const i = tx + (idx % m);
|
|
206
|
+
const j = ty + Math.floor(idx / m);
|
|
207
|
+
if (0 <= i && i < m && 0 <= j && j < n) {
|
|
208
|
+
grid[i + j * m] = value[row];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function imageData(mark, w, h) {
|
|
214
|
+
if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
|
|
215
|
+
const canvas = createCanvas(w, h);
|
|
216
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
217
|
+
const img = ctx.getImageData(0, 0, w, h);
|
|
218
|
+
mark.image = { canvas, ctx, img, w, h };
|
|
219
|
+
}
|
|
220
|
+
return mark.image;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function imageScale(mark) {
|
|
224
|
+
const { densityMap, kde, plot } = mark;
|
|
225
|
+
let domain = densityMap.fill && plot.getAttribute('colorDomain');
|
|
226
|
+
|
|
227
|
+
// compute kde grid extents if no explicit domain
|
|
228
|
+
if (!domain) {
|
|
229
|
+
let lo = 0, hi = 0;
|
|
230
|
+
kde.forEach(grid => {
|
|
231
|
+
for (const v of grid) {
|
|
232
|
+
if (v < lo) lo = v;
|
|
233
|
+
if (v > hi) hi = v;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
domain = (lo === 0 && hi === 0) ? [0, 1] : [lo, hi];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const type = plot.getAttribute('colorScale');
|
|
240
|
+
return scale({ x: { type, domain, range: [0, 1] } }).apply;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function imagePalette(mark, domain, value, steps = 1024) {
|
|
244
|
+
const { densityMap, plot } = mark;
|
|
245
|
+
const scheme = plot.getAttribute('colorScheme');
|
|
246
|
+
|
|
247
|
+
// initialize color to constant fill, if specified
|
|
248
|
+
const fill = mark.channel('fill');
|
|
249
|
+
let color = isColor(fill?.value) ? fill.value : undefined;
|
|
250
|
+
|
|
251
|
+
if (densityMap.fill || (scheme && !color)) {
|
|
252
|
+
if (scheme) {
|
|
253
|
+
try {
|
|
254
|
+
return palette(
|
|
255
|
+
steps,
|
|
256
|
+
scale({color: { scheme, domain: [0, 1] }}).interpolate
|
|
257
|
+
);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
console.warn(err);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else if (domain.length) {
|
|
263
|
+
// fill is based on data values
|
|
264
|
+
const range = plot.getAttribute('colorRange');
|
|
265
|
+
const spec = {
|
|
266
|
+
domain,
|
|
267
|
+
range,
|
|
268
|
+
scheme: scheme || (range ? undefined : 'tableau10')
|
|
269
|
+
};
|
|
270
|
+
color = scale({ color: spec }).apply(value);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return palette(steps, opacityMap(color));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function bin1d(x, x0, x1, n, reverse, pad) {
|
|
277
|
+
const d = (n - pad) / (x1 - x0);
|
|
278
|
+
const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
279
|
+
return reverse
|
|
280
|
+
? sql`(${x1} - ${x}::DOUBLE)${f}`
|
|
281
|
+
: sql`(${x}::DOUBLE - ${x0})${f}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function bin2d(q, xp, yp, value, xn, groupby) {
|
|
285
|
+
return q
|
|
286
|
+
.select({
|
|
287
|
+
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
288
|
+
value
|
|
289
|
+
})
|
|
290
|
+
.groupby('index', groupby);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
294
|
+
const w = value.column ? `* ${value.column}` : '';
|
|
295
|
+
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
296
|
+
|
|
297
|
+
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
298
|
+
const a = subq(
|
|
299
|
+
sql`FLOOR(xp)::INTEGER + FLOOR(yp)::INTEGER * ${xn}`,
|
|
300
|
+
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
// grid[xu + yv * xn] += (xv - xp) * (yp - yu) * wi;
|
|
304
|
+
const b = subq(
|
|
305
|
+
sql`FLOOR(xp)::INTEGER + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
306
|
+
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// grid[xv + yu * xn] += (xp - xu) * (yv - yp) * wi;
|
|
310
|
+
const c = subq(
|
|
311
|
+
sql`FLOOR(xp)::INTEGER + 1 + FLOOR(yp)::INTEGER * ${xn}`,
|
|
312
|
+
sql`(xp - FLOOR(xp)::INTEGER) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// grid[xv + yv * xn] += (xp - xu) * (yp - yu) * wi;
|
|
316
|
+
const d = subq(
|
|
317
|
+
sql`FLOOR(xp)::INTEGER + 1 + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
318
|
+
sql`(xp - FLOOR(xp)::INTEGER) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return Query
|
|
322
|
+
.from(Query.unionAll(a, b, c, d))
|
|
323
|
+
.select({ index: 'i', value: sum('w') }, groupby)
|
|
324
|
+
.groupby('index', groupby)
|
|
325
|
+
.having(neq('value', 0));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function tileFloor(value) {
|
|
329
|
+
const floored = Math.floor(value);
|
|
330
|
+
return floored === value ? floored - 1 : floored;
|
|
331
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { range } from 'd3';
|
|
2
|
+
import {
|
|
3
|
+
Query, max, min, castDouble, isNotNull,
|
|
4
|
+
regrIntercept, regrSlope, regrCount,
|
|
5
|
+
regrSYY, regrSXX, regrAvgX
|
|
6
|
+
} from '@uwdata/mosaic-sql';
|
|
7
|
+
import { qt } from './util/stats.js';
|
|
8
|
+
import { Mark, channelOption } from './Mark.js';
|
|
9
|
+
import { handleParam } from './util/handle-param.js';
|
|
10
|
+
import { toDataArray } from './util/to-data-array.js';
|
|
11
|
+
|
|
12
|
+
export class RegressionMark extends Mark {
|
|
13
|
+
constructor(source, options) {
|
|
14
|
+
const { ci = 0.95, precision = 4, ...channels } = options;
|
|
15
|
+
super('line', source, channels);
|
|
16
|
+
const update = () => {
|
|
17
|
+
return this.modelFit ? this.confidenceBand().update() : null
|
|
18
|
+
};
|
|
19
|
+
handleParam(this, 'ci', ci, update);
|
|
20
|
+
handleParam(this, 'precision', precision, update);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
query(filter = []) {
|
|
24
|
+
const x = this.channelField('x').as;
|
|
25
|
+
const y = this.channelField('y').as;
|
|
26
|
+
const groupby = Array.from(new Set(
|
|
27
|
+
['stroke', 'z', 'fx', 'fy'].flatMap(c => this.channelField(c)?.as || [])
|
|
28
|
+
));
|
|
29
|
+
|
|
30
|
+
return Query
|
|
31
|
+
.from(super.query(filter))
|
|
32
|
+
.select({
|
|
33
|
+
intercept: regrIntercept(y, x),
|
|
34
|
+
slope: regrSlope(y, x),
|
|
35
|
+
n: regrCount(y, x),
|
|
36
|
+
ssy: regrSYY(y, x),
|
|
37
|
+
ssx: regrSXX(y, x),
|
|
38
|
+
xm: regrAvgX(y, x),
|
|
39
|
+
x0: castDouble(min(x).where(isNotNull(y))),
|
|
40
|
+
x1: castDouble(max(x).where(isNotNull(y)))
|
|
41
|
+
})
|
|
42
|
+
.select(groupby)
|
|
43
|
+
.groupby(groupby);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
queryResult(data) {
|
|
47
|
+
this.modelFit = toDataArray(data);
|
|
48
|
+
|
|
49
|
+
// regression line
|
|
50
|
+
this.lineData = this.modelFit.flatMap(m => linePoints(m));
|
|
51
|
+
|
|
52
|
+
// prepare confidence band
|
|
53
|
+
return this.confidenceBand();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
confidenceBand() {
|
|
57
|
+
// regression ci area
|
|
58
|
+
const { ci, modelFit, precision, plot } = this;
|
|
59
|
+
const w = plot.innerWidth();
|
|
60
|
+
this.areaData = ci ? modelFit.flatMap(m => areaPoints(ci, precision, m, w)) : null;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
plotSpecs() {
|
|
65
|
+
const { lineData, areaData, channels, ci } = this;
|
|
66
|
+
const lopt = { x: 'x', y: 'y' };
|
|
67
|
+
const aopt = { x: 'x', y1: 'y1', y2: 'y2', fillOpacity: 0.1 };
|
|
68
|
+
|
|
69
|
+
for (const c of channels) {
|
|
70
|
+
switch (c.channel) {
|
|
71
|
+
case 'x':
|
|
72
|
+
case 'y':
|
|
73
|
+
case 'fill':
|
|
74
|
+
break;
|
|
75
|
+
case 'stroke':
|
|
76
|
+
lopt.stroke = aopt.fill = channelOption(c);
|
|
77
|
+
break;
|
|
78
|
+
case 'strokeOpacity':
|
|
79
|
+
lopt.strokeOpacity = channelOption(c);
|
|
80
|
+
break;
|
|
81
|
+
case 'fillOpacity':
|
|
82
|
+
aopt.fillOpacity = channelOption(c);
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
lopt[c.channel] = aopt[c.channel] = channelOption(c);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
...(ci ? [{ type: 'areaY', data: areaData, options: aopt }] : []),
|
|
92
|
+
{ type: 'line', data: lineData, options: lopt }
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function linePoints(model) {
|
|
98
|
+
// eslint-disable-next-line no-unused-vars
|
|
99
|
+
const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = model;
|
|
100
|
+
return [
|
|
101
|
+
{ x: x0, y: intercept + x0 * slope, ...rest },
|
|
102
|
+
{ x: x1, y: intercept + x1 * slope, ...rest }
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function areaPoints(ci, precision, model, width) {
|
|
107
|
+
const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = model;
|
|
108
|
+
const pp = precision * (x1 - x0) / width;
|
|
109
|
+
const t_sy = qt((1 - ci) / 2, n - 2) * Math.sqrt(ssy / (n - 2));
|
|
110
|
+
return range(x0, x1 - pp / 2, pp)
|
|
111
|
+
.concat(x1)
|
|
112
|
+
.map(x => {
|
|
113
|
+
const y = intercept + x * slope;
|
|
114
|
+
const ye = t_sy * Math.sqrt(1 / n + (x - xm) ** 2 / ssx);
|
|
115
|
+
return { x, y1: y - ye, y2: y + ye, ...rest };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { epoch_ms, sql } from '@uwdata/mosaic-sql';
|
|
2
|
+
|
|
3
|
+
export function binField(mark, channel, expr) {
|
|
4
|
+
if (!mark.stats) return field;
|
|
5
|
+
const { field } = mark.channelField(channel);
|
|
6
|
+
const { type } = mark.stats[field.column];
|
|
7
|
+
expr = expr ?? field;
|
|
8
|
+
return type === 'date' ? epoch_ms(expr) : expr;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function bin1d(x, x0, x1, n, reverse = false, pad = 1) {
|
|
12
|
+
const d = (n - pad) / (x1 - x0);
|
|
13
|
+
const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
14
|
+
return reverse
|
|
15
|
+
? sql`(${+x1} - ${x}::DOUBLE)${f}`
|
|
16
|
+
: sql`(${x}::DOUBLE - ${+x0})${f}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Deriche's approximation of Gaussian smoothing
|
|
2
|
+
// Adapted from Getreuer's C implementation (BSD license)
|
|
3
|
+
// https://www.ipol.im/pub/art/2013/87/gaussian_20131215.tgz
|
|
4
|
+
// http://dev.ipol.im/~getreuer/code/doc/gaussian_20131215_doc/gaussian__conv__deriche_8c.html
|
|
5
|
+
|
|
6
|
+
export function dericheConfig(sigma, negative = false) {
|
|
7
|
+
// compute causal filter coefficients
|
|
8
|
+
const a = new Float64Array(5);
|
|
9
|
+
const bc = new Float64Array(4);
|
|
10
|
+
dericheCausalCoeff(a, bc, sigma);
|
|
11
|
+
|
|
12
|
+
// numerator coefficients of the anticausal filter
|
|
13
|
+
const ba = Float64Array.of(
|
|
14
|
+
0,
|
|
15
|
+
bc[1] - a[1] * bc[0],
|
|
16
|
+
bc[2] - a[2] * bc[0],
|
|
17
|
+
bc[3] - a[3] * bc[0],
|
|
18
|
+
-a[4] * bc[0]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// impulse response sums
|
|
22
|
+
const accum_denom = 1.0 + a[1] + a[2] + a[3] + a[4];
|
|
23
|
+
const sum_causal = (bc[0] + bc[1] + bc[2] + bc[3]) / accum_denom;
|
|
24
|
+
const sum_anticausal = (ba[1] + ba[2] + ba[3] + ba[4]) / accum_denom;
|
|
25
|
+
|
|
26
|
+
// coefficients object
|
|
27
|
+
return {
|
|
28
|
+
sigma,
|
|
29
|
+
negative,
|
|
30
|
+
a,
|
|
31
|
+
b_causal: bc,
|
|
32
|
+
b_anticausal: ba,
|
|
33
|
+
sum_causal,
|
|
34
|
+
sum_anticausal
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function dericheCausalCoeff(a_out, b_out, sigma) {
|
|
39
|
+
const K = 4;
|
|
40
|
+
|
|
41
|
+
const alpha = Float64Array.of(
|
|
42
|
+
0.84, 1.8675,
|
|
43
|
+
0.84, -1.8675,
|
|
44
|
+
-0.34015, -0.1299,
|
|
45
|
+
-0.34015, 0.1299
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const x1 = Math.exp(-1.783 / sigma);
|
|
49
|
+
const x2 = Math.exp(-1.723 / sigma);
|
|
50
|
+
const y1 = 0.6318 / sigma;
|
|
51
|
+
const y2 = 1.997 / sigma;
|
|
52
|
+
const beta = Float64Array.of(
|
|
53
|
+
-x1 * Math.cos( y1), x1 * Math.sin( y1),
|
|
54
|
+
-x1 * Math.cos(-y1), x1 * Math.sin(-y1),
|
|
55
|
+
-x2 * Math.cos( y2), x2 * Math.sin( y2),
|
|
56
|
+
-x2 * Math.cos(-y2), x2 * Math.sin(-y2)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const denom = sigma * 2.5066282746310007;
|
|
60
|
+
|
|
61
|
+
// initialize b/a = alpha[0] / (1 + beta[0] z^-1)
|
|
62
|
+
const b = Float64Array.of(alpha[0], alpha[1], 0, 0, 0, 0, 0, 0);
|
|
63
|
+
const a = Float64Array.of(1, 0, beta[0], beta[1], 0, 0, 0, 0, 0, 0);
|
|
64
|
+
|
|
65
|
+
let j, k;
|
|
66
|
+
|
|
67
|
+
for (k = 2; k < 8; k += 2) {
|
|
68
|
+
// add kth term, b/a += alpha[k] / (1 + beta[k] z^-1)
|
|
69
|
+
b[k] = beta[k] * b[k - 2] - beta[k + 1] * b[k - 1];
|
|
70
|
+
b[k + 1] = beta[k] * b[k - 1] + beta[k + 1] * b[k - 2];
|
|
71
|
+
for (j = k - 2; j > 0; j -= 2) {
|
|
72
|
+
b[j] += beta[k] * b[j - 2] - beta[k + 1] * b[j - 1];
|
|
73
|
+
b[j + 1] += beta[k] * b[j - 1] + beta[k + 1] * b[j - 2];
|
|
74
|
+
}
|
|
75
|
+
for (j = 0; j <= k; j += 2) {
|
|
76
|
+
b[j] += alpha[k] * a[j] - alpha[k + 1] * a[j + 1];
|
|
77
|
+
b[j + 1] += alpha[k] * a[j + 1] + alpha[k + 1] * a[j];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
a[k + 2] = beta[k] * a[k] - beta[k + 1] * a[k + 1];
|
|
81
|
+
a[k + 3] = beta[k] * a[k + 1] + beta[k + 1] * a[k];
|
|
82
|
+
for (j = k; j > 0; j -= 2) {
|
|
83
|
+
a[j] += beta[k] * a[j - 2] - beta[k + 1] * a[j - 1];
|
|
84
|
+
a[j + 1] += beta[k] * a[j - 1] + beta[k + 1] * a[j - 2];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (k = 0; k < K; ++k) {
|
|
89
|
+
j = k << 1;
|
|
90
|
+
b_out[k] = b[j] / denom;
|
|
91
|
+
a_out[k + 1] = a[j + 2];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function dericheConv2d(cx, cy, grid, [nx, ny]) {
|
|
96
|
+
// allocate buffers
|
|
97
|
+
const yc = new Float64Array(Math.max(nx, ny)); // causal
|
|
98
|
+
const ya = new Float64Array(Math.max(nx, ny)); // anticausal
|
|
99
|
+
const h = new Float64Array(5);
|
|
100
|
+
const d = new Float64Array(grid.length);
|
|
101
|
+
|
|
102
|
+
// convolve rows
|
|
103
|
+
for (let row = 0, r0 = 0; row < ny; ++row, r0 += nx) {
|
|
104
|
+
const dx = d.subarray(r0);
|
|
105
|
+
dericheConv1d(cx, grid.subarray(r0), nx, 1, yc, ya, h, dx);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// convolve columns
|
|
109
|
+
for (let c0 = 0; c0 < nx; ++c0) {
|
|
110
|
+
const dy = d.subarray(c0);
|
|
111
|
+
dericheConv1d(cy, dy, ny, nx, yc, ya, h, dy);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return d;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function dericheConv1d(
|
|
118
|
+
c, src, N,
|
|
119
|
+
stride = 1,
|
|
120
|
+
y_causal = new Float64Array(N),
|
|
121
|
+
y_anticausal = new Float64Array(N),
|
|
122
|
+
h = new Float64Array(5),
|
|
123
|
+
d = y_causal,
|
|
124
|
+
init = dericheInitZeroPad
|
|
125
|
+
) {
|
|
126
|
+
const stride_2 = stride * 2;
|
|
127
|
+
const stride_3 = stride * 3;
|
|
128
|
+
const stride_4 = stride * 4;
|
|
129
|
+
const stride_N = stride * N;
|
|
130
|
+
let i, n;
|
|
131
|
+
|
|
132
|
+
// initialize causal filter on the left boundary
|
|
133
|
+
init(
|
|
134
|
+
y_causal, src, N, stride,
|
|
135
|
+
c.b_causal, 3, c.a, 4, c.sum_causal, h, c.sigma
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// filter the interior samples using a 4th order filter. Implements:
|
|
139
|
+
// for n = K, ..., N - 1,
|
|
140
|
+
// y^+(n) = \sum_{k=0}^{K-1} b^+_k src(n - k)
|
|
141
|
+
// - \sum_{k=1}^K a_k y^+(n - k)
|
|
142
|
+
// variable i tracks the offset to the nth sample of src, it is
|
|
143
|
+
// updated together with n such that i = stride * n.
|
|
144
|
+
for (n = 4, i = stride_4; n < N; ++n, i += stride) {
|
|
145
|
+
y_causal[n] = c.b_causal[0] * src[i]
|
|
146
|
+
+ c.b_causal[1] * src[i - stride]
|
|
147
|
+
+ c.b_causal[2] * src[i - stride_2]
|
|
148
|
+
+ c.b_causal[3] * src[i - stride_3]
|
|
149
|
+
- c.a[1] * y_causal[n - 1]
|
|
150
|
+
- c.a[2] * y_causal[n - 2]
|
|
151
|
+
- c.a[3] * y_causal[n - 3]
|
|
152
|
+
- c.a[4] * y_causal[n - 4];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// initialize the anticausal filter on the right boundary
|
|
156
|
+
init(
|
|
157
|
+
y_anticausal, src, N, -stride,
|
|
158
|
+
c.b_anticausal, 4, c.a, 4, c.sum_anticausal, h, c.sigma
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// similar to the causal filter above, the following implements:
|
|
162
|
+
// for n = K, ..., N - 1,
|
|
163
|
+
// y^-(n) = \sum_{k=1}^K b^-_k src(N - n - 1 - k)
|
|
164
|
+
// - \sum_{k=1}^K a_k y^-(n - k)
|
|
165
|
+
// variable i is updated such that i = stride * (N - n - 1).
|
|
166
|
+
for (n = 4, i = stride_N - stride * 5; n < N; ++n, i -= stride) {
|
|
167
|
+
y_anticausal[n] = c.b_anticausal[1] * src[i + stride]
|
|
168
|
+
+ c.b_anticausal[2] * src[i + stride_2]
|
|
169
|
+
+ c.b_anticausal[3] * src[i + stride_3]
|
|
170
|
+
+ c.b_anticausal[4] * src[i + stride_4]
|
|
171
|
+
- c.a[1] * y_anticausal[n - 1]
|
|
172
|
+
- c.a[2] * y_anticausal[n - 2]
|
|
173
|
+
- c.a[3] * y_anticausal[n - 3]
|
|
174
|
+
- c.a[4] * y_anticausal[n - 4];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// sum the causal and anticausal responses to obtain the final result
|
|
178
|
+
if (c.negative) {
|
|
179
|
+
// do not threshold if the input grid includes negatively weighted values
|
|
180
|
+
for (n = 0, i = 0; n < N; ++n, i += stride) {
|
|
181
|
+
d[i] = y_causal[n] + y_anticausal[N - n - 1];
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// threshold to prevent small negative values due to floating point error
|
|
185
|
+
for (n = 0, i = 0; n < N; ++n, i += stride) {
|
|
186
|
+
d[i] = Math.max(0, y_causal[n] + y_anticausal[N - n - 1]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return d;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function dericheInitZeroPad(dest, src, N, stride, b, p, a, q, sum, h) {
|
|
194
|
+
const stride_N = Math.abs(stride) * N;
|
|
195
|
+
const off = stride < 0 ? stride_N + stride : 0;
|
|
196
|
+
let i, n, m;
|
|
197
|
+
|
|
198
|
+
// compute the first q taps of the impulse response, h_0, ..., h_{q-1}
|
|
199
|
+
for (n = 0; n < q; ++n) {
|
|
200
|
+
h[n] = (n <= p) ? b[n] : 0;
|
|
201
|
+
for (m = 1; m <= q && m <= n; ++m) {
|
|
202
|
+
h[n] -= a[m] * h[n - m];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// compute dest_m = sum_{n=1}^m h_{m-n} src_n, m = 0, ..., q-1
|
|
207
|
+
// note: q == 4
|
|
208
|
+
for (m = 0; m < q; ++m) {
|
|
209
|
+
for (dest[m] = 0, n = 1; n <= m; ++n) {
|
|
210
|
+
i = off + stride * n;
|
|
211
|
+
if (i >= 0 && i < stride_N) {
|
|
212
|
+
dest[m] += h[m - n] * src[i];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// dest_m = dest_m + h_{n+m} src_{-n}
|
|
218
|
+
const cur = src[off];
|
|
219
|
+
if (cur > 0) {
|
|
220
|
+
for (m = 0; m < q; ++m) {
|
|
221
|
+
dest[m] += h[m] * cur;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return;
|
|
226
|
+
}
|