@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.
@@ -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 { convertArrow, isArrowTable } from './arrow.js';
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 = convertArrow(type);
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) {
@@ -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, ['x', 'x1', 'x2']);
80
- inferLabel('y', spec, marks, ['y', 'y1', 'y2']);
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, channels = [key]) {
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(channels)?.field);
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, channels) || 'number';
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, channels) {
150
+ function getType(data, channel) {
153
151
  for (const row of data) {
154
- for (let j = 0; j < channels.length; ++j) {
155
- const v = row[channels[j]];
156
- if (v != null) {
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
- return this.getAttribute('height') - top - bottom;
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) {
@@ -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.stats[column];
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})`;
@@ -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
- }