@uwdata/mosaic-plot 0.6.0 → 0.7.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 +19 -0
- package/dist/mosaic-plot.js +6206 -5928
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +4 -4
- package/src/interactors/Interval1D.js +1 -1
- package/src/interactors/Interval2D.js +2 -2
- package/src/interactors/PanZoom.js +2 -2
- package/src/interactors/util/get-field.js +2 -2
- package/src/legend.js +4 -2
- package/src/marks/ConnectedMark.js +5 -8
- package/src/marks/DenseLineMark.js +11 -4
- package/src/marks/Grid2DMark.js +23 -3
- package/src/marks/Mark.js +45 -24
- package/src/marks/RasterMark.js +1 -1
- package/src/marks/RasterTileMark.js +1 -1
- package/src/marks/util/channel-scale.js +1 -2
- package/src/marks/util/density.js +26 -7
- package/src/marks/util/extent.js +3 -5
- package/src/marks/util/grid.js +50 -58
- package/src/marks/util/interpolate.js +205 -0
- package/src/marks/util/is-color.js +3 -0
- package/src/marks/util/to-data-array.js +2 -2
- package/src/plot-renderer.js +9 -13
- package/src/plot.js +7 -2
- package/src/transforms/bin.js +2 -2
- package/src/marks/util/arrow.js +0 -25
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Delaunay, randomLcg } from 'd3';
|
|
2
|
+
|
|
3
|
+
// Derived from Observable Plot’s interpolatorBarycentric:
|
|
4
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L283-L334
|
|
5
|
+
|
|
6
|
+
export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
|
|
7
|
+
return (index, width, height, X, Y, V, W) => {
|
|
8
|
+
// Interpolate the interior of all triangles with barycentric coordinates
|
|
9
|
+
const {points, triangles, hull} = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
10
|
+
const S = new Uint8Array(width * height); // 1 if pixel has been seen.
|
|
11
|
+
const mix = mixer(V, random);
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < triangles.length; i += 3) {
|
|
14
|
+
const ta = triangles[i];
|
|
15
|
+
const tb = triangles[i + 1];
|
|
16
|
+
const tc = triangles[i + 2];
|
|
17
|
+
const Ax = points[2 * ta];
|
|
18
|
+
const Bx = points[2 * tb];
|
|
19
|
+
const Cx = points[2 * tc];
|
|
20
|
+
const Ay = points[2 * ta + 1];
|
|
21
|
+
const By = points[2 * tb + 1];
|
|
22
|
+
const Cy = points[2 * tc + 1];
|
|
23
|
+
const x1 = Math.min(Ax, Bx, Cx);
|
|
24
|
+
const x2 = Math.max(Ax, Bx, Cx);
|
|
25
|
+
const y1 = Math.min(Ay, By, Cy);
|
|
26
|
+
const y2 = Math.max(Ay, By, Cy);
|
|
27
|
+
const z = (By - Cy) * (Ax - Cx) + (Ay - Cy) * (Cx - Bx);
|
|
28
|
+
if (!z) continue;
|
|
29
|
+
const va = V[index[ta]];
|
|
30
|
+
const vb = V[index[tb]];
|
|
31
|
+
const vc = V[index[tc]];
|
|
32
|
+
for (let x = Math.floor(x1); x < x2; ++x) {
|
|
33
|
+
for (let y = Math.floor(y1); y < y2; ++y) {
|
|
34
|
+
if (x < 0 || x >= width || y < 0 || y >= height) continue;
|
|
35
|
+
const xp = x + 0.5; // sample pixel centroids
|
|
36
|
+
const yp = y + 0.5;
|
|
37
|
+
const ga = ((By - Cy) * (xp - Cx) + (yp - Cy) * (Cx - Bx)) / z;
|
|
38
|
+
if (ga < 0) continue;
|
|
39
|
+
const gb = ((Cy - Ay) * (xp - Cx) + (yp - Cy) * (Ax - Cx)) / z;
|
|
40
|
+
if (gb < 0) continue;
|
|
41
|
+
const gc = 1 - ga - gb;
|
|
42
|
+
if (gc < 0) continue;
|
|
43
|
+
const i = x + width * y;
|
|
44
|
+
W[i] = mix(va, ga, vb, gb, vc, gc, x, y);
|
|
45
|
+
S[i] = 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix);
|
|
50
|
+
return W;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extrapolate by finding the closest point on the hull.
|
|
55
|
+
function extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix) {
|
|
56
|
+
X = Float64Array.from(hull, (i) => X[index[i]]);
|
|
57
|
+
Y = Float64Array.from(hull, (i) => Y[index[i]]);
|
|
58
|
+
V = Array.from(hull, (i) => V[index[i]]);
|
|
59
|
+
const n = X.length;
|
|
60
|
+
const rays = Array.from({length: n}, (_, j) => ray(j, X, Y));
|
|
61
|
+
let k = 0;
|
|
62
|
+
for (let y = 0; y < height; ++y) {
|
|
63
|
+
const yp = y + 0.5;
|
|
64
|
+
for (let x = 0; x < width; ++x) {
|
|
65
|
+
const i = x + width * y;
|
|
66
|
+
if (!S[i]) {
|
|
67
|
+
const xp = x + 0.5;
|
|
68
|
+
for (let l = 0; l < n; ++l) {
|
|
69
|
+
const j = (n + k + (l % 2 ? (l + 1) / 2 : -l / 2)) % n;
|
|
70
|
+
if (rays[j](xp, yp)) {
|
|
71
|
+
const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
|
|
72
|
+
W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
|
|
73
|
+
k = j;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Projects a point p = [x, y] onto the line segment [p1, p2], returning the
|
|
83
|
+
// projected coordinates p’ as t in [0, 1] with p’ = t p1 + (1 - t) p2.
|
|
84
|
+
function segmentProject(x1, y1, x2, y2, x, y) {
|
|
85
|
+
const dx = x2 - x1;
|
|
86
|
+
const dy = y2 - y1;
|
|
87
|
+
const a = dx * (x2 - x) + dy * (y2 - y);
|
|
88
|
+
const b = dx * (x - x1) + dy * (y - y1);
|
|
89
|
+
return a > 0 && b > 0 ? a / (a + b) : +(a > b);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function cross(xa, ya, xb, yb) {
|
|
93
|
+
return xa * yb - xb * ya;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ray(j, X, Y) {
|
|
97
|
+
const n = X.length;
|
|
98
|
+
const xc = X.at(j - 2);
|
|
99
|
+
const yc = Y.at(j - 2);
|
|
100
|
+
const xa = X.at(j - 1);
|
|
101
|
+
const ya = Y.at(j - 1);
|
|
102
|
+
const xb = X[j];
|
|
103
|
+
const yb = Y[j];
|
|
104
|
+
const xd = X.at(j + 1 - n);
|
|
105
|
+
const yd = Y.at(j + 1 - n);
|
|
106
|
+
const dxab = xa - xb;
|
|
107
|
+
const dyab = ya - yb;
|
|
108
|
+
const dxca = xc - xa;
|
|
109
|
+
const dyca = yc - ya;
|
|
110
|
+
const dxbd = xb - xd;
|
|
111
|
+
const dybd = yb - yd;
|
|
112
|
+
const hab = Math.hypot(dxab, dyab);
|
|
113
|
+
const hca = Math.hypot(dxca, dyca);
|
|
114
|
+
const hbd = Math.hypot(dxbd, dybd);
|
|
115
|
+
return (x, y) => {
|
|
116
|
+
const dxa = x - xa;
|
|
117
|
+
const dya = y - ya;
|
|
118
|
+
const dxb = x - xb;
|
|
119
|
+
const dyb = y - yb;
|
|
120
|
+
return (
|
|
121
|
+
cross(dxa, dya, dxb, dyb) > -1e-6 &&
|
|
122
|
+
cross(dxa, dya, dxab, dyab) * hca - cross(dxa, dya, dxca, dyca) * hab > -1e-6 &&
|
|
123
|
+
cross(dxb, dyb, dxbd, dybd) * hab - cross(dxb, dyb, dxab, dyab) * hbd <= 0
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Derived from Observable Plot’s interpolateNearest:
|
|
129
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L410-L428
|
|
130
|
+
|
|
131
|
+
export function interpolateNearest(index, width, height, X, Y, V, W) {
|
|
132
|
+
const delaunay = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
133
|
+
// memoization of delaunay.find for the line start (iy) and pixel (ix)
|
|
134
|
+
let iy, ix;
|
|
135
|
+
for (let y = 0.5, k = 0; y < height; ++y) {
|
|
136
|
+
ix = iy;
|
|
137
|
+
for (let x = 0.5; x < width; ++x, ++k) {
|
|
138
|
+
ix = delaunay.find(x, y, ix);
|
|
139
|
+
if (x === 0.5) iy = ix;
|
|
140
|
+
W[k] = V[index[ix]];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return W;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Derived from Observable Plot’s interpolatorRandomWalk:
|
|
147
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L430-L462
|
|
148
|
+
|
|
149
|
+
// https://observablehq.com/@observablehq/walk-on-spheres-precision
|
|
150
|
+
export function interpolatorRandomWalk({random = randomLcg(42), minDistance = 0.5, maxSteps = 2} = {}) {
|
|
151
|
+
return (index, width, height, X, Y, V, W) => {
|
|
152
|
+
const delaunay = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
153
|
+
// memoization of delaunay.find for the line start (iy), pixel (ix), and wos step (iw)
|
|
154
|
+
let iy, ix, iw;
|
|
155
|
+
for (let y = 0.5, k = 0; y < height; ++y) {
|
|
156
|
+
ix = iy;
|
|
157
|
+
for (let x = 0.5; x < width; ++x, ++k) {
|
|
158
|
+
let cx = x;
|
|
159
|
+
let cy = y;
|
|
160
|
+
iw = ix = delaunay.find(cx, cy, ix);
|
|
161
|
+
if (x === 0.5) iy = ix;
|
|
162
|
+
let distance; // distance to closest sample
|
|
163
|
+
let step = 0; // count of steps for this walk
|
|
164
|
+
while ((distance = Math.hypot(X[index[iw]] - cx, Y[index[iw]] - cy)) > minDistance && step < maxSteps) {
|
|
165
|
+
const angle = random(x, y, step) * 2 * Math.PI;
|
|
166
|
+
cx += Math.cos(angle) * distance;
|
|
167
|
+
cy += Math.sin(angle) * distance;
|
|
168
|
+
iw = delaunay.find(cx, cy, iw);
|
|
169
|
+
++step;
|
|
170
|
+
}
|
|
171
|
+
W[k] = V[index[iw]];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return W;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function blend(a, ca, b, cb, c, cc) {
|
|
179
|
+
return ca * a + cb * b + cc * c;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function pick(random) {
|
|
183
|
+
return (a, ca, b, cb, c, cc, x, y) => {
|
|
184
|
+
const u = random(x, y);
|
|
185
|
+
return u < ca ? a : u < ca + cb ? b : c;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function mixer(F, random) {
|
|
190
|
+
return isNumeric(F) || isTemporal(F) ? blend : pick(random);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isNumeric(values) {
|
|
194
|
+
for (const value of values) {
|
|
195
|
+
if (value == null) continue;
|
|
196
|
+
return typeof value === "number";
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isTemporal(values) {
|
|
201
|
+
for (const value of values) {
|
|
202
|
+
if (value == null) continue;
|
|
203
|
+
return value instanceof Date;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { color } from 'd3';
|
|
2
2
|
|
|
3
|
+
// Derived from Observable Plot’s isColor:
|
|
4
|
+
// https://github.com/observablehq/plot/blob/a063b226fec284c5b0e973701fdbbb244ef9ac2c/src/options.js#L462-L477
|
|
5
|
+
|
|
3
6
|
// Mostly relies on d3-color, with a few extra color keywords. Currently this
|
|
4
7
|
// strictly requires that the value be a string; we might want to apply string
|
|
5
8
|
// coercion here, though note that d3-color instances would need to support
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { convertArrowValue, isArrowTable } from '@uwdata/mosaic-core';
|
|
2
2
|
|
|
3
3
|
export function toDataArray(data) {
|
|
4
4
|
return isArrowTable(data) ? arrowToObjects(data) : data;
|
|
@@ -34,7 +34,7 @@ export function arrowToObjects(data) {
|
|
|
34
34
|
for (let j = 0; j < numCols; ++j) {
|
|
35
35
|
const child = batch.getChildAt(j);
|
|
36
36
|
const { name, type } = schema.fields[j];
|
|
37
|
-
const valueOf =
|
|
37
|
+
const valueOf = convertArrowValue(type);
|
|
38
38
|
|
|
39
39
|
// for each row in the current batch...
|
|
40
40
|
for (let o = k, i = 0; i < numRows; ++i, ++o) {
|
package/src/plot-renderer.js
CHANGED
|
@@ -9,8 +9,6 @@ const OPTIONS_ONLY_MARKS = new Set([
|
|
|
9
9
|
'graticule'
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
12
|
// construct Plot output
|
|
15
13
|
// see https://github.com/observablehq/plot
|
|
16
14
|
export async function plotRenderer(plot) {
|
|
@@ -76,17 +74,17 @@ function setSymbolAttributes(plot, svg, attributes, symbols) {
|
|
|
76
74
|
|
|
77
75
|
function inferLabels(spec, plot) {
|
|
78
76
|
const { marks } = plot;
|
|
79
|
-
inferLabel('x', spec, marks
|
|
80
|
-
inferLabel('y', spec, marks
|
|
77
|
+
inferLabel('x', spec, marks);
|
|
78
|
+
inferLabel('y', spec, marks);
|
|
81
79
|
inferLabel('fx', spec, marks);
|
|
82
80
|
inferLabel('fy', spec, marks);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
function inferLabel(key, spec, marks
|
|
83
|
+
function inferLabel(key, spec, marks) {
|
|
86
84
|
const scale = spec[key] || {};
|
|
87
85
|
if (scale.axis === null || scale.label !== undefined) return; // nothing to do
|
|
88
86
|
|
|
89
|
-
const fields = marks.map(mark => mark.channelField(
|
|
87
|
+
const fields = marks.map(mark => mark.channelField(key)?.field);
|
|
90
88
|
if (fields.every(x => x == null)) return; // no columns found
|
|
91
89
|
|
|
92
90
|
// check for consistent columns / labels
|
|
@@ -100,7 +98,7 @@ function inferLabel(key, spec, marks, channels = [key]) {
|
|
|
100
98
|
} else if (candCol === undefined && candLabel === undefined) {
|
|
101
99
|
candCol = column;
|
|
102
100
|
candLabel = label;
|
|
103
|
-
type = getType(marks[i].data,
|
|
101
|
+
type = getType(marks[i].data, key) || 'number';
|
|
104
102
|
} else if (candLabel !== label) {
|
|
105
103
|
candLabel = undefined;
|
|
106
104
|
} else if (candCol !== column) {
|
|
@@ -149,13 +147,11 @@ function annotateMarks(svg, indices) {
|
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
152
|
-
function getType(data,
|
|
150
|
+
function getType(data, channel) {
|
|
153
151
|
for (const row of data) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return v instanceof Date ? 'date' : typeof v;
|
|
158
|
-
}
|
|
152
|
+
const v = row[channel] ?? row[channel+'1'] ?? row[channel+'2'];
|
|
153
|
+
if (v != null) {
|
|
154
|
+
return v instanceof Date ? 'date' : typeof v;
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
157
|
}
|
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) {
|
package/src/transforms/bin.js
CHANGED
|
@@ -24,12 +24,12 @@ function binField(mark, channel, column, options) {
|
|
|
24
24
|
return {
|
|
25
25
|
column,
|
|
26
26
|
label: column,
|
|
27
|
-
get stats() { return ['min', 'max']; },
|
|
27
|
+
get stats() { return { column, stats: ['min', 'max'] }; },
|
|
28
28
|
get columns() { return [column]; },
|
|
29
29
|
get basis() { return column; },
|
|
30
30
|
toString() {
|
|
31
31
|
const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
|
|
32
|
-
const { min, max } = mark.
|
|
32
|
+
const { min, max } = mark.channelField(channel);
|
|
33
33
|
const b = bins(apply(min), apply(max), options);
|
|
34
34
|
const col = sqlApply(column);
|
|
35
35
|
const base = b.min === 0 ? col : `(${col} - ${b.min})`;
|
package/src/marks/util/arrow.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const INTEGER = 2;
|
|
2
|
-
export const FLOAT = 3;
|
|
3
|
-
export const DECIMAL = 7;
|
|
4
|
-
export const TIMESTAMP = 10;
|
|
5
|
-
|
|
6
|
-
export function isArrowTable(values) {
|
|
7
|
-
return typeof values?.getChild === 'function';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function convertArrow(type) {
|
|
11
|
-
const { typeId } = type;
|
|
12
|
-
|
|
13
|
-
// map timestamp numbers to date objects
|
|
14
|
-
if (typeId === TIMESTAMP) {
|
|
15
|
-
return v => v == null ? v : new Date(v);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// map bignum to number
|
|
19
|
-
if (typeId === INTEGER && type.bitWidth >= 64) {
|
|
20
|
-
return v => v == null ? v : Number(v);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// otherwise use Arrow JS defaults
|
|
24
|
-
return v => v;
|
|
25
|
-
}
|