@uwdata/mosaic-plot 0.6.0 → 0.6.1
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/dist/mosaic-plot.js +5886 -5651
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +3 -3
- package/src/marks/Grid2DMark.js +22 -2
- package/src/marks/util/arrow.js +30 -0
- package/src/marks/util/density.js +26 -7
- package/src/marks/util/grid.js +42 -48
- package/src/marks/util/interpolate.js +196 -0
- package/src/plot.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A Mosaic-powered plotting framework based on Observable Plot.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@observablehq/plot": "^0.6.13",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.6.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.6.1",
|
|
33
33
|
"@uwdata/mosaic-sql": "^0.6.0",
|
|
34
34
|
"d3": "^7.8.5",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "9e788e6dc5241fa1c54967a25fd9599f97da1a41"
|
|
38
38
|
}
|
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -5,6 +5,9 @@ import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
|
5
5
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
6
6
|
import { grid2d } from './util/grid.js';
|
|
7
7
|
import { handleParam } from './util/handle-param.js';
|
|
8
|
+
import {
|
|
9
|
+
interpolateNearest, interpolatorBarycentric, interpolatorRandomWalk
|
|
10
|
+
} from './util/interpolate.js';
|
|
8
11
|
import { Mark } from './Mark.js';
|
|
9
12
|
|
|
10
13
|
export const DENSITY = 'density';
|
|
@@ -121,8 +124,9 @@ export class Grid2DMark extends Mark {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
queryResult(data) {
|
|
124
|
-
const [
|
|
125
|
-
|
|
127
|
+
const [w, h] = this.bins;
|
|
128
|
+
const interp = maybeInterpolate(this.interpolate);
|
|
129
|
+
this.grids = grid2d(w, h, data, this.aggr, this.groupby, interp);
|
|
126
130
|
return this.convolve();
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -179,6 +183,22 @@ function createDensityMap(channels) {
|
|
|
179
183
|
return densityMap;
|
|
180
184
|
}
|
|
181
185
|
|
|
186
|
+
function maybeInterpolate(interpolate = 'none') {
|
|
187
|
+
if (typeof interpolate === 'function') return interpolate;
|
|
188
|
+
switch (`${interpolate}`.toLowerCase()) {
|
|
189
|
+
case 'none':
|
|
190
|
+
case 'linear':
|
|
191
|
+
return undefined; // no special interpolation need
|
|
192
|
+
case 'nearest':
|
|
193
|
+
return interpolateNearest;
|
|
194
|
+
case 'barycentric':
|
|
195
|
+
return interpolatorBarycentric();
|
|
196
|
+
case 'random-walk':
|
|
197
|
+
return interpolatorRandomWalk();
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`invalid interpolate: ${interpolate}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
182
202
|
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
183
203
|
return q
|
|
184
204
|
.select({
|
package/src/marks/util/arrow.js
CHANGED
|
@@ -7,6 +7,17 @@ export function isArrowTable(values) {
|
|
|
7
7
|
return typeof values?.getChild === 'function';
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function convertArrowType(type) {
|
|
11
|
+
switch (type.typeId) {
|
|
12
|
+
case INTEGER:
|
|
13
|
+
case FLOAT:
|
|
14
|
+
case DECIMAL:
|
|
15
|
+
return Float64Array;
|
|
16
|
+
default:
|
|
17
|
+
return Array;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export function convertArrow(type) {
|
|
11
22
|
const { typeId } = type;
|
|
12
23
|
|
|
@@ -23,3 +34,22 @@ export function convertArrow(type) {
|
|
|
23
34
|
// otherwise use Arrow JS defaults
|
|
24
35
|
return v => v;
|
|
25
36
|
}
|
|
37
|
+
|
|
38
|
+
export function convertArrowColumn(column) {
|
|
39
|
+
const { type } = column;
|
|
40
|
+
const { typeId } = type;
|
|
41
|
+
|
|
42
|
+
// map bignum to number
|
|
43
|
+
if (typeId === INTEGER && type.bitWidth >= 64) {
|
|
44
|
+
const size = column.length;
|
|
45
|
+
const array = new Float64Array(size);
|
|
46
|
+
for (let row = 0; row < size; ++row) {
|
|
47
|
+
const v = column.get(row);
|
|
48
|
+
array[row] = v == null ? NaN : Number(v);
|
|
49
|
+
}
|
|
50
|
+
return array;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// otherwise use Arrow JS defaults
|
|
54
|
+
return column.toArray();
|
|
55
|
+
}
|
|
@@ -96,7 +96,7 @@ export function dericheConv2d(cx, cy, grid, [nx, ny]) {
|
|
|
96
96
|
// allocate buffers
|
|
97
97
|
const yc = new Float64Array(Math.max(nx, ny)); // causal
|
|
98
98
|
const ya = new Float64Array(Math.max(nx, ny)); // anticausal
|
|
99
|
-
const h = new Float64Array(5);
|
|
99
|
+
const h = new Float64Array(5); // q + 1
|
|
100
100
|
const d = new Float64Array(grid.length);
|
|
101
101
|
|
|
102
102
|
// convolve rows
|
|
@@ -119,7 +119,7 @@ export function dericheConv1d(
|
|
|
119
119
|
stride = 1,
|
|
120
120
|
y_causal = new Float64Array(N),
|
|
121
121
|
y_anticausal = new Float64Array(N),
|
|
122
|
-
h = new Float64Array(5),
|
|
122
|
+
h = new Float64Array(5), // q + 1
|
|
123
123
|
d = y_causal,
|
|
124
124
|
init = dericheInitZeroPad
|
|
125
125
|
) {
|
|
@@ -153,6 +153,7 @@ export function dericheConv1d(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// initialize the anticausal filter on the right boundary
|
|
156
|
+
// dest, src, N, stride, b, p, a, q, sum, h
|
|
156
157
|
init(
|
|
157
158
|
y_anticausal, src, N, -stride,
|
|
158
159
|
c.b_anticausal, 4, c.a, 4, c.sum_anticausal, h, c.sigma
|
|
@@ -176,7 +177,7 @@ export function dericheConv1d(
|
|
|
176
177
|
|
|
177
178
|
// sum the causal and anticausal responses to obtain the final result
|
|
178
179
|
if (c.negative) {
|
|
179
|
-
// do not threshold if the input grid includes
|
|
180
|
+
// do not threshold if the input grid includes negative values
|
|
180
181
|
for (n = 0, i = 0; n < N; ++n, i += stride) {
|
|
181
182
|
d[i] = y_causal[n] + y_anticausal[N - n - 1];
|
|
182
183
|
}
|
|
@@ -190,13 +191,16 @@ export function dericheConv1d(
|
|
|
190
191
|
return d;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
export function dericheInitZeroPad(
|
|
194
|
+
export function dericheInitZeroPad(
|
|
195
|
+
dest, src, N, stride, b, p, a, q,
|
|
196
|
+
sum, h, sigma, tol = 0.5
|
|
197
|
+
) {
|
|
194
198
|
const stride_N = Math.abs(stride) * N;
|
|
195
199
|
const off = stride < 0 ? stride_N + stride : 0;
|
|
196
200
|
let i, n, m;
|
|
197
201
|
|
|
198
202
|
// compute the first q taps of the impulse response, h_0, ..., h_{q-1}
|
|
199
|
-
for (n = 0; n
|
|
203
|
+
for (n = 0; n <= q; ++n) {
|
|
200
204
|
h[n] = (n <= p) ? b[n] : 0;
|
|
201
205
|
for (m = 1; m <= q && m <= n; ++m) {
|
|
202
206
|
h[n] -= a[m] * h[n - m];
|
|
@@ -214,12 +218,27 @@ export function dericheInitZeroPad(dest, src, N, stride, b, p, a, q, sum, h) {
|
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
// dest_m = dest_m + h_{n+m} src_{-n}
|
|
218
221
|
const cur = src[off];
|
|
219
|
-
|
|
222
|
+
const max_iter = Math.ceil(sigma * 10);
|
|
223
|
+
for (n = 0; n < max_iter; ++n) {
|
|
224
|
+
/* dest_m = dest_m + h_{n+m} src_{-n} */
|
|
220
225
|
for (m = 0; m < q; ++m) {
|
|
221
226
|
dest[m] += h[m] * cur;
|
|
222
227
|
}
|
|
228
|
+
|
|
229
|
+
sum -= Math.abs(h[0]);
|
|
230
|
+
if (sum <= tol) break;
|
|
231
|
+
|
|
232
|
+
/* Compute the next impulse response tap, h_{n+q} */
|
|
233
|
+
h[q] = (n + q <= p) ? b[n + q] : 0;
|
|
234
|
+
for (m = 1; m <= q; ++m) {
|
|
235
|
+
h[q] -= a[m] * h[q - m];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Shift the h array for the next iteration */
|
|
239
|
+
for (m = 0; m < q; ++m) {
|
|
240
|
+
h[m] = h[m + 1];
|
|
241
|
+
}
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
return;
|
package/src/marks/util/grid.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { InternSet, ascending } from 'd3';
|
|
2
|
-
import {
|
|
2
|
+
import { convertArrowColumn, convertArrowType, isArrowTable } from './arrow.js';
|
|
3
3
|
|
|
4
4
|
function arrayType(values, name = 'density') {
|
|
5
5
|
if (isArrowTable(values)) {
|
|
6
|
-
|
|
7
|
-
switch (type.typeId) {
|
|
8
|
-
case INTEGER:
|
|
9
|
-
case FLOAT:
|
|
10
|
-
case DECIMAL:
|
|
11
|
-
return Float64Array;
|
|
12
|
-
default:
|
|
13
|
-
return Array;
|
|
14
|
-
}
|
|
6
|
+
return convertArrowType(values.getChild(name).type);
|
|
15
7
|
} else {
|
|
16
8
|
return typeof values[0][name] === 'number' ? Float64Array : Array;
|
|
17
9
|
}
|
|
@@ -22,27 +14,13 @@ export function grid1d(n, values) {
|
|
|
22
14
|
return valuesToGrid(new Type(n), values);
|
|
23
15
|
}
|
|
24
16
|
|
|
25
|
-
export function grid2d(m, n, values, aggr, groupby = []) {
|
|
26
|
-
if (groupby.length) {
|
|
27
|
-
// generate grids per group
|
|
28
|
-
return groupedValuesToGrids(m * n, values, aggr, groupby);
|
|
29
|
-
} else {
|
|
30
|
-
const cell = {};
|
|
31
|
-
aggr.forEach(name => {
|
|
32
|
-
const Type = arrayType(values, name);
|
|
33
|
-
cell[name] = valuesToGrid(new Type(m * n), values, name);
|
|
34
|
-
});
|
|
35
|
-
return [cell];
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
17
|
function valuesToGrid(grid, values, name = 'density') {
|
|
40
18
|
if (isArrowTable(values)) {
|
|
41
19
|
// optimize access for Arrow tables
|
|
42
20
|
const numRows = values.numRows;
|
|
43
21
|
if (numRows === 0) return grid;
|
|
44
|
-
const index = values.getChild('index')
|
|
45
|
-
const value = values.getChild(name)
|
|
22
|
+
const index = convertArrowColumn(values.getChild('index'));
|
|
23
|
+
const value = convertArrowColumn(values.getChild(name));
|
|
46
24
|
for (let row = 0; row < numRows; ++row) {
|
|
47
25
|
grid[index[row]] = value[row];
|
|
48
26
|
}
|
|
@@ -55,30 +33,35 @@ function valuesToGrid(grid, values, name = 'density') {
|
|
|
55
33
|
return grid;
|
|
56
34
|
}
|
|
57
35
|
|
|
58
|
-
function
|
|
36
|
+
export function grid2d(w, h, values, aggr, groupby = [], interpolate) {
|
|
37
|
+
const size = w * h;
|
|
59
38
|
const Types = aggr.map(name => arrayType(values, name));
|
|
60
39
|
const numAggr = aggr.length;
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
groupby.forEach((name, i) => cell[name] = key[i]);
|
|
68
|
-
aggr.forEach((name, i) => cell[name] = new Types[i](size));
|
|
69
|
-
}
|
|
41
|
+
// grid data tuples
|
|
42
|
+
const createCell = (key) => {
|
|
43
|
+
const cell = {};
|
|
44
|
+
groupby.forEach((name, i) => cell[name] = key[i]);
|
|
45
|
+
aggr.forEach((name, i) => cell[name] = new Types[i](size));
|
|
70
46
|
return cell;
|
|
71
47
|
};
|
|
48
|
+
const cellMap = {};
|
|
49
|
+
const baseCell = groupby.length ? null : (cellMap[[]] = createCell([]));
|
|
50
|
+
const getCell = groupby.length
|
|
51
|
+
? key => cellMap[key] ?? (cellMap[key] = createCell(key))
|
|
52
|
+
: () => baseCell;
|
|
72
53
|
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (numRows === 0) return [];
|
|
54
|
+
// early exit if empty query result
|
|
55
|
+
const numRows = values.numRows;
|
|
56
|
+
if (numRows === 0) return Object.values(cellMap);
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
// extract arrays from arrow table
|
|
59
|
+
const index = convertArrowColumn(values.getChild('index'));
|
|
60
|
+
const value = aggr.map(name => convertArrowColumn(values.getChild(name)));
|
|
61
|
+
const groups = groupby.map(name => values.getChild(name));
|
|
81
62
|
|
|
63
|
+
if (!interpolate) {
|
|
64
|
+
// if no interpolation, copy values over
|
|
82
65
|
for (let row = 0; row < numRows; ++row) {
|
|
83
66
|
const key = groups.map(vec => vec.get(row));
|
|
84
67
|
const cell = getCell(key);
|
|
@@ -87,14 +70,25 @@ function groupedValuesToGrids(size, values, aggr, groupby) {
|
|
|
87
70
|
}
|
|
88
71
|
}
|
|
89
72
|
} else {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (let
|
|
95
|
-
|
|
73
|
+
// prepare index arrays, then interpolate grid values
|
|
74
|
+
const X = index.map(k => k % w);
|
|
75
|
+
const Y = index.map(k => Math.floor(k / w));
|
|
76
|
+
if (groupby.length) {
|
|
77
|
+
for (let row = 0; row < numRows; ++row) {
|
|
78
|
+
const key = groups.map(vec => vec.get(row));
|
|
79
|
+
const cell = getCell(key);
|
|
80
|
+
if (!cell.index) { cell.index = []; }
|
|
81
|
+
cell.index.push(row);
|
|
96
82
|
}
|
|
83
|
+
} else {
|
|
84
|
+
baseCell.index = index.map((_, i) => i);
|
|
97
85
|
}
|
|
86
|
+
Object.values(cellMap).forEach(cell => {
|
|
87
|
+
for (let i = 0; i < numAggr; ++i) {
|
|
88
|
+
interpolate(cell.index, w, h, X, Y, value[i], cell[aggr[i]]);
|
|
89
|
+
}
|
|
90
|
+
delete cell.index;
|
|
91
|
+
})
|
|
98
92
|
}
|
|
99
93
|
|
|
100
94
|
return Object.values(cellMap);
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Delaunay, randomLcg } from 'd3';
|
|
2
|
+
|
|
3
|
+
export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
|
|
4
|
+
return (index, width, height, X, Y, V, W) => {
|
|
5
|
+
// Interpolate the interior of all triangles with barycentric coordinates
|
|
6
|
+
const {points, triangles, hull} = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
7
|
+
const S = new Uint8Array(width * height); // 1 if pixel has been seen.
|
|
8
|
+
const mix = mixer(V, random);
|
|
9
|
+
|
|
10
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
11
|
+
const ta = triangles[i];
|
|
12
|
+
const tb = triangles[i + 1];
|
|
13
|
+
const tc = triangles[i + 2];
|
|
14
|
+
const Ax = points[2 * ta];
|
|
15
|
+
const Bx = points[2 * tb];
|
|
16
|
+
const Cx = points[2 * tc];
|
|
17
|
+
const Ay = points[2 * ta + 1];
|
|
18
|
+
const By = points[2 * tb + 1];
|
|
19
|
+
const Cy = points[2 * tc + 1];
|
|
20
|
+
const x1 = Math.min(Ax, Bx, Cx);
|
|
21
|
+
const x2 = Math.max(Ax, Bx, Cx);
|
|
22
|
+
const y1 = Math.min(Ay, By, Cy);
|
|
23
|
+
const y2 = Math.max(Ay, By, Cy);
|
|
24
|
+
const z = (By - Cy) * (Ax - Cx) + (Ay - Cy) * (Cx - Bx);
|
|
25
|
+
if (!z) continue;
|
|
26
|
+
const va = V[index[ta]];
|
|
27
|
+
const vb = V[index[tb]];
|
|
28
|
+
const vc = V[index[tc]];
|
|
29
|
+
for (let x = Math.floor(x1); x < x2; ++x) {
|
|
30
|
+
for (let y = Math.floor(y1); y < y2; ++y) {
|
|
31
|
+
if (x < 0 || x >= width || y < 0 || y >= height) continue;
|
|
32
|
+
const xp = x + 0.5; // sample pixel centroids
|
|
33
|
+
const yp = y + 0.5;
|
|
34
|
+
const ga = ((By - Cy) * (xp - Cx) + (yp - Cy) * (Cx - Bx)) / z;
|
|
35
|
+
if (ga < 0) continue;
|
|
36
|
+
const gb = ((Cy - Ay) * (xp - Cx) + (yp - Cy) * (Ax - Cx)) / z;
|
|
37
|
+
if (gb < 0) continue;
|
|
38
|
+
const gc = 1 - ga - gb;
|
|
39
|
+
if (gc < 0) continue;
|
|
40
|
+
const i = x + width * y;
|
|
41
|
+
W[i] = mix(va, ga, vb, gb, vc, gc, x, y);
|
|
42
|
+
S[i] = 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix);
|
|
47
|
+
return W;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Extrapolate by finding the closest point on the hull.
|
|
52
|
+
function extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix) {
|
|
53
|
+
X = Float64Array.from(hull, (i) => X[index[i]]);
|
|
54
|
+
Y = Float64Array.from(hull, (i) => Y[index[i]]);
|
|
55
|
+
V = Array.from(hull, (i) => V[index[i]]);
|
|
56
|
+
const n = X.length;
|
|
57
|
+
const rays = Array.from({length: n}, (_, j) => ray(j, X, Y));
|
|
58
|
+
let k = 0;
|
|
59
|
+
for (let y = 0; y < height; ++y) {
|
|
60
|
+
const yp = y + 0.5;
|
|
61
|
+
for (let x = 0; x < width; ++x) {
|
|
62
|
+
const i = x + width * y;
|
|
63
|
+
if (!S[i]) {
|
|
64
|
+
const xp = x + 0.5;
|
|
65
|
+
for (let l = 0; l < n; ++l) {
|
|
66
|
+
const j = (n + k + (l % 2 ? (l + 1) / 2 : -l / 2)) % n;
|
|
67
|
+
if (rays[j](xp, yp)) {
|
|
68
|
+
const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
|
|
69
|
+
W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
|
|
70
|
+
k = j;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Projects a point p = [x, y] onto the line segment [p1, p2], returning the
|
|
80
|
+
// projected coordinates p’ as t in [0, 1] with p’ = t p1 + (1 - t) p2.
|
|
81
|
+
function segmentProject(x1, y1, x2, y2, x, y) {
|
|
82
|
+
const dx = x2 - x1;
|
|
83
|
+
const dy = y2 - y1;
|
|
84
|
+
const a = dx * (x2 - x) + dy * (y2 - y);
|
|
85
|
+
const b = dx * (x - x1) + dy * (y - y1);
|
|
86
|
+
return a > 0 && b > 0 ? a / (a + b) : +(a > b);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cross(xa, ya, xb, yb) {
|
|
90
|
+
return xa * yb - xb * ya;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ray(j, X, Y) {
|
|
94
|
+
const n = X.length;
|
|
95
|
+
const xc = X.at(j - 2);
|
|
96
|
+
const yc = Y.at(j - 2);
|
|
97
|
+
const xa = X.at(j - 1);
|
|
98
|
+
const ya = Y.at(j - 1);
|
|
99
|
+
const xb = X[j];
|
|
100
|
+
const yb = Y[j];
|
|
101
|
+
const xd = X.at(j + 1 - n);
|
|
102
|
+
const yd = Y.at(j + 1 - n);
|
|
103
|
+
const dxab = xa - xb;
|
|
104
|
+
const dyab = ya - yb;
|
|
105
|
+
const dxca = xc - xa;
|
|
106
|
+
const dyca = yc - ya;
|
|
107
|
+
const dxbd = xb - xd;
|
|
108
|
+
const dybd = yb - yd;
|
|
109
|
+
const hab = Math.hypot(dxab, dyab);
|
|
110
|
+
const hca = Math.hypot(dxca, dyca);
|
|
111
|
+
const hbd = Math.hypot(dxbd, dybd);
|
|
112
|
+
return (x, y) => {
|
|
113
|
+
const dxa = x - xa;
|
|
114
|
+
const dya = y - ya;
|
|
115
|
+
const dxb = x - xb;
|
|
116
|
+
const dyb = y - yb;
|
|
117
|
+
return (
|
|
118
|
+
cross(dxa, dya, dxb, dyb) > -1e-6 &&
|
|
119
|
+
cross(dxa, dya, dxab, dyab) * hca - cross(dxa, dya, dxca, dyca) * hab > -1e-6 &&
|
|
120
|
+
cross(dxb, dyb, dxbd, dybd) * hab - cross(dxb, dyb, dxab, dyab) * hbd <= 0
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function interpolateNearest(index, width, height, X, Y, V, W) {
|
|
126
|
+
const delaunay = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
127
|
+
// memoization of delaunay.find for the line start (iy) and pixel (ix)
|
|
128
|
+
let iy, ix;
|
|
129
|
+
for (let y = 0.5, k = 0; y < height; ++y) {
|
|
130
|
+
ix = iy;
|
|
131
|
+
for (let x = 0.5; x < width; ++x, ++k) {
|
|
132
|
+
ix = delaunay.find(x, y, ix);
|
|
133
|
+
if (x === 0.5) iy = ix;
|
|
134
|
+
W[k] = V[index[ix]];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return W;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// https://observablehq.com/@observablehq/walk-on-spheres-precision
|
|
141
|
+
export function interpolatorRandomWalk({random = randomLcg(42), minDistance = 0.5, maxSteps = 2} = {}) {
|
|
142
|
+
return (index, width, height, X, Y, V, W) => {
|
|
143
|
+
const delaunay = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
144
|
+
// memoization of delaunay.find for the line start (iy), pixel (ix), and wos step (iw)
|
|
145
|
+
let iy, ix, iw;
|
|
146
|
+
for (let y = 0.5, k = 0; y < height; ++y) {
|
|
147
|
+
ix = iy;
|
|
148
|
+
for (let x = 0.5; x < width; ++x, ++k) {
|
|
149
|
+
let cx = x;
|
|
150
|
+
let cy = y;
|
|
151
|
+
iw = ix = delaunay.find(cx, cy, ix);
|
|
152
|
+
if (x === 0.5) iy = ix;
|
|
153
|
+
let distance; // distance to closest sample
|
|
154
|
+
let step = 0; // count of steps for this walk
|
|
155
|
+
while ((distance = Math.hypot(X[index[iw]] - cx, Y[index[iw]] - cy)) > minDistance && step < maxSteps) {
|
|
156
|
+
const angle = random(x, y, step) * 2 * Math.PI;
|
|
157
|
+
cx += Math.cos(angle) * distance;
|
|
158
|
+
cy += Math.sin(angle) * distance;
|
|
159
|
+
iw = delaunay.find(cx, cy, iw);
|
|
160
|
+
++step;
|
|
161
|
+
}
|
|
162
|
+
W[k] = V[index[iw]];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return W;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function blend(a, ca, b, cb, c, cc) {
|
|
170
|
+
return ca * a + cb * b + cc * c;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pick(random) {
|
|
174
|
+
return (a, ca, b, cb, c, cc, x, y) => {
|
|
175
|
+
const u = random(x, y);
|
|
176
|
+
return u < ca ? a : u < ca + cb ? b : c;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function mixer(F, random) {
|
|
181
|
+
return isNumeric(F) || isTemporal(F) ? blend : pick(random);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isNumeric(values) {
|
|
185
|
+
for (const value of values) {
|
|
186
|
+
if (value == null) continue;
|
|
187
|
+
return typeof value === "number";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isTemporal(values) {
|
|
192
|
+
for (const value of values) {
|
|
193
|
+
if (value == null) continue;
|
|
194
|
+
return value instanceof Date;
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/plot.js
CHANGED
|
@@ -39,9 +39,14 @@ export class Plot {
|
|
|
39
39
|
return this.getAttribute('width') - left - right;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
innerHeight() {
|
|
42
|
+
innerHeight(defaultValue = 400) {
|
|
43
43
|
const { top, bottom } = this.margins();
|
|
44
|
-
|
|
44
|
+
let h = this.getAttribute('height');
|
|
45
|
+
if (h == null && defaultValue != null) {
|
|
46
|
+
h = defaultValue; // TODO could apply more nuanced logic here
|
|
47
|
+
this.setAttribute('height', h, { silent: true });
|
|
48
|
+
}
|
|
49
|
+
return h - top - bottom;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
pending(mark) {
|