@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.
- package/dist/mosaic-plot.js +2723 -4098
- package/dist/mosaic-plot.min.js +14 -14
- package/package.json +5 -5
- package/src/interactors/Interval1D.js +6 -4
- package/src/interactors/Interval2D.js +3 -1
- package/src/interactors/Nearest.js +11 -10
- package/src/interactors/PanZoom.js +3 -2
- package/src/interactors/Toggle.js +12 -11
- package/src/interactors/util/patchScreenCTM.js +2 -0
- package/src/legend.js +135 -27
- package/src/marks/ConnectedMark.js +6 -0
- package/src/marks/ContourMark.js +36 -16
- package/src/marks/DenseLineMark.js +9 -5
- package/src/marks/Density1DMark.js +22 -13
- package/src/marks/Density2DMark.js +33 -18
- package/src/marks/GeoMark.js +7 -8
- package/src/marks/Grid2DMark.js +58 -28
- package/src/marks/HexbinMark.js +10 -2
- package/src/marks/Mark.js +43 -11
- package/src/marks/RasterMark.js +55 -23
- package/src/marks/RasterTileMark.js +33 -20
- package/src/marks/RegressionMark.js +69 -34
- package/src/marks/util/grid.js +94 -86
- package/src/marks/util/handle-param.js +10 -11
- package/src/marks/util/stats.js +33 -1
- package/src/marks/util/to-data-columns.js +71 -0
- package/src/plot-attributes.js +11 -3
- package/src/plot-renderer.js +24 -4
- package/src/plot.js +20 -0
- package/src/marks/util/interpolate.js +0 -205
- package/src/marks/util/to-data-array.js +0 -50
|
@@ -26,46 +26,61 @@ export class Density2DMark extends Grid2DMark {
|
|
|
26
26
|
const deltaY = (y1 - y0) / (ny - pad);
|
|
27
27
|
const offset = pad ? 0 : 0.5;
|
|
28
28
|
this.data = points(
|
|
29
|
-
this.
|
|
29
|
+
this.grids, bins, x0, y0, deltaX, deltaY,
|
|
30
30
|
scaleX.invert, scaleY.invert, offset
|
|
31
31
|
);
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
plotSpecs() {
|
|
36
|
-
const { type, channels, densityMap, data } = this;
|
|
36
|
+
const { type, channels, densityMap, data: { numRows: length, columns } } = this;
|
|
37
37
|
const options = {};
|
|
38
38
|
for (const c of channels) {
|
|
39
39
|
const { channel } = c;
|
|
40
40
|
options[channel] = (channel === 'x' || channel === 'y')
|
|
41
|
-
? channel // use generated x/y data fields
|
|
42
|
-
: channelOption(c);
|
|
41
|
+
? columns[channel] // use generated x/y data fields
|
|
42
|
+
: channelOption(c, columns);
|
|
43
43
|
}
|
|
44
44
|
for (const channel in densityMap) {
|
|
45
45
|
if (densityMap[channel]) {
|
|
46
|
-
options[channel] =
|
|
46
|
+
options[channel] = columns.density;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
return [{ type, data, options }];
|
|
49
|
+
return [{ type, data: { length }, options }];
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function points(
|
|
53
|
+
function points(data, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
|
|
54
54
|
const scale = 1 / (deltaX * deltaY);
|
|
55
55
|
const [nx, ny] = bins;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const batch = nx * ny;
|
|
57
|
+
const numRows = batch * data.numRows;
|
|
58
|
+
|
|
59
|
+
const x = new Float64Array(numRows);
|
|
60
|
+
const y = new Float64Array(numRows);
|
|
61
|
+
const density = new Float64Array(numRows);
|
|
62
|
+
const columns = { x, y, density };
|
|
63
|
+
const { density: grids, ...rest } = data.columns;
|
|
64
|
+
for (const name in rest) {
|
|
65
|
+
columns[name] = new rest[name].constructor(numRows);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let r = 0;
|
|
69
|
+
for (let row = 0; row < data.numRows; ++row) {
|
|
70
|
+
// copy repeated values in batch
|
|
71
|
+
for (const name in rest) {
|
|
72
|
+
columns[name].fill(rest[name][row], r, r + batch);
|
|
73
|
+
}
|
|
74
|
+
// copy individual grid values
|
|
75
|
+
const grid = grids[row];
|
|
59
76
|
for (let k = 0, j = 0; j < ny; ++j) {
|
|
60
|
-
for (let i = 0; i < nx; ++i, ++k) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
y: invertY(y0 + (j + offset) * deltaY),
|
|
65
|
-
density: grid[k] * scale
|
|
66
|
-
});
|
|
77
|
+
for (let i = 0; i < nx; ++i, ++r, ++k) {
|
|
78
|
+
x[r] = invertX(x0 + (i + offset) * deltaX);
|
|
79
|
+
y[r] = invertY(y0 + (j + offset) * deltaY);
|
|
80
|
+
density[r] = grid[k] * scale;
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
|
|
85
|
+
return { numRows, columns };
|
|
71
86
|
}
|
package/src/marks/GeoMark.js
CHANGED
|
@@ -18,16 +18,15 @@ export class GeoMark extends Mark {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
queryResult(data) {
|
|
21
|
-
super.queryResult(data);
|
|
21
|
+
super.queryResult(data); // map to columns, set this.data
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// look for an explicit geometry field
|
|
24
24
|
const geom = this.channelField('geometry')?.as;
|
|
25
|
-
if (geom
|
|
26
|
-
this.data
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
25
|
+
if (geom) {
|
|
26
|
+
const { columns } = this.data;
|
|
27
|
+
if (typeof columns[geom][0] === 'string') {
|
|
28
|
+
columns[geom] = columns[geom].map(s => JSON.parse(s));
|
|
29
|
+
}
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
return this;
|
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk } from '@observablehq/plot';
|
|
1
2
|
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
3
|
import { Transient } from '../symbols.js';
|
|
3
4
|
import { binExpr } from './util/bin-expr.js';
|
|
@@ -5,9 +6,7 @@ import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
|
5
6
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
6
7
|
import { grid2d } from './util/grid.js';
|
|
7
8
|
import { handleParam } from './util/handle-param.js';
|
|
8
|
-
import {
|
|
9
|
-
interpolateNearest, interpolatorBarycentric, interpolatorRandomWalk
|
|
10
|
-
} from './util/interpolate.js';
|
|
9
|
+
import { toDataColumns } from './util/to-data-columns.js';
|
|
11
10
|
import { Mark } from './Mark.js';
|
|
12
11
|
|
|
13
12
|
export const DENSITY = 'density';
|
|
@@ -28,21 +27,47 @@ export class Grid2DMark extends Mark {
|
|
|
28
27
|
super(type, source, channels, xyext);
|
|
29
28
|
this.densityMap = densityMap;
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.bandwidth = handleParam(bandwidth, value => {
|
|
32
|
+
this.bandwidth = value;
|
|
32
33
|
return this.grids ? this.convolve().update() : null;
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
handleParam(
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
/** @type {string} */
|
|
37
|
+
this.interpolate = handleParam(interpolate, value => {
|
|
38
|
+
return (this.interpolate = value, this.requestUpdate());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** @type {number} */
|
|
42
|
+
this.pixelSize = handleParam(pixelSize, value => {
|
|
43
|
+
return (this.pixelSize = value, this.requestUpdate());
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** @type {number} */
|
|
47
|
+
this.pad = handleParam(pad, value => {
|
|
48
|
+
return (this.pad = value, this.requestUpdate());
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** @type {number|undefined} */
|
|
52
|
+
this.width = handleParam(width, value => {
|
|
53
|
+
return (this.width = value, this.requestUpdate());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** @type {number|undefined} */
|
|
57
|
+
this.height = handleParam(height, value => {
|
|
58
|
+
return (this.height = value, this.requestUpdate());
|
|
59
|
+
});
|
|
39
60
|
}
|
|
40
61
|
|
|
62
|
+
/**
|
|
63
|
+
* @param {import('../plot.js').Plot} plot The plot.
|
|
64
|
+
* @param {number} index
|
|
65
|
+
*/
|
|
41
66
|
setPlot(plot, index) {
|
|
42
67
|
const update = () => { if (this.hasFieldInfo()) this.requestUpdate(); };
|
|
43
|
-
plot.addAttributeListener('
|
|
44
|
-
plot.addAttributeListener('
|
|
45
|
-
|
|
68
|
+
plot.addAttributeListener('xDomain', update);
|
|
69
|
+
plot.addAttributeListener('yDomain', update);
|
|
70
|
+
super.setPlot(plot, index);
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
get filterIndexable() {
|
|
@@ -55,7 +80,7 @@ export class Grid2DMark extends Mark {
|
|
|
55
80
|
const { interpolate, pad, channels, densityMap, source } = this;
|
|
56
81
|
const [x0, x1] = this.extentX = extentX(this, filter);
|
|
57
82
|
const [y0, y1] = this.extentY = extentY(this, filter);
|
|
58
|
-
const [nx, ny] = this.bins = this.binDimensions(
|
|
83
|
+
const [nx, ny] = this.bins = this.binDimensions();
|
|
59
84
|
const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
|
|
60
85
|
const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
|
|
61
86
|
|
|
@@ -115,6 +140,9 @@ export class Grid2DMark extends Mark {
|
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @returns {[number, number]} The bin dimensions.
|
|
145
|
+
*/
|
|
118
146
|
binDimensions() {
|
|
119
147
|
const { plot, pixelSize, width, height } = this;
|
|
120
148
|
return [
|
|
@@ -126,47 +154,49 @@ export class Grid2DMark extends Mark {
|
|
|
126
154
|
queryResult(data) {
|
|
127
155
|
const [w, h] = this.bins;
|
|
128
156
|
const interp = maybeInterpolate(this.interpolate);
|
|
129
|
-
|
|
157
|
+
const { columns } = toDataColumns(data);
|
|
158
|
+
this.grids0 = grid2d(w, h, columns.index, columns, this.aggr, this.groupby, interp);
|
|
130
159
|
return this.convolve();
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
convolve() {
|
|
134
|
-
const { aggr, bandwidth, bins,
|
|
163
|
+
const { aggr, bandwidth, bins, grids0, plot } = this;
|
|
135
164
|
|
|
136
165
|
// no smoothing as default fallback
|
|
137
|
-
this.
|
|
166
|
+
this.grids = grids0;
|
|
138
167
|
|
|
139
168
|
if (bandwidth > 0) {
|
|
140
169
|
// determine which grid to smooth
|
|
141
|
-
const
|
|
170
|
+
const prop = aggr.length === 1 ? aggr[0]
|
|
142
171
|
: aggr.includes(DENSITY) ? DENSITY
|
|
143
172
|
: null;
|
|
144
173
|
|
|
145
174
|
// bail if no compatible grid found
|
|
146
|
-
if (!
|
|
175
|
+
if (!prop) {
|
|
147
176
|
console.warn('No compatible grid found for smoothing.');
|
|
148
177
|
return this;
|
|
149
178
|
}
|
|
179
|
+
const g = grids0.columns[prop];
|
|
150
180
|
|
|
151
181
|
// apply smoothing, bandwidth uses units of screen pixels
|
|
152
182
|
const w = plot.innerWidth();
|
|
153
183
|
const h = plot.innerHeight();
|
|
154
184
|
const [nx, ny] = bins;
|
|
155
|
-
const neg =
|
|
185
|
+
const neg = g.some(grid => grid.some(v => v < 0));
|
|
156
186
|
const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
|
|
157
187
|
const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
this.grids = {
|
|
189
|
+
numRows: grids0.numRows,
|
|
190
|
+
columns: {
|
|
191
|
+
...grids0.columns,
|
|
192
|
+
// @ts-ignore
|
|
193
|
+
[prop]: g.map(grid => dericheConv2d(configX, configY, grid, bins))
|
|
194
|
+
}
|
|
195
|
+
};
|
|
162
196
|
}
|
|
163
197
|
|
|
164
198
|
return this;
|
|
165
199
|
}
|
|
166
|
-
|
|
167
|
-
plotSpecs() {
|
|
168
|
-
throw new Error('Unimplemented. Use a Grid2D mark subclass.');
|
|
169
|
-
}
|
|
170
200
|
}
|
|
171
201
|
|
|
172
202
|
/**
|
|
@@ -185,7 +215,7 @@ function createDensityMap(channels) {
|
|
|
185
215
|
|
|
186
216
|
function maybeInterpolate(interpolate = 'none') {
|
|
187
217
|
if (typeof interpolate === 'function') return interpolate;
|
|
188
|
-
switch (
|
|
218
|
+
switch (interpolate.toLowerCase()) {
|
|
189
219
|
case 'none':
|
|
190
220
|
case 'linear':
|
|
191
221
|
return undefined; // no special interpolation need
|
package/src/marks/HexbinMark.js
CHANGED
|
@@ -2,12 +2,17 @@ import { Query, isNotNull, sql } from '@uwdata/mosaic-sql';
|
|
|
2
2
|
import { Transient } from '../symbols.js';
|
|
3
3
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
4
4
|
import { Mark } from './Mark.js';
|
|
5
|
+
import { handleParam } from './util/handle-param.js';
|
|
5
6
|
|
|
6
7
|
export class HexbinMark extends Mark {
|
|
7
8
|
constructor(source, options) {
|
|
8
9
|
const { type = 'hexagon', binWidth = 20, ...channels } = options;
|
|
9
10
|
super(type, source, { r: binWidth / 2, clip: true, ...channels }, xyext);
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
/** @type {number} */
|
|
13
|
+
this.binWidth = handleParam(binWidth, value => {
|
|
14
|
+
return (this.binWidth = value, this.requestUpdate());
|
|
15
|
+
});
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
get filterIndexable() {
|
|
@@ -39,9 +44,10 @@ export class HexbinMark extends Mark {
|
|
|
39
44
|
let x, y;
|
|
40
45
|
const aggr = new Set;
|
|
41
46
|
const cols = {};
|
|
47
|
+
let orderby;
|
|
42
48
|
for (const c of channels) {
|
|
43
49
|
if (c.channel === 'orderby') {
|
|
44
|
-
|
|
50
|
+
orderby = c.value; // TODO revisit once groupby is added
|
|
45
51
|
} else if (c.channel === 'x') {
|
|
46
52
|
x = c;
|
|
47
53
|
} else if (c.channel === 'y') {
|
|
@@ -63,6 +69,8 @@ export class HexbinMark extends Mark {
|
|
|
63
69
|
...cols
|
|
64
70
|
}).groupby('x', 'y');
|
|
65
71
|
|
|
72
|
+
if (orderby) q.orderby(orderby);
|
|
73
|
+
|
|
66
74
|
// Map x/y channels to screen space
|
|
67
75
|
const xx = `${xr} * (${x.field} - ${x1}::DOUBLE)`;
|
|
68
76
|
const yy = `${yr} * (${y2}::DOUBLE - ${y.field})`;
|
package/src/marks/Mark.js
CHANGED
|
@@ -3,10 +3,11 @@ import { Query, Ref, column, isParamLike } from '@uwdata/mosaic-sql';
|
|
|
3
3
|
import { isColor } from './util/is-color.js';
|
|
4
4
|
import { isConstantOption } from './util/is-constant-option.js';
|
|
5
5
|
import { isSymbol } from './util/is-symbol.js';
|
|
6
|
-
import {
|
|
6
|
+
import { toDataColumns } from './util/to-data-columns.js';
|
|
7
7
|
import { Transform } from '../symbols.js';
|
|
8
8
|
|
|
9
9
|
const isColorChannel = channel => channel === 'stroke' || channel === 'fill';
|
|
10
|
+
const isOpacityChannel = channel => /opacity$/i.test(channel);
|
|
10
11
|
const isSymbolChannel = channel => channel === 'symbol';
|
|
11
12
|
const isFieldObject = (channel, field) => {
|
|
12
13
|
return channel !== 'sort' && channel !== 'tip'
|
|
@@ -31,7 +32,7 @@ export class Mark extends MosaicClient {
|
|
|
31
32
|
|
|
32
33
|
this.source = source;
|
|
33
34
|
if (isDataArray(this.source)) {
|
|
34
|
-
this.data = this.source;
|
|
35
|
+
this.data = toDataColumns(this.source);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const channels = this.channels = [];
|
|
@@ -63,12 +64,15 @@ export class Mark extends MosaicClient {
|
|
|
63
64
|
}
|
|
64
65
|
} else if (isParamLike(entry)) {
|
|
65
66
|
if (Array.isArray(entry.columns)) {
|
|
67
|
+
// we currently duck-type to having a columns array
|
|
68
|
+
// as a check that this is SQLExpression-compatible
|
|
66
69
|
channels.push(fieldEntry(channel, entry));
|
|
67
70
|
params.add(entry);
|
|
68
71
|
} else {
|
|
69
72
|
const c = valueEntry(channel, entry.value);
|
|
70
73
|
channels.push(c);
|
|
71
74
|
entry.addEventListener('value', value => {
|
|
75
|
+
// update immediately, the value is simply passed to Plot
|
|
72
76
|
c.value = value;
|
|
73
77
|
return this.update();
|
|
74
78
|
});
|
|
@@ -85,6 +89,10 @@ export class Mark extends MosaicClient {
|
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @param {import('../plot.js').Plot} plot The plot.
|
|
94
|
+
* @param {number} index
|
|
95
|
+
*/
|
|
88
96
|
setPlot(plot, index) {
|
|
89
97
|
this.plot = plot;
|
|
90
98
|
this.index = index;
|
|
@@ -104,7 +112,7 @@ export class Mark extends MosaicClient {
|
|
|
104
112
|
return this.channels.find(c => c.channel === channel);
|
|
105
113
|
}
|
|
106
114
|
|
|
107
|
-
channelField(channel, { exact } = {}) {
|
|
115
|
+
channelField(channel, { exact = false } = {}) {
|
|
108
116
|
const c = exact
|
|
109
117
|
? this.channel(channel)
|
|
110
118
|
: this.channels.find(c => c.channel.startsWith(channel));
|
|
@@ -140,6 +148,11 @@ export class Mark extends MosaicClient {
|
|
|
140
148
|
return this;
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Return a query specifying the data needed by this Mark client.
|
|
153
|
+
* @param {*} [filter] The filtering criteria to apply in the query.
|
|
154
|
+
* @returns {*} The client query
|
|
155
|
+
*/
|
|
143
156
|
query(filter = []) {
|
|
144
157
|
if (this.hasOwnData()) return null;
|
|
145
158
|
const { channels, source: { table } } = this;
|
|
@@ -151,8 +164,11 @@ export class Mark extends MosaicClient {
|
|
|
151
164
|
return this;
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Provide query result data to the mark.
|
|
169
|
+
*/
|
|
154
170
|
queryResult(data) {
|
|
155
|
-
this.data =
|
|
171
|
+
this.data = toDataColumns(data);
|
|
156
172
|
return this;
|
|
157
173
|
}
|
|
158
174
|
|
|
@@ -160,16 +176,29 @@ export class Mark extends MosaicClient {
|
|
|
160
176
|
return this.plot.update(this);
|
|
161
177
|
}
|
|
162
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Generate an array of Plot mark specifications.
|
|
181
|
+
* @returns {object[]}
|
|
182
|
+
*/
|
|
163
183
|
plotSpecs() {
|
|
164
|
-
const { type,
|
|
184
|
+
const { type, detail, channels } = this;
|
|
185
|
+
// @ts-ignore
|
|
186
|
+
const { numRows: length, values, columns } = this.data || {};
|
|
187
|
+
|
|
188
|
+
// populate plot specification options
|
|
165
189
|
const options = {};
|
|
166
190
|
const side = {};
|
|
167
191
|
for (const c of channels) {
|
|
168
192
|
const obj = detail.has(c.channel) ? side : options;
|
|
169
|
-
obj[c.channel] = channelOption(c)
|
|
193
|
+
obj[c.channel] = channelOption(c, columns);
|
|
170
194
|
}
|
|
171
195
|
if (detail.size) options.channels = side;
|
|
172
|
-
|
|
196
|
+
|
|
197
|
+
// if provided raw source values (not objects) pass as-is
|
|
198
|
+
// otherwise we pass columnar data directy in the options
|
|
199
|
+
const data = values ?? (this.data ? { length } : null);
|
|
200
|
+
const spec = [{ type, data, options }];
|
|
201
|
+
return spec;
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
204
|
|
|
@@ -178,14 +207,17 @@ export class Mark extends MosaicClient {
|
|
|
178
207
|
* Checks if a constant value or a data field is needed.
|
|
179
208
|
* Also avoids misinterpretation of data values as color names.
|
|
180
209
|
* @param {*} c a visual encoding channel spec
|
|
210
|
+
* @param {object} columns named data column arrays
|
|
181
211
|
* @returns the Plot channel option
|
|
182
212
|
*/
|
|
183
|
-
export function channelOption(c) {
|
|
213
|
+
export function channelOption(c, columns) {
|
|
184
214
|
// use a scale override for color channels to sidestep
|
|
185
215
|
// https://github.com/observablehq/plot/issues/1593
|
|
216
|
+
const value = columns?.[c.as] ?? c.as;
|
|
186
217
|
return Object.hasOwn(c, 'value') ? c.value
|
|
187
|
-
: isColorChannel(c.channel) ? { value
|
|
188
|
-
: c.
|
|
218
|
+
: isColorChannel(c.channel) ? { value, scale: 'color' }
|
|
219
|
+
: isOpacityChannel(c.channel) ? { value, scale: 'opacity' }
|
|
220
|
+
: value;
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
/**
|
|
@@ -196,7 +228,7 @@ export function channelOption(c) {
|
|
|
196
228
|
* @param {*} table the table to query.
|
|
197
229
|
* @param {*} skip an optional array of channels to skip.
|
|
198
230
|
* Mark subclasses can skip channels that require special handling.
|
|
199
|
-
* @returns a Query instance
|
|
231
|
+
* @returns {Query} a Query instance
|
|
200
232
|
*/
|
|
201
233
|
export function markQuery(channels, table, skip = []) {
|
|
202
234
|
const q = Query.from({ source: table });
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -20,6 +20,7 @@ import { Fixed, Transient } from '../symbols.js';
|
|
|
20
20
|
export class RasterMark extends Grid2DMark {
|
|
21
21
|
constructor(source, options) {
|
|
22
22
|
super('image', source, options);
|
|
23
|
+
this.image = null;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
setPlot(plot, index) {
|
|
@@ -33,37 +34,45 @@ export class RasterMark extends Grid2DMark {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
rasterize() {
|
|
36
|
-
const { bins,
|
|
37
|
+
const { bins, grids } = this;
|
|
37
38
|
const [ w, h ] = bins;
|
|
39
|
+
const { numRows, columns } = grids;
|
|
38
40
|
|
|
39
41
|
// raster data
|
|
40
42
|
const { canvas, ctx, img } = imageData(this, w, h);
|
|
41
43
|
|
|
42
44
|
// color + opacity encodings
|
|
43
45
|
const { alpha, alphaProp, color, colorProp } = rasterEncoding(this);
|
|
46
|
+
const alphaData = columns[alphaProp] ?? [];
|
|
47
|
+
const colorData = columns[colorProp] ?? [];
|
|
44
48
|
|
|
45
49
|
// generate rasters
|
|
46
|
-
this.data =
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
this.data = {
|
|
51
|
+
numRows,
|
|
52
|
+
columns: {
|
|
53
|
+
src: Array.from({ length: numRows }, (_, i) => {
|
|
54
|
+
color?.(img.data, w, h, colorData[i]);
|
|
55
|
+
alpha?.(img.data, w, h, alphaData[i]);
|
|
56
|
+
ctx.putImageData(img, 0, 0);
|
|
57
|
+
return canvas.toDataURL();
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
};
|
|
52
61
|
|
|
53
62
|
return this;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
plotSpecs() {
|
|
57
|
-
const { type, plot, data } = this;
|
|
66
|
+
const { type, plot, data: { numRows: length, columns } } = this;
|
|
58
67
|
const options = {
|
|
59
|
-
src:
|
|
68
|
+
src: columns.src,
|
|
60
69
|
width: plot.innerWidth(),
|
|
61
70
|
height: plot.innerHeight(),
|
|
62
71
|
preserveAspectRatio: 'none',
|
|
63
72
|
imageRendering: this.channel('imageRendering')?.value,
|
|
64
73
|
frameAnchor: 'middle'
|
|
65
74
|
};
|
|
66
|
-
return [{ type, data, options }];
|
|
75
|
+
return [{ type, data: { length }, options }];
|
|
67
76
|
}
|
|
68
77
|
}
|
|
69
78
|
|
|
@@ -86,6 +95,7 @@ export class HeatmapMark extends RasterMark {
|
|
|
86
95
|
/**
|
|
87
96
|
* Utility method to generate color and alpha encoding helpers.
|
|
88
97
|
* The returned methods can write directly to a pixel raster.
|
|
98
|
+
* @param {RasterMark} mark
|
|
89
99
|
*/
|
|
90
100
|
export function rasterEncoding(mark) {
|
|
91
101
|
const { aggr, densityMap, groupby, plot } = mark;
|
|
@@ -132,18 +142,24 @@ export function rasterEncoding(mark) {
|
|
|
132
142
|
return { alphaProp, colorProp, alpha, color };
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Generate an opacity rasterizer for a bitmap alpha channel.
|
|
147
|
+
* @param {RasterMark} mark The mark instance
|
|
148
|
+
* @param {string} prop The data property name
|
|
149
|
+
* @returns A bitmap rasterizer function.
|
|
150
|
+
*/
|
|
135
151
|
function alphaScale(mark, prop) {
|
|
136
|
-
const { plot,
|
|
152
|
+
const { plot, grids } = mark;
|
|
137
153
|
|
|
138
154
|
// determine scale domain
|
|
139
155
|
const domainAttr = plot.getAttribute('opacityDomain');
|
|
140
156
|
const domainFixed = domainAttr === Fixed;
|
|
141
157
|
const domainTransient = domainAttr?.[Transient];
|
|
142
158
|
const domain = (!domainFixed && !domainTransient && domainAttr)
|
|
143
|
-
|| gridDomainContinuous(grids
|
|
144
|
-
if (domainFixed || domainTransient) {
|
|
145
|
-
if (
|
|
146
|
-
plot.setAttribute('
|
|
159
|
+
|| gridDomainContinuous(grids.columns[prop]);
|
|
160
|
+
if (domainFixed || domainTransient || !domainAttr) {
|
|
161
|
+
if (!domainFixed) domain[Transient] = true;
|
|
162
|
+
plot.setAttribute('opacityDomain', domain);
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
// generate opacity scale
|
|
@@ -163,22 +179,29 @@ function alphaScale(mark, prop) {
|
|
|
163
179
|
return alphaScheme(s);
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Generate an color rasterizer for bitmap r, g, b channels.
|
|
184
|
+
* @param {RasterMark} mark The mark instance
|
|
185
|
+
* @param {string} prop
|
|
186
|
+
* @returns A bitmap rasterizer function.
|
|
187
|
+
*/
|
|
166
188
|
function colorScale(mark, prop) {
|
|
167
|
-
const { plot,
|
|
168
|
-
const
|
|
169
|
-
const
|
|
189
|
+
const { plot, grids } = mark;
|
|
190
|
+
const data = grids.columns[prop];
|
|
191
|
+
const flat = !data[0]?.map; // not array-like
|
|
192
|
+
const discrete = flat || Array.isArray(data[0]);
|
|
170
193
|
|
|
171
194
|
// determine scale domain
|
|
172
195
|
const domainAttr = plot.getAttribute('colorDomain');
|
|
173
196
|
const domainFixed = domainAttr === Fixed;
|
|
174
197
|
const domainTransient = domainAttr?.[Transient];
|
|
175
198
|
const domain = (!domainFixed && !domainTransient && domainAttr) || (
|
|
176
|
-
flat ?
|
|
177
|
-
: discrete ? gridDomainDiscrete(
|
|
178
|
-
: gridDomainContinuous(
|
|
199
|
+
flat ? data.sort(ascending)
|
|
200
|
+
: discrete ? gridDomainDiscrete(data)
|
|
201
|
+
: gridDomainContinuous(data)
|
|
179
202
|
);
|
|
180
|
-
if (domainFixed || domainTransient) {
|
|
181
|
-
if (
|
|
203
|
+
if (domainFixed || domainTransient || !domainAttr) {
|
|
204
|
+
if (!domainFixed) domain[Transient] = true;
|
|
182
205
|
plot.setAttribute('colorDomain', domain);
|
|
183
206
|
}
|
|
184
207
|
|
|
@@ -234,6 +257,15 @@ function inferScaleType(type) {
|
|
|
234
257
|
return type;
|
|
235
258
|
}
|
|
236
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Retrieve canvas image data for a 2D raster bitmap.
|
|
262
|
+
* The resulting data is cached in the mark.image property.
|
|
263
|
+
* If the canvas dimensions change, a new canvas is created.
|
|
264
|
+
* @param {RasterMark} mark The mark instance
|
|
265
|
+
* @param {number} w The canvas width.
|
|
266
|
+
* @param {number} h The canvas height.
|
|
267
|
+
* @returns An object with a canvas, context, image data, and dimensions.
|
|
268
|
+
*/
|
|
237
269
|
export function imageData(mark, w, h) {
|
|
238
270
|
if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
|
|
239
271
|
const canvas = createCanvas(w, h);
|