@uwdata/mosaic-plot 0.5.0 → 0.6.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 +843 -547
- package/dist/mosaic-plot.min.js +14 -14
- package/package.json +4 -4
- package/src/index.js +1 -1
- package/src/marks/ConnectedMark.js +21 -22
- package/src/marks/ContourMark.js +16 -10
- package/src/marks/DenseLineMark.js +8 -13
- package/src/marks/Density1DMark.js +11 -13
- package/src/marks/Density2DMark.js +27 -25
- package/src/marks/Grid2DMark.js +86 -53
- package/src/marks/RasterMark.js +188 -64
- package/src/marks/RasterTileMark.js +54 -100
- package/src/marks/util/arrow.js +25 -0
- package/src/marks/util/bin-expr.js +30 -0
- package/src/marks/util/channel-scale.js +27 -0
- package/src/marks/util/grid.js +90 -19
- package/src/marks/util/raster.js +113 -26
- package/src/marks/util/to-data-array.js +2 -22
- package/src/plot-attributes.js +18 -0
- package/src/transforms/bin.js +18 -20
- package/src/marks/util/bin-field.js +0 -17
- package/src/marks/util/is-arrow-table.js +0 -3
package/src/marks/RasterMark.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
|
+
import { ascending } from 'd3';
|
|
1
2
|
import { scale } from '@observablehq/plot';
|
|
3
|
+
import { gridDomainContinuous, gridDomainDiscrete } from './util/grid.js';
|
|
2
4
|
import { isColor } from './util/is-color.js';
|
|
3
|
-
import {
|
|
4
|
-
import { Grid2DMark } from './Grid2DMark.js';
|
|
5
|
-
|
|
5
|
+
import { alphaScheme, alphaConstant, colorConstant, colorCategory, colorScheme, createCanvas } from './util/raster.js';
|
|
6
|
+
import { DENSITY, Grid2DMark } from './Grid2DMark.js';
|
|
7
|
+
import { Fixed, Transient } from '../symbols.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Raster image mark. Data is binned to a grid based on the x and y options.
|
|
11
|
+
* The grid cells are then colored to form an image.
|
|
12
|
+
* The raster grid size defaults to the pixel width/height of the
|
|
13
|
+
* plot. The pixelSize option (default 1) changes the grid cell to pixel
|
|
14
|
+
* ratio. For example, a pixelSize of 0.5 will create a larger raster
|
|
15
|
+
* for higher resolution images on retina displays. The width and height
|
|
16
|
+
* options set the grid dimensions directly, overriding other options.
|
|
17
|
+
* The raster grid can optionally be smoothed (blurred) by setting
|
|
18
|
+
* the bandwidth option.
|
|
19
|
+
*/
|
|
6
20
|
export class RasterMark extends Grid2DMark {
|
|
7
21
|
constructor(source, options) {
|
|
8
22
|
super('image', source, options);
|
|
@@ -19,23 +33,19 @@ export class RasterMark extends Grid2DMark {
|
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
rasterize() {
|
|
22
|
-
const { bins, kde
|
|
36
|
+
const { bins, kde } = this;
|
|
23
37
|
const [ w, h ] = bins;
|
|
24
38
|
|
|
25
39
|
// raster data
|
|
26
40
|
const { canvas, ctx, img } = imageData(this, w, h);
|
|
27
41
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
// gather color domain as needed
|
|
32
|
-
const idx = groupby.indexOf(this.channelField('fill')?.as);
|
|
33
|
-
const domain = idx < 0 ? [] : kde.map(({ key }) => key[idx]);
|
|
42
|
+
// color + opacity encodings
|
|
43
|
+
const { alpha, alphaProp, color, colorProp } = rasterEncoding(this);
|
|
34
44
|
|
|
35
|
-
// generate
|
|
36
|
-
this.data = kde.map(
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
// generate rasters
|
|
46
|
+
this.data = kde.map(cell => {
|
|
47
|
+
color?.(img.data, w, h, cell[colorProp]);
|
|
48
|
+
alpha?.(img.data, w, h, cell[alphaProp]);
|
|
39
49
|
ctx.putImageData(img, 0, 0);
|
|
40
50
|
return { src: canvas.toDataURL() };
|
|
41
51
|
});
|
|
@@ -57,65 +67,179 @@ export class RasterMark extends Grid2DMark {
|
|
|
57
67
|
}
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Density heatmap image.
|
|
72
|
+
* This is just a raster mark with default options for
|
|
73
|
+
* accurate binning and smoothing for density estimation.
|
|
74
|
+
*/
|
|
75
|
+
export class HeatmapMark extends RasterMark {
|
|
76
|
+
constructor(source, options) {
|
|
77
|
+
super(source, {
|
|
78
|
+
bandwidth: 20,
|
|
79
|
+
interpolate: 'linear',
|
|
80
|
+
pixelSize: 2,
|
|
81
|
+
...options
|
|
82
|
+
});
|
|
66
83
|
}
|
|
67
|
-
return mark.image;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Utility method to generate color and alpha encoding helpers.
|
|
88
|
+
* The returned methods can write directly to a pixel raster.
|
|
89
|
+
*/
|
|
90
|
+
export function rasterEncoding(mark) {
|
|
91
|
+
const { aggr, densityMap, groupby, plot } = mark;
|
|
92
|
+
const hasDensity = aggr.includes(DENSITY);
|
|
93
|
+
const hasFillOpacity = aggr.includes('fillOpacity');
|
|
94
|
+
const fillEntry = mark.channel('fill');
|
|
95
|
+
const opacEntry = mark.channel('fillOpacity');
|
|
96
|
+
|
|
97
|
+
// check constraints, raise errors
|
|
98
|
+
if (aggr.length > 2 || (hasDensity && hasFillOpacity)) {
|
|
99
|
+
throw new Error('Invalid raster encodings. Try dropping an aggregate?');
|
|
100
|
+
}
|
|
101
|
+
if (groupby.includes(opacEntry?.as)) {
|
|
102
|
+
throw new Error('Raster fillOpacity must be an aggregate or constant.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// determine fill encoding channel use
|
|
106
|
+
const fill = densityMap.fill || aggr.includes('fill') ? 'grid'
|
|
107
|
+
: groupby.includes(fillEntry?.as) ? 'group' // groupby
|
|
108
|
+
: isColor(fillEntry?.value) ? fillEntry.value // constant
|
|
109
|
+
: hasDensity && plot.getAttribute('colorScheme') ? 'grid'
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
// determine fill opacity encoding channel use
|
|
113
|
+
const opac = densityMap.fillOpacity || aggr.includes('fillOpacity') ? 'grid'
|
|
114
|
+
: typeof opacEntry?.value === 'number' ? opacEntry.value // constant
|
|
115
|
+
: hasDensity && fill !== 'grid' ? 'grid'
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
if (fill !== 'grid' && opac !== 'grid') {
|
|
119
|
+
// TODO: use a threshold-based encoding?
|
|
120
|
+
throw new Error('Raster mark missing density values.');
|
|
84
121
|
}
|
|
85
122
|
|
|
86
|
-
const
|
|
87
|
-
|
|
123
|
+
const colorProp = fillEntry?.as ?? (fill === 'grid' ? DENSITY : null);
|
|
124
|
+
const alphaProp = opacEntry?.as ?? (opac === 'grid' ? DENSITY : null);
|
|
125
|
+
const color = fill !== 'grid' && fill !== 'group'
|
|
126
|
+
? colorConstant(fill)
|
|
127
|
+
: colorScale(mark, colorProp);
|
|
128
|
+
const alpha = opac !== 'grid'
|
|
129
|
+
? alphaConstant(opac)
|
|
130
|
+
: alphaScale(mark, alphaProp);
|
|
131
|
+
|
|
132
|
+
return { alphaProp, colorProp, alpha, color };
|
|
88
133
|
}
|
|
89
134
|
|
|
90
|
-
function
|
|
91
|
-
const {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
function alphaScale(mark, prop) {
|
|
136
|
+
const { plot, kde: grids } = mark;
|
|
137
|
+
|
|
138
|
+
// determine scale domain
|
|
139
|
+
const domainAttr = plot.getAttribute('opacityDomain');
|
|
140
|
+
const domainFixed = domainAttr === Fixed;
|
|
141
|
+
const domainTransient = domainAttr?.[Transient];
|
|
142
|
+
const domain = (!domainFixed && !domainTransient && domainAttr)
|
|
143
|
+
|| gridDomainContinuous(grids, prop);
|
|
144
|
+
if (domainFixed || domainTransient) {
|
|
145
|
+
if (domainTransient) domain[Transient] = true;
|
|
146
|
+
plot.setAttribute('colorDomain', domain);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// generate opacity scale
|
|
150
|
+
const s = scale({
|
|
151
|
+
opacity: {
|
|
152
|
+
type: plot.getAttribute('opacityScale'),
|
|
153
|
+
domain,
|
|
154
|
+
clamp: plot.getAttribute('opacityClamp'),
|
|
155
|
+
nice: plot.getAttribute('opacityNice'),
|
|
156
|
+
reverse: plot.getAttribute('opacityReverse'),
|
|
157
|
+
zero: plot.getAttribute('opacityZero'),
|
|
158
|
+
base: plot.getAttribute('opacityBase'),
|
|
159
|
+
exponent: plot.getAttribute('opacityExponent'),
|
|
160
|
+
constant: plot.getAttribute('opacityConstant')
|
|
108
161
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
162
|
+
});
|
|
163
|
+
return alphaScheme(s);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function colorScale(mark, prop) {
|
|
167
|
+
const { plot, kde: grids } = mark;
|
|
168
|
+
const flat = !grids[0][prop]?.map; // not array-like
|
|
169
|
+
const discrete = flat || Array.isArray(grids[0][prop]);
|
|
170
|
+
|
|
171
|
+
// determine scale domain
|
|
172
|
+
const domainAttr = plot.getAttribute('colorDomain');
|
|
173
|
+
const domainFixed = domainAttr === Fixed;
|
|
174
|
+
const domainTransient = domainAttr?.[Transient];
|
|
175
|
+
const domain = (!domainFixed && !domainTransient && domainAttr) || (
|
|
176
|
+
flat ? grids.map(cell => cell[prop]).sort(ascending)
|
|
177
|
+
: discrete ? gridDomainDiscrete(grids, prop)
|
|
178
|
+
: gridDomainContinuous(grids, prop)
|
|
179
|
+
);
|
|
180
|
+
if (domainFixed || domainTransient) {
|
|
181
|
+
if (domainTransient) domain[Transient] = true;
|
|
182
|
+
plot.setAttribute('colorDomain', domain);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// generate color scale
|
|
186
|
+
const s = scale({
|
|
187
|
+
color: {
|
|
188
|
+
type: plot.getAttribute('colorScale'),
|
|
113
189
|
domain,
|
|
114
|
-
range,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
190
|
+
range: plot.getAttribute('colorRange'),
|
|
191
|
+
clamp: plot.getAttribute('colorClamp'),
|
|
192
|
+
n: plot.getAttribute('colorN'),
|
|
193
|
+
nice: plot.getAttribute('colorNice'),
|
|
194
|
+
reverse: plot.getAttribute('colorReverse'),
|
|
195
|
+
scheme: plot.getAttribute('colorScheme'),
|
|
196
|
+
interpolate: plot.getAttribute('colorInterpolate'),
|
|
197
|
+
pivot: plot.getAttribute('colorPivot'),
|
|
198
|
+
symmetric: plot.getAttribute('colorSymmetric'),
|
|
199
|
+
zero: plot.getAttribute('colorZero'),
|
|
200
|
+
base: plot.getAttribute('colorBase'),
|
|
201
|
+
exponent: plot.getAttribute('colorExponent'),
|
|
202
|
+
constant: plot.getAttribute('colorConstant')
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// TODO: add support for threshold scales?
|
|
207
|
+
if (discrete) {
|
|
208
|
+
return colorCategory(s);
|
|
209
|
+
} else {
|
|
210
|
+
// Plot scales do not expose intermediate transformation of
|
|
211
|
+
// values to [0, 1] fractions. So we hobble together our own.
|
|
212
|
+
const frac = scale({
|
|
213
|
+
x: {
|
|
214
|
+
type: inferScaleType(s.type),
|
|
215
|
+
domain: s.domain,
|
|
216
|
+
reverse: s.reverse,
|
|
217
|
+
range: [0, 1],
|
|
218
|
+
clamp: s.clamp,
|
|
219
|
+
base: s.base,
|
|
220
|
+
exponent: s.exponent,
|
|
221
|
+
constant: s.constant
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return colorScheme(1024, s, frac.apply);
|
|
118
225
|
}
|
|
226
|
+
}
|
|
119
227
|
|
|
120
|
-
|
|
228
|
+
function inferScaleType(type) {
|
|
229
|
+
if (type.endsWith('symlog')) return 'symlog';
|
|
230
|
+
if (type.endsWith('log')) return 'log';
|
|
231
|
+
if (type.endsWith('pow')) return 'pow';
|
|
232
|
+
if (type.endsWith('sqrt')) return 'sqrt';
|
|
233
|
+
if (type === 'diverging') return 'linear';
|
|
234
|
+
return type;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function imageData(mark, w, h) {
|
|
238
|
+
if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
|
|
239
|
+
const canvas = createCanvas(w, h);
|
|
240
|
+
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
241
|
+
const img = ctx.getImageData(0, 0, w, h);
|
|
242
|
+
mark.image = { canvas, ctx, img, w, h };
|
|
243
|
+
}
|
|
244
|
+
return mark.image;
|
|
121
245
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { coordinator } from '@uwdata/mosaic-core';
|
|
2
2
|
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
|
-
import {
|
|
3
|
+
import { binExpr } from './util/bin-expr.js';
|
|
4
4
|
import { extentX, extentY } from './util/extent.js';
|
|
5
|
-
import {
|
|
6
|
-
import { createCanvas, raster, opacityMap, palette } from './util/raster.js';
|
|
5
|
+
import { createCanvas } from './util/raster.js';
|
|
7
6
|
import { Grid2DMark } from './Grid2DMark.js';
|
|
8
|
-
import {
|
|
7
|
+
import { rasterEncoding } from './RasterMark.js';
|
|
9
8
|
|
|
10
9
|
export class RasterTileMark extends Grid2DMark {
|
|
11
10
|
constructor(source, options) {
|
|
@@ -35,46 +34,66 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
tileQuery(extent) {
|
|
38
|
-
const {
|
|
37
|
+
const { binType, binPad, channels, densityMap, source } = this;
|
|
39
38
|
const [[x0, x1], [y0, y1]] = extent;
|
|
40
39
|
const [nx, ny] = this.bins;
|
|
41
|
-
const bx =
|
|
42
|
-
const by =
|
|
43
|
-
const rx = !!plot.getAttribute('xReverse');
|
|
44
|
-
const ry = !!plot.getAttribute('yReverse');
|
|
45
|
-
const x = bin1d(bx, x0, x1, nx, rx, binPad);
|
|
46
|
-
const y = bin1d(by, y0, y1, ny, ry, binPad);
|
|
40
|
+
const [x, bx] = binExpr(this, 'x', nx, [x0, x1], binPad);
|
|
41
|
+
const [y, by] = binExpr(this, 'y', ny, [y0, y1], binPad);
|
|
47
42
|
|
|
48
43
|
// with padded bins, include the entire domain extent
|
|
49
44
|
// if the bins are flush, exclude the extent max
|
|
50
45
|
const bounds = binPad
|
|
51
|
-
? [isBetween(bx, [x0, x1]), isBetween(by, [y0, y1])]
|
|
52
|
-
: [lte(x0, bx), lt(bx, x1), lte(y0, by), lt(by, y1)];
|
|
46
|
+
? [isBetween(bx, [+x0, +x1]), isBetween(by, [+y0, +y1])]
|
|
47
|
+
: [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
|
|
53
48
|
|
|
54
49
|
const q = Query
|
|
55
50
|
.from(source.table)
|
|
56
51
|
.where(bounds);
|
|
57
52
|
|
|
58
53
|
const groupby = this.groupby = [];
|
|
59
|
-
|
|
54
|
+
const aggrMap = {};
|
|
60
55
|
for (const c of channels) {
|
|
61
56
|
if (Object.hasOwn(c, 'field')) {
|
|
62
|
-
const { channel, field } = c;
|
|
57
|
+
const { as, channel, field } = c;
|
|
63
58
|
if (field.aggregate) {
|
|
64
|
-
|
|
59
|
+
// include custom aggregate
|
|
60
|
+
aggrMap[channel] = field;
|
|
65
61
|
densityMap[channel] = true;
|
|
66
62
|
} else if (channel === 'weight') {
|
|
67
|
-
|
|
63
|
+
// compute weighted density
|
|
64
|
+
aggrMap.density = sum(field);
|
|
68
65
|
} else if (channel !== 'x' && channel !== 'y') {
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// add groupby field
|
|
67
|
+
q.select({ [as]: field });
|
|
68
|
+
groupby.push(as);
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
}
|
|
72
|
+
const aggr = this.aggr = Object.keys(aggrMap);
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
// check for incompatible encodings
|
|
75
|
+
if (aggrMap.density && aggr.length > 1) {
|
|
76
|
+
throw new Error('Weight option can not be used with custom aggregates.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// if no aggregates, default to count density
|
|
80
|
+
if (!aggr.length) {
|
|
81
|
+
aggr.push('density');
|
|
82
|
+
aggrMap.density = count();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// generate grid binning query
|
|
86
|
+
if (binType === 'linear') {
|
|
87
|
+
if (aggr.length > 1) {
|
|
88
|
+
throw new Error('Linear binning not applicable to multiple aggregates.');
|
|
89
|
+
}
|
|
90
|
+
if (!aggrMap.density) {
|
|
91
|
+
throw new Error('Linear binning not applicable to custom aggregates.');
|
|
92
|
+
}
|
|
93
|
+
return binLinear2d(q, x, y, aggrMap.density, nx, groupby);
|
|
94
|
+
} else {
|
|
95
|
+
return bin2d(q, x, y, aggrMap, nx, groupby);
|
|
96
|
+
}
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
async requestTiles() {
|
|
@@ -136,7 +155,7 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
136
155
|
|
|
137
156
|
// wait for tile queries to complete, then update
|
|
138
157
|
const tiles = await Promise.all(queries);
|
|
139
|
-
this.grids = [{
|
|
158
|
+
this.grids = [{ density: processTiles(m, n, xx, yy, coords, tiles) }];
|
|
140
159
|
this.convolve().update();
|
|
141
160
|
}
|
|
142
161
|
|
|
@@ -145,23 +164,19 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
145
164
|
}
|
|
146
165
|
|
|
147
166
|
rasterize() {
|
|
148
|
-
const { bins, kde
|
|
167
|
+
const { bins, kde } = this;
|
|
149
168
|
const [ w, h ] = bins;
|
|
150
169
|
|
|
151
170
|
// raster data
|
|
152
171
|
const { canvas, ctx, img } = imageData(this, w, h);
|
|
153
172
|
|
|
154
|
-
//
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
// gather color domain as needed
|
|
158
|
-
const idx = groupby.indexOf(this.channelField('fill')?.as);
|
|
159
|
-
const domain = idx < 0 ? [] : kde.map(({ key }) => key[idx]);
|
|
173
|
+
// color + opacity encodings
|
|
174
|
+
const { alpha, alphaProp, color, colorProp } = rasterEncoding(this);
|
|
160
175
|
|
|
161
|
-
// generate
|
|
162
|
-
this.data = kde.map(
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
// generate rasters
|
|
177
|
+
this.data = kde.map(cell => {
|
|
178
|
+
color?.(img.data, w, h, cell[colorProp]);
|
|
179
|
+
alpha?.(img.data, w, h, cell[alphaProp]);
|
|
165
180
|
ctx.putImageData(img, 0, 0);
|
|
166
181
|
return { src: canvas.toDataURL() };
|
|
167
182
|
});
|
|
@@ -199,7 +214,7 @@ function copy(m, n, grid, values, tx, ty) {
|
|
|
199
214
|
const num = values.numRows;
|
|
200
215
|
if (num === 0) return;
|
|
201
216
|
const index = values.getChild('index').toArray();
|
|
202
|
-
const value = values.getChild('
|
|
217
|
+
const value = values.getChild('density').toArray();
|
|
203
218
|
for (let row = 0; row < num; ++row) {
|
|
204
219
|
const idx = index[row];
|
|
205
220
|
const i = tx + (idx % m);
|
|
@@ -220,72 +235,11 @@ function imageData(mark, w, h) {
|
|
|
220
235
|
return mark.image;
|
|
221
236
|
}
|
|
222
237
|
|
|
223
|
-
function
|
|
224
|
-
const { densityMap, kde, plot } = mark;
|
|
225
|
-
let domain = densityMap.fill && plot.getAttribute('colorDomain');
|
|
226
|
-
|
|
227
|
-
// compute kde grid extents if no explicit domain
|
|
228
|
-
if (!domain) {
|
|
229
|
-
let lo = 0, hi = 0;
|
|
230
|
-
kde.forEach(grid => {
|
|
231
|
-
for (const v of grid) {
|
|
232
|
-
if (v < lo) lo = v;
|
|
233
|
-
if (v > hi) hi = v;
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
domain = (lo === 0 && hi === 0) ? [0, 1] : [lo, hi];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const type = plot.getAttribute('colorScale');
|
|
240
|
-
return scale({ x: { type, domain, range: [0, 1] } }).apply;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function imagePalette(mark, domain, value, steps = 1024) {
|
|
244
|
-
const { densityMap, plot } = mark;
|
|
245
|
-
const scheme = plot.getAttribute('colorScheme');
|
|
246
|
-
|
|
247
|
-
// initialize color to constant fill, if specified
|
|
248
|
-
const fill = mark.channel('fill');
|
|
249
|
-
let color = isColor(fill?.value) ? fill.value : undefined;
|
|
250
|
-
|
|
251
|
-
if (densityMap.fill || (scheme && !color)) {
|
|
252
|
-
if (scheme) {
|
|
253
|
-
try {
|
|
254
|
-
return palette(
|
|
255
|
-
steps,
|
|
256
|
-
scale({color: { scheme, domain: [0, 1] }}).interpolate
|
|
257
|
-
);
|
|
258
|
-
} catch (err) {
|
|
259
|
-
console.warn(err);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
} else if (domain.length) {
|
|
263
|
-
// fill is based on data values
|
|
264
|
-
const range = plot.getAttribute('colorRange');
|
|
265
|
-
const spec = {
|
|
266
|
-
domain,
|
|
267
|
-
range,
|
|
268
|
-
scheme: scheme || (range ? undefined : 'tableau10')
|
|
269
|
-
};
|
|
270
|
-
color = scale({ color: spec }).apply(value);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return palette(steps, opacityMap(color));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function bin1d(x, x0, x1, n, reverse, pad) {
|
|
277
|
-
const d = (n - pad) / (x1 - x0);
|
|
278
|
-
const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
279
|
-
return reverse
|
|
280
|
-
? sql`(${x1} - ${x}::DOUBLE)${f}`
|
|
281
|
-
: sql`(${x}::DOUBLE - ${x0})${f}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function bin2d(q, xp, yp, value, xn, groupby) {
|
|
238
|
+
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
285
239
|
return q
|
|
286
240
|
.select({
|
|
287
241
|
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
288
|
-
|
|
242
|
+
...aggs
|
|
289
243
|
})
|
|
290
244
|
.groupby('index', groupby);
|
|
291
245
|
}
|
|
@@ -320,9 +274,9 @@ function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
|
320
274
|
|
|
321
275
|
return Query
|
|
322
276
|
.from(Query.unionAll(a, b, c, d))
|
|
323
|
-
.select({ index: 'i',
|
|
277
|
+
.select({ index: 'i', density: sum('w') }, groupby)
|
|
324
278
|
.groupby('index', groupby)
|
|
325
|
-
.having(neq('
|
|
279
|
+
.having(neq('density', 0));
|
|
326
280
|
}
|
|
327
281
|
|
|
328
282
|
function tileFloor(value) {
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { sql } from '@uwdata/mosaic-sql';
|
|
2
|
+
import { channelScale } from './channel-scale.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a SQL expression for 1D pixel-level binning.
|
|
6
|
+
* Adjusts for scale transformations (log, sqrt, ...).
|
|
7
|
+
* Returns a [binExpression, field] array, where field is the
|
|
8
|
+
* input value that is binned. Often the field is just a column
|
|
9
|
+
* name. For time data, fields are mapped to numerical timestamps.
|
|
10
|
+
*/
|
|
11
|
+
export function binExpr(mark, channel, n, extent, pad = 1, expr) {
|
|
12
|
+
// get base expression, the channel field unless otherwise given
|
|
13
|
+
const { field } = mark.channelField(channel);
|
|
14
|
+
expr = expr ?? field;
|
|
15
|
+
|
|
16
|
+
// extract scale information
|
|
17
|
+
const { type, apply, sqlApply } = channelScale(mark, channel);
|
|
18
|
+
const reverse = !!mark.plot.getAttribute(`${channel}Reverse`);
|
|
19
|
+
|
|
20
|
+
// return expressions for (unrounded) bin index and field
|
|
21
|
+
const [lo, hi] = extent.map(v => apply(v));
|
|
22
|
+
const v = sqlApply(expr);
|
|
23
|
+
const f = type === 'time' || type === 'utc' ? v : expr;
|
|
24
|
+
const d = hi === lo ? 0 : (n - pad) / (hi - lo);
|
|
25
|
+
const s = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
26
|
+
const bin = reverse
|
|
27
|
+
? sql`(${hi} - ${v}::DOUBLE)${s}`
|
|
28
|
+
: sql`(${v}::DOUBLE - ${lo})${s}`;
|
|
29
|
+
return [bin, f];
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { scaleTransform } from '@uwdata/mosaic-sql';
|
|
2
|
+
|
|
3
|
+
export function channelScale(mark, channel) {
|
|
4
|
+
const { plot } = mark;
|
|
5
|
+
|
|
6
|
+
let scaleType = plot.getAttribute(`${channel}Scale`);
|
|
7
|
+
if (!scaleType) {
|
|
8
|
+
const { field } = mark.channelField(channel, `${channel}1`, `${channel}2`);
|
|
9
|
+
const { type } = mark.stats[field.column];
|
|
10
|
+
scaleType = type === 'date' ? 'time' : 'linear';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const options = { type: scaleType };
|
|
14
|
+
switch (scaleType) {
|
|
15
|
+
case 'log':
|
|
16
|
+
options.base = plot.getAttribute(`${channel}Base`) ?? 10;
|
|
17
|
+
break;
|
|
18
|
+
case 'pow':
|
|
19
|
+
options.exponent = plot.getAttribute(`${channel}Exponent`) ?? 1;
|
|
20
|
+
break;
|
|
21
|
+
case 'symlog':
|
|
22
|
+
options.constant = plot.getAttribute(`${channel}Constant`) ?? 1;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return scaleTransform(options);
|
|
27
|
+
}
|