@uwdata/mosaic-plot 0.7.1 → 0.8.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.
@@ -14,7 +14,6 @@ export const attributeMap = new Map([
14
14
  ['grid', 'grid'],
15
15
  ['label', 'label'],
16
16
  ['padding', 'padding'],
17
- ['round', 'round'],
18
17
  ['xScale', 'x.type'],
19
18
  ['xDomain', 'x.domain'],
20
19
  ['xRange', 'x.range'],
@@ -39,10 +38,12 @@ export const attributeMap = new Map([
39
38
  ['xLine', 'x.line'],
40
39
  ['xLabel', 'x.label'],
41
40
  ['xLabelAnchor', 'x.labelAnchor'],
41
+ ['xLabelArrow', 'x.labelArrow'],
42
42
  ['xLabelOffset', 'x.labelOffset'],
43
43
  ['xFontVariant', 'x.fontVariant'],
44
44
  ['xAriaLabel', 'x.ariaLabel'],
45
45
  ['xAriaDescription', 'x.ariaDescription'],
46
+ ['xPercent', 'x.percent'],
46
47
  ['xReverse', 'x.reverse'],
47
48
  ['xZero', 'x.zero'],
48
49
  ['xBase', 'x.base'],
@@ -72,10 +73,12 @@ export const attributeMap = new Map([
72
73
  ['yLine', 'y.line'],
73
74
  ['yLabel', 'y.label'],
74
75
  ['yLabelAnchor', 'y.labelAnchor'],
76
+ ['yLabelArrow', 'y.labelArrow'],
75
77
  ['yLabelOffset', 'y.labelOffset'],
76
78
  ['yFontVariant', 'y.fontVariant'],
77
79
  ['yAriaLabel', 'y.ariaLabel'],
78
80
  ['yAriaDescription', 'y.ariaDescription'],
81
+ ['yPercent', 'y.percent'],
79
82
  ['yReverse', 'y.reverse'],
80
83
  ['yZero', 'y.zero'],
81
84
  ['yBase', 'y.base'],
@@ -90,7 +93,6 @@ export const attributeMap = new Map([
90
93
  ['facetLabel', 'facet.label'],
91
94
  ['fxDomain', 'fx.domain'],
92
95
  ['fxRange', 'fx.range'],
93
- ['fxNice', 'fx.nice'],
94
96
  ['fxInset', 'fx.inset'],
95
97
  ['fxInsetLeft', 'fx.insetLeft'],
96
98
  ['fxInsetRight', 'fx.insetRight'],
@@ -117,7 +119,6 @@ export const attributeMap = new Map([
117
119
  ['fxReverse', 'fx.reverse'],
118
120
  ['fyDomain', 'fy.domain'],
119
121
  ['fyRange', 'fy.range'],
120
- ['fyNice', 'fy.mice'],
121
122
  ['fyInset', 'fy,inset'],
122
123
  ['fyInsetTop', 'fy.insetTop'],
123
124
  ['fyInsetBottom', 'fy.insetBottom'],
@@ -153,6 +154,7 @@ export const attributeMap = new Map([
153
154
  ['colorPivot', 'color.pivot'],
154
155
  ['colorSymmetric', 'color.symmetric'],
155
156
  ['colorLabel', 'color.label'],
157
+ ['colorPercent', 'color.percent'],
156
158
  ['colorReverse', 'color.reverse'],
157
159
  ['colorZero', 'color.zero'],
158
160
  ['colorTickFormat', 'color.tickFormat'],
@@ -165,17 +167,22 @@ export const attributeMap = new Map([
165
167
  ['opacityClamp', 'opacity.clamp'],
166
168
  ['opacityNice', 'opacity.nice'],
167
169
  ['opacityLabel', 'opacity.label'],
170
+ ['opacityPercent', 'opacity.percent'],
168
171
  ['opacityReverse', 'opacity.reverse'],
169
172
  ['opacityZero', 'opacity.zero'],
170
173
  ['opacityTickFormat', 'opacity.tickFormat'],
171
174
  ['opacityBase', 'opacity.base'],
172
175
  ['opacityExponent', 'opacity.exponent'],
173
176
  ['opacityConstant', 'opacity.constant'],
177
+ ['symbolScale', 'symbol.type'],
178
+ ['symbolDomain', 'symbol.domain'],
179
+ ['symbolRange', 'symbol.range'],
174
180
  ['rScale', 'r.type'],
175
181
  ['rDomain', 'r.domain'],
176
182
  ['rRange', 'r.range'],
177
183
  ['rClamp', 'r.clamp'],
178
184
  ['rNice', 'r.nice'],
185
+ ['rPercent', 'r.percent'],
179
186
  ['rZero', 'r.zero'],
180
187
  ['rBase', 'r.base'],
181
188
  ['rExponent', 'r.exponent'],
@@ -185,6 +192,7 @@ export const attributeMap = new Map([
185
192
  ['lengthRange', 'length.range'],
186
193
  ['lengthClamp', 'length.clamp'],
187
194
  ['lengthNice', 'length.nice'],
195
+ ['lengthPercent', 'length.percent'],
188
196
  ['lengthZero', 'length.zero'],
189
197
  ['lengthBase', 'length.base'],
190
198
  ['lengthExponent', 'length.exponent'],
@@ -1,6 +1,7 @@
1
1
  import * as Plot from '@observablehq/plot';
2
2
  import { setAttributes } from './plot-attributes.js';
3
3
  import { Fixed } from './symbols.js';
4
+ import { isArrowTable } from '@uwdata/mosaic-core';
4
5
 
5
6
  const OPTIONS_ONLY_MARKS = new Set([
6
7
  'frame',
@@ -25,6 +26,22 @@ export async function plotRenderer(plot) {
25
26
  for (const { type, data, options } of mark.plotSpecs()) {
26
27
  if (OPTIONS_ONLY_MARKS.has(type)) {
27
28
  spec.marks.push(Plot[type](options));
29
+ } else if (isArrowTable(data)) {
30
+ // optimized calls to Plot for Arrow:
31
+ // https://github.com/observablehq/plot/issues/191#issuecomment-2010986851
32
+ const opts = Object.fromEntries(
33
+ Object.entries(options).map(([k, v]) => {
34
+ let val = v;
35
+ if (typeof v === 'string') {
36
+ val = data.getChild(v) ?? v;
37
+ } else if (typeof v === 'object') {
38
+ const value = data.getChild(v.value);
39
+ val = value ? {value} : v;
40
+ }
41
+ return [k, val]
42
+ })
43
+ );
44
+ spec.marks.push(Plot[type]({length: data.numRows}, opts));
28
45
  } else {
29
46
  spec.marks.push(Plot[type](data, options));
30
47
  }
@@ -148,10 +165,13 @@ function annotateMarks(svg, indices) {
148
165
  }
149
166
 
150
167
  function getType(data, channel) {
151
- for (const row of data) {
152
- const v = row[channel] ?? row[channel+'1'] ?? row[channel+'2'];
153
- if (v != null) {
154
- return v instanceof Date ? 'date' : typeof v;
168
+ const { columns } = data;
169
+ const col = columns[channel] ?? columns[channel+'1'] ?? columns[channel+'2'];
170
+ if (col) {
171
+ for (const v of col) {
172
+ if (v != null) {
173
+ return v instanceof Date ? 'date' : typeof v;
174
+ }
155
175
  }
156
176
  }
157
177
  }
package/src/plot.js CHANGED
@@ -72,10 +72,20 @@ export class Plot {
72
72
  this.synch.resolve();
73
73
  }
74
74
 
75
+ /**
76
+ * @param {string} name The attribute to return.
77
+ * @returns {*} The value of the attribute.
78
+ */
75
79
  getAttribute(name) {
76
80
  return this.attributes[name];
77
81
  }
78
82
 
83
+ /**
84
+ * @param {string} name The name of the attribute to set.
85
+ * @param {*} value The value to set.
86
+ * @param {{silent: boolean}} [options] Options for setting the attribute.
87
+ * @returns {boolean} whether the value changed.
88
+ */
79
89
  setAttribute(name, value, options) {
80
90
  if (distinct(this.attributes[name], value)) {
81
91
  if (value === undefined) {
@@ -91,6 +101,11 @@ export class Plot {
91
101
  return false;
92
102
  }
93
103
 
104
+ /**
105
+ * @param {string} name The attribute name.
106
+ * @param {*} callback The function to call when the attribute changes.
107
+ * @returns {this}
108
+ */
94
109
  addAttributeListener(name, callback) {
95
110
  const map = this.listeners || (this.listeners = new Map);
96
111
  if (!map.has(name)) map.set(name, new Set);
@@ -98,6 +113,11 @@ export class Plot {
98
113
  return this;
99
114
  }
100
115
 
116
+ /**
117
+ * @param {string} name The attribute name.
118
+ * @param {*} callback The function to call when the attribute changes.
119
+ * @returns {void}
120
+ */
101
121
  removeAttributeListener(name, callback) {
102
122
  return this.listeners?.get(name)?.delete(callback);
103
123
  }
@@ -1,205 +0,0 @@
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,50 +0,0 @@
1
- import { convertArrowValue, isArrowTable } from '@uwdata/mosaic-core';
2
-
3
- export function toDataArray(data) {
4
- return isArrowTable(data) ? arrowToObjects(data) : data;
5
- }
6
-
7
- /**
8
- * Convert Apache Arrow tables to an array of vanilla JS objects.
9
- * The internal conversions performed by the Arrow JS lib may not
10
- * always produce values properly interpreted by Observable Plot.
11
- * In addition, the Arrow JS lib uses Proxy objects to create
12
- * row (tuple) objects, which can introduce some perf overhead.
13
- * This method provides custom conversions of data that we can hand
14
- * to Observable Plot. Internally, Plot will copy input data values
15
- * into an array-based columnar organizations. If in the future Plot
16
- * provides an efficient path to directly pass in columnar-data, we
17
- * can revisit this method.
18
- */
19
- export function arrowToObjects(data) {
20
- const { batches, numRows: length } = data;
21
-
22
- // return an empty array for empty tables
23
- if (!length) return [];
24
-
25
- // pre-allocate output objects
26
- const objects = Array.from({ length }, () => ({}));
27
-
28
- // for each row batch...
29
- for (let k = 0, b = 0; b < batches.length; ++b) {
30
- const batch = batches[b];
31
- const { schema, numRows, numCols } = batch;
32
-
33
- // for each column...
34
- for (let j = 0; j < numCols; ++j) {
35
- const child = batch.getChildAt(j);
36
- const { name, type } = schema.fields[j];
37
- const valueOf = convertArrowValue(type);
38
-
39
- // for each row in the current batch...
40
- for (let o = k, i = 0; i < numRows; ++i, ++o) {
41
- // extract/convert value from arrow, copy to output object
42
- objects[o][name] = valueOf(child.get(i));
43
- }
44
- }
45
-
46
- k += numRows;
47
- }
48
-
49
- return objects;
50
- }