@uwdata/vgplot 0.4.0 → 0.5.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/README.md +4 -2
- package/dist/vgplot.js +5643 -5842
- package/dist/vgplot.min.js +14 -35
- package/package.json +8 -10
- package/src/api.js +292 -0
- package/src/connect.js +14 -0
- package/src/context.js +20 -0
- package/src/index.js +14 -303
- package/src/inputs.js +24 -0
- package/src/{directives → plot}/attributes.js +14 -5
- package/src/{directives → plot}/interactors.js +8 -6
- package/src/{directives → plot}/legends.js +14 -6
- package/src/{directives → plot}/marks.js +16 -13
- package/src/plot/named-plots.js +49 -0
- package/src/plot/plot.js +9 -0
- package/src/directives/plot.js +0 -39
- package/src/interactors/Highlight.js +0 -101
- package/src/interactors/Interval1D.js +0 -90
- package/src/interactors/Interval2D.js +0 -102
- package/src/interactors/Nearest.js +0 -66
- package/src/interactors/PanZoom.js +0 -121
- package/src/interactors/Toggle.js +0 -111
- package/src/interactors/util/brush.js +0 -45
- package/src/interactors/util/close-to.js +0 -9
- package/src/interactors/util/get-field.js +0 -4
- package/src/interactors/util/invert.js +0 -3
- package/src/interactors/util/patchScreenCTM.js +0 -13
- package/src/interactors/util/sanitize-styles.js +0 -9
- package/src/interactors/util/to-kebab-case.js +0 -9
- package/src/layout/index.js +0 -2
- package/src/legend.js +0 -64
- package/src/marks/ConnectedMark.js +0 -63
- package/src/marks/ContourMark.js +0 -89
- package/src/marks/DenseLineMark.js +0 -146
- package/src/marks/Density1DMark.js +0 -104
- package/src/marks/Density2DMark.js +0 -69
- package/src/marks/Grid2DMark.js +0 -191
- package/src/marks/HexbinMark.js +0 -88
- package/src/marks/Mark.js +0 -195
- package/src/marks/RasterMark.js +0 -122
- package/src/marks/RasterTileMark.js +0 -332
- package/src/marks/RegressionMark.js +0 -117
- package/src/marks/util/bin-field.js +0 -17
- package/src/marks/util/density.js +0 -226
- package/src/marks/util/extent.js +0 -56
- package/src/marks/util/grid.js +0 -57
- package/src/marks/util/handle-param.js +0 -14
- package/src/marks/util/is-arrow-table.js +0 -3
- package/src/marks/util/is-color.js +0 -18
- package/src/marks/util/is-constant-option.js +0 -40
- package/src/marks/util/is-symbol.js +0 -20
- package/src/marks/util/raster.js +0 -44
- package/src/marks/util/stats.js +0 -133
- package/src/marks/util/to-data-array.js +0 -58
- package/src/plot-attributes.js +0 -211
- package/src/plot-renderer.js +0 -161
- package/src/plot.js +0 -136
- package/src/spec/parse-data.js +0 -69
- package/src/spec/parse-spec.js +0 -422
- package/src/spec/to-module.js +0 -465
- package/src/spec/util.js +0 -43
- package/src/symbols.js +0 -3
- package/src/transforms/bin.js +0 -81
- package/src/transforms/index.js +0 -3
- /package/src/{directives → plot}/data.js +0 -0
package/src/marks/Mark.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { MosaicClient } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query, Ref, column, isParamLike } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { isColor } from './util/is-color.js';
|
|
4
|
-
import { isConstantOption } from './util/is-constant-option.js';
|
|
5
|
-
import { isSymbol } from './util/is-symbol.js';
|
|
6
|
-
import { toDataArray } from './util/to-data-array.js';
|
|
7
|
-
import { Transform } from '../symbols.js';
|
|
8
|
-
|
|
9
|
-
const isColorChannel = channel => channel === 'stroke' || channel === 'fill';
|
|
10
|
-
const isSymbolChannel = channel => channel === 'symbol';
|
|
11
|
-
const isFieldObject = (channel, field) => {
|
|
12
|
-
return channel !== 'sort' && field != null && !Array.isArray(field);
|
|
13
|
-
};
|
|
14
|
-
const fieldEntry = (channel, field) => ({
|
|
15
|
-
channel,
|
|
16
|
-
field,
|
|
17
|
-
as: field instanceof Ref ? field.column : channel
|
|
18
|
-
});
|
|
19
|
-
const valueEntry = (channel, value) => ({ channel, value });
|
|
20
|
-
|
|
21
|
-
export class Mark extends MosaicClient {
|
|
22
|
-
constructor(type, source, encodings, reqs = {}) {
|
|
23
|
-
super(source?.options?.filterBy);
|
|
24
|
-
this.type = type;
|
|
25
|
-
this.reqs = reqs;
|
|
26
|
-
|
|
27
|
-
this.source = source;
|
|
28
|
-
if (Array.isArray(this.source)) {
|
|
29
|
-
this.data = this.source;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const channels = this.channels = [];
|
|
33
|
-
const params = this.params = new Set;
|
|
34
|
-
|
|
35
|
-
const process = (channel, entry) => {
|
|
36
|
-
const type = typeof entry;
|
|
37
|
-
if (type === 'function' && entry[Transform]) {
|
|
38
|
-
const enc = entry(this, channel);
|
|
39
|
-
for (const key in enc) {
|
|
40
|
-
process(key, enc[key]);
|
|
41
|
-
}
|
|
42
|
-
} else if (type === 'string') {
|
|
43
|
-
if (
|
|
44
|
-
isConstantOption(channel) ||
|
|
45
|
-
isColorChannel(channel) && isColor(entry) ||
|
|
46
|
-
isSymbolChannel(channel) && isSymbol(entry)
|
|
47
|
-
) {
|
|
48
|
-
// interpret constants and color/symbol names as values, not fields
|
|
49
|
-
channels.push(valueEntry(channel, entry));
|
|
50
|
-
} else {
|
|
51
|
-
channels.push(fieldEntry(channel, column(entry)));
|
|
52
|
-
}
|
|
53
|
-
} else if (isParamLike(entry)) {
|
|
54
|
-
if (Array.isArray(entry.columns)) {
|
|
55
|
-
channels.push(fieldEntry(channel, entry));
|
|
56
|
-
params.add(entry);
|
|
57
|
-
} else {
|
|
58
|
-
const c = valueEntry(channel, entry.value);
|
|
59
|
-
channels.push(c);
|
|
60
|
-
entry.addEventListener('value', value => {
|
|
61
|
-
c.value = value;
|
|
62
|
-
return this.update();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
} else if (type === 'object' && isFieldObject(channel, entry)) {
|
|
66
|
-
channels.push(fieldEntry(channel, entry));
|
|
67
|
-
} else if (entry !== undefined) {
|
|
68
|
-
channels.push(valueEntry(channel, entry));
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
for (const channel in encodings) {
|
|
73
|
-
process(channel, encodings[channel]);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setPlot(plot, index) {
|
|
78
|
-
this.plot = plot;
|
|
79
|
-
this.index = index;
|
|
80
|
-
plot.addParams(this, this.params);
|
|
81
|
-
if (this.source?.table) this.queryPending();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
hasOwnData() {
|
|
85
|
-
return this.source == null || Array.isArray(this.source);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
channel(channel) {
|
|
89
|
-
return this.channels.find(c => c.channel === channel);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
channelField(...channels) {
|
|
93
|
-
const list = channels.flat();
|
|
94
|
-
for (const channel of list) {
|
|
95
|
-
const c = this.channel(channel);
|
|
96
|
-
if (c?.field) return c;
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fields() {
|
|
102
|
-
if (this.hasOwnData()) return null;
|
|
103
|
-
const { source: { table }, channels, reqs } = this;
|
|
104
|
-
|
|
105
|
-
const fields = new Map;
|
|
106
|
-
for (const { channel, field } of channels) {
|
|
107
|
-
const column = field?.column;
|
|
108
|
-
if (!column) {
|
|
109
|
-
continue; // no column to lookup
|
|
110
|
-
} else if (field.stats?.length || reqs[channel]) {
|
|
111
|
-
if (!fields.has(column)) fields.set(column, new Set);
|
|
112
|
-
const entry = fields.get(column);
|
|
113
|
-
reqs[channel]?.forEach(s => entry.add(s));
|
|
114
|
-
field.stats?.forEach(s => entry.add(s));
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return Array.from(fields, ([column, stats]) => {
|
|
118
|
-
return { table, column, stats: Array.from(stats) };
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fieldInfo(info) {
|
|
123
|
-
this.stats = info.reduce(
|
|
124
|
-
(o, d) => (o[d.column] = d, o),
|
|
125
|
-
Object.create(null)
|
|
126
|
-
);
|
|
127
|
-
return this;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
query(filter = []) {
|
|
131
|
-
if (this.hasOwnData()) return null;
|
|
132
|
-
const { channels, source: { table } } = this;
|
|
133
|
-
return markQuery(channels, table).where(filter);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
queryPending() {
|
|
137
|
-
this.plot.pending(this);
|
|
138
|
-
return this;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
queryResult(data) {
|
|
142
|
-
this.data = toDataArray(data);
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
update() {
|
|
147
|
-
return this.plot.update(this);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
plotSpecs() {
|
|
151
|
-
const { type, data, channels } = this;
|
|
152
|
-
const options = {};
|
|
153
|
-
for (const c of channels) {
|
|
154
|
-
options[c.channel] = channelOption(c)
|
|
155
|
-
}
|
|
156
|
-
return [{ type, data, options }];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function channelOption(c) {
|
|
161
|
-
// use a scale override for color channels to sidestep
|
|
162
|
-
// https://github.com/observablehq/plot/issues/1593
|
|
163
|
-
return Object.hasOwn(c, 'value') ? c.value
|
|
164
|
-
: isColorChannel(c.channel) ? { value: c.as, scale: 'color' }
|
|
165
|
-
: c.as;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function markQuery(channels, table, skip = []) {
|
|
169
|
-
const q = Query.from({ source: table });
|
|
170
|
-
const dims = new Set;
|
|
171
|
-
let aggr = false;
|
|
172
|
-
|
|
173
|
-
for (const c of channels) {
|
|
174
|
-
const { channel, field, as } = c;
|
|
175
|
-
if (skip.includes(channel)) continue;
|
|
176
|
-
|
|
177
|
-
if (channel === 'orderby') {
|
|
178
|
-
q.orderby(c.value);
|
|
179
|
-
} else if (field) {
|
|
180
|
-
if (field.aggregate) {
|
|
181
|
-
aggr = true;
|
|
182
|
-
} else {
|
|
183
|
-
if (dims.has(as)) continue;
|
|
184
|
-
dims.add(as);
|
|
185
|
-
}
|
|
186
|
-
q.select({ [as]: field });
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (aggr) {
|
|
191
|
-
q.groupby(Array.from(dims));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return q;
|
|
195
|
-
}
|
package/src/marks/RasterMark.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { scale } from '@observablehq/plot';
|
|
2
|
-
import { isColor } from './util/is-color.js';
|
|
3
|
-
import { createCanvas, raster, opacityMap, palette } from './util/raster.js';
|
|
4
|
-
import { Grid2DMark } from './Grid2DMark.js';
|
|
5
|
-
|
|
6
|
-
export class RasterMark extends Grid2DMark {
|
|
7
|
-
constructor(source, options) {
|
|
8
|
-
super('image', source, options);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
setPlot(plot, index) {
|
|
12
|
-
const update = () => { if (this.stats) this.rasterize(); };
|
|
13
|
-
plot.addAttributeListener('schemeColor', update);
|
|
14
|
-
super.setPlot(plot, index);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
convolve() {
|
|
18
|
-
return super.convolve().rasterize();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
rasterize() {
|
|
22
|
-
const { bins, kde, groupby } = this;
|
|
23
|
-
const [ w, h ] = bins;
|
|
24
|
-
|
|
25
|
-
// raster data
|
|
26
|
-
const { canvas, ctx, img } = imageData(this, w, h);
|
|
27
|
-
|
|
28
|
-
// scale function to map densities to [0, 1]
|
|
29
|
-
const s = imageScale(this);
|
|
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]);
|
|
34
|
-
|
|
35
|
-
// generate raster images
|
|
36
|
-
this.data = kde.map(grid => {
|
|
37
|
-
const palette = imagePalette(this, domain, grid.key?.[idx]);
|
|
38
|
-
raster(grid, img.data, w, h, s, palette);
|
|
39
|
-
ctx.putImageData(img, 0, 0);
|
|
40
|
-
return { src: canvas.toDataURL() };
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
plotSpecs() {
|
|
47
|
-
const { type, plot, data } = this;
|
|
48
|
-
const options = {
|
|
49
|
-
src: 'src',
|
|
50
|
-
width: plot.innerWidth(),
|
|
51
|
-
height: plot.innerHeight(),
|
|
52
|
-
preserveAspectRatio: 'none',
|
|
53
|
-
imageRendering: this.channel('imageRendering')?.value,
|
|
54
|
-
frameAnchor: 'middle'
|
|
55
|
-
};
|
|
56
|
-
return [{ type, data, options }];
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function imageData(mark, w, h) {
|
|
61
|
-
if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
|
|
62
|
-
const canvas = createCanvas(w, h);
|
|
63
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
64
|
-
const img = ctx.getImageData(0, 0, w, h);
|
|
65
|
-
mark.image = { canvas, ctx, img, w, h };
|
|
66
|
-
}
|
|
67
|
-
return mark.image;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function imageScale(mark) {
|
|
71
|
-
const { densityMap, kde, plot } = mark;
|
|
72
|
-
let domain = densityMap.fill && plot.getAttribute('colorDomain');
|
|
73
|
-
|
|
74
|
-
// compute kde grid extents if no explicit domain
|
|
75
|
-
if (!domain) {
|
|
76
|
-
let lo = 0, hi = 0;
|
|
77
|
-
kde.forEach(grid => {
|
|
78
|
-
for (const v of grid) {
|
|
79
|
-
if (v < lo) lo = v;
|
|
80
|
-
if (v > hi) hi = v;
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
domain = (lo === 0 && hi === 0) ? [0, 1] : [lo, hi];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const type = plot.getAttribute('colorScale');
|
|
87
|
-
return scale({ x: { type, domain, range: [0, 1] } }).apply;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function imagePalette(mark, domain, value, steps = 1024) {
|
|
91
|
-
const { densityMap, plot } = mark;
|
|
92
|
-
const scheme = plot.getAttribute('colorScheme');
|
|
93
|
-
let color;
|
|
94
|
-
|
|
95
|
-
if (densityMap.fill) {
|
|
96
|
-
if (scheme) {
|
|
97
|
-
try {
|
|
98
|
-
return palette(
|
|
99
|
-
steps,
|
|
100
|
-
scale({color: { scheme, domain: [0, 1] }}).interpolate
|
|
101
|
-
);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.warn(err);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} else if (domain.length) {
|
|
107
|
-
// fill is based on data values
|
|
108
|
-
const range = plot.getAttribute('colorRange');
|
|
109
|
-
const spec = {
|
|
110
|
-
domain,
|
|
111
|
-
range,
|
|
112
|
-
scheme: scheme || (range ? undefined : 'tableau10')
|
|
113
|
-
};
|
|
114
|
-
color = scale({ color: spec }).apply(value);
|
|
115
|
-
} else {
|
|
116
|
-
// fill color is a constant
|
|
117
|
-
const fill = mark.channelField('fill');
|
|
118
|
-
color = isColor(fill?.value) ? fill.value : undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return palette(steps, opacityMap(color));
|
|
122
|
-
}
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { coordinator } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { scale } from '@observablehq/plot';
|
|
4
|
-
import { extentX, extentY } from './util/extent.js';
|
|
5
|
-
import { isColor } from './util/is-color.js';
|
|
6
|
-
import { createCanvas, raster, opacityMap, palette } from './util/raster.js';
|
|
7
|
-
import { Grid2DMark } from './Grid2DMark.js';
|
|
8
|
-
import { binField } from './util/bin-field.js';
|
|
9
|
-
|
|
10
|
-
export class RasterTileMark extends Grid2DMark {
|
|
11
|
-
constructor(source, options) {
|
|
12
|
-
const { origin = [0, 0], dim = 'xy', ...markOptions } = options;
|
|
13
|
-
super('image', source, markOptions);
|
|
14
|
-
|
|
15
|
-
// TODO: make part of data source instead of options?
|
|
16
|
-
this.origin = origin;
|
|
17
|
-
this.tileX = dim.toLowerCase().includes('x');
|
|
18
|
-
this.tileY = dim.toLowerCase().includes('y');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
setPlot(plot, index) {
|
|
22
|
-
const update = () => { if (this.stats) this.rasterize(); };
|
|
23
|
-
plot.addAttributeListener('schemeColor', update);
|
|
24
|
-
super.setPlot(plot, index);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
requestQuery() {
|
|
28
|
-
return this.requestTiles();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
query(filter = []) {
|
|
32
|
-
this._filter = filter;
|
|
33
|
-
// we will submit our own queries
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
tileQuery(extent) {
|
|
38
|
-
const { plot, binType, binPad, channels, densityMap, source } = this;
|
|
39
|
-
const [[x0, x1], [y0, y1]] = extent;
|
|
40
|
-
const [nx, ny] = this.bins;
|
|
41
|
-
const bx = binField(this, 'x');
|
|
42
|
-
const by = binField(this, 'y');
|
|
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);
|
|
47
|
-
|
|
48
|
-
// with padded bins, include the entire domain extent
|
|
49
|
-
// if the bins are flush, exclude the extent max
|
|
50
|
-
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)];
|
|
53
|
-
|
|
54
|
-
const q = Query
|
|
55
|
-
.from(source.table)
|
|
56
|
-
.where(bounds);
|
|
57
|
-
|
|
58
|
-
const groupby = this.groupby = [];
|
|
59
|
-
let agg = count();
|
|
60
|
-
for (const c of channels) {
|
|
61
|
-
if (Object.hasOwn(c, 'field')) {
|
|
62
|
-
const { channel, field } = c;
|
|
63
|
-
if (field.aggregate) {
|
|
64
|
-
agg = field;
|
|
65
|
-
densityMap[channel] = true;
|
|
66
|
-
} else if (channel === 'weight') {
|
|
67
|
-
agg = sum(field);
|
|
68
|
-
} else if (channel !== 'x' && channel !== 'y') {
|
|
69
|
-
q.select({ [channel]: field });
|
|
70
|
-
groupby.push(channel);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return binType === 'linear'
|
|
76
|
-
? binLinear2d(q, x, y, agg, nx, groupby)
|
|
77
|
-
: bin2d(q, x, y, agg, nx, groupby);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async requestTiles() {
|
|
81
|
-
// get coordinator, cancel prior prefetch queries
|
|
82
|
-
const mc = coordinator();
|
|
83
|
-
if (this.prefetch) mc.cancel(this.prefetch);
|
|
84
|
-
|
|
85
|
-
// get view extent info
|
|
86
|
-
const { binPad, tileX, tileY, origin: [tx, ty] } = this;
|
|
87
|
-
const [m, n] = this.bins = this.binDimensions(this);
|
|
88
|
-
const [x0, x1] = extentX(this, this._filter);
|
|
89
|
-
const [y0, y1] = extentY(this, this._filter);
|
|
90
|
-
const xspan = x1 - x0;
|
|
91
|
-
const yspan = y1 - y0;
|
|
92
|
-
const xx = Math.floor((x0 - tx) * (m - binPad) / xspan);
|
|
93
|
-
const yy = Math.floor((y0 - ty) * (n - binPad) / yspan);
|
|
94
|
-
|
|
95
|
-
const tileExtent = (i, j) => [
|
|
96
|
-
[tx + i * xspan, tx + (i + 1) * xspan],
|
|
97
|
-
[ty + j * yspan, ty + (j + 1) * yspan]
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
// get tile coords that overlap current view extent
|
|
101
|
-
const i0 = Math.floor((x0 - tx) / xspan);
|
|
102
|
-
const i1 = tileX ? tileFloor((x1 - tx) / xspan) : i0;
|
|
103
|
-
const j0 = Math.floor((y0 - ty) / yspan);
|
|
104
|
-
const j1 = tileY ? tileFloor((y1 - ty) / yspan) : j0;
|
|
105
|
-
|
|
106
|
-
// query for currently needed data tiles
|
|
107
|
-
const coords = [];
|
|
108
|
-
for (let i = i0; i <= i1; ++i) {
|
|
109
|
-
for (let j = j0; j <= j1; ++j) {
|
|
110
|
-
coords.push([i, j]);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const queries = coords.map(
|
|
114
|
-
([i, j]) => mc.query(this.tileQuery(tileExtent(i, j)))
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// prefetch tiles along periphery of current tiles
|
|
118
|
-
const prefetchCoords = [];
|
|
119
|
-
if (tileX) {
|
|
120
|
-
for (let j = j0; j <= j1; ++j) {
|
|
121
|
-
prefetchCoords.push([i1 + 1, j]);
|
|
122
|
-
prefetchCoords.push([i0 - 1, j]);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (tileY) {
|
|
126
|
-
const x0 = tileX ? i0 - 1 : i0;
|
|
127
|
-
const x1 = tileX ? i1 + 1 : i1;
|
|
128
|
-
for (let i = x0; i <= x1; ++i) {
|
|
129
|
-
prefetchCoords.push([i, j1 + 1]);
|
|
130
|
-
prefetchCoords.push([i, j0 - 1]);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
this.prefetch = prefetchCoords.map(
|
|
134
|
-
([i, j]) => mc.prefetch(this.tileQuery(tileExtent(i, j)))
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// wait for tile queries to complete, then update
|
|
138
|
-
const tiles = await Promise.all(queries);
|
|
139
|
-
this.grids = [{ grid: processTiles(m, n, xx, yy, coords, tiles) }];
|
|
140
|
-
this.convolve().update();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
convolve() {
|
|
144
|
-
return super.convolve().rasterize();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
rasterize() {
|
|
148
|
-
const { bins, kde, groupby } = this;
|
|
149
|
-
const [ w, h ] = bins;
|
|
150
|
-
|
|
151
|
-
// raster data
|
|
152
|
-
const { canvas, ctx, img } = imageData(this, w, h);
|
|
153
|
-
|
|
154
|
-
// scale function to map densities to [0, 1]
|
|
155
|
-
const s = imageScale(this);
|
|
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]);
|
|
160
|
-
|
|
161
|
-
// generate raster images
|
|
162
|
-
this.data = kde.map(grid => {
|
|
163
|
-
const palette = imagePalette(this, domain, grid.key?.[idx]);
|
|
164
|
-
raster(grid, img.data, w, h, s, palette);
|
|
165
|
-
ctx.putImageData(img, 0, 0);
|
|
166
|
-
return { src: canvas.toDataURL() };
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
return this;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
plotSpecs() {
|
|
173
|
-
const { type, data, plot } = this;
|
|
174
|
-
const options = {
|
|
175
|
-
src: 'src',
|
|
176
|
-
width: plot.innerWidth(),
|
|
177
|
-
height: plot.innerHeight(),
|
|
178
|
-
preserveAspectRatio: 'none',
|
|
179
|
-
imageRendering: this.channel('imageRendering')?.value,
|
|
180
|
-
frameAnchor: 'middle'
|
|
181
|
-
};
|
|
182
|
-
return [{ type, data, options }];
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function processTiles(m, n, x, y, coords, tiles) {
|
|
187
|
-
const grid = new Float64Array(m * n);
|
|
188
|
-
tiles.forEach((data, index) => {
|
|
189
|
-
const [i, j] = coords[index];
|
|
190
|
-
const tx = i * m - x;
|
|
191
|
-
const ty = j * n - y;
|
|
192
|
-
copy(m, n, grid, data, tx, ty);
|
|
193
|
-
});
|
|
194
|
-
return grid;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function copy(m, n, grid, values, tx, ty) {
|
|
198
|
-
// index = row + col * width
|
|
199
|
-
const num = values.numRows;
|
|
200
|
-
if (num === 0) return;
|
|
201
|
-
const index = values.getChild('index').toArray();
|
|
202
|
-
const value = values.getChild('value').toArray();
|
|
203
|
-
for (let row = 0; row < num; ++row) {
|
|
204
|
-
const idx = index[row];
|
|
205
|
-
const i = tx + (idx % m);
|
|
206
|
-
const j = ty + Math.floor(idx / m);
|
|
207
|
-
if (0 <= i && i < m && 0 <= j && j < n) {
|
|
208
|
-
grid[i + j * m] = value[row];
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function imageData(mark, w, h) {
|
|
214
|
-
if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
|
|
215
|
-
const canvas = createCanvas(w, h);
|
|
216
|
-
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
|
217
|
-
const img = ctx.getImageData(0, 0, w, h);
|
|
218
|
-
mark.image = { canvas, ctx, img, w, h };
|
|
219
|
-
}
|
|
220
|
-
return mark.image;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function imageScale(mark) {
|
|
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
|
-
let color;
|
|
247
|
-
|
|
248
|
-
if (densityMap.fill) {
|
|
249
|
-
if (scheme) {
|
|
250
|
-
try {
|
|
251
|
-
return palette(
|
|
252
|
-
steps,
|
|
253
|
-
scale({color: { scheme, domain: [0, 1] }}).interpolate
|
|
254
|
-
);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
console.warn(err);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} else if (domain.length) {
|
|
260
|
-
// fill is based on data values
|
|
261
|
-
const range = plot.getAttribute('colorRange');
|
|
262
|
-
const spec = {
|
|
263
|
-
domain,
|
|
264
|
-
range,
|
|
265
|
-
scheme: scheme || (range ? undefined : 'tableau10')
|
|
266
|
-
};
|
|
267
|
-
color = scale({ color: spec }).apply(value);
|
|
268
|
-
} else {
|
|
269
|
-
// fill color is a constant
|
|
270
|
-
const fill = mark.channelField('fill');
|
|
271
|
-
color = isColor(fill?.value) ? fill.value : undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return palette(steps, opacityMap(color));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function bin1d(x, x0, x1, n, reverse, pad) {
|
|
278
|
-
const d = (n - pad) / (x1 - x0);
|
|
279
|
-
const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
280
|
-
return reverse
|
|
281
|
-
? sql`(${x1} - ${x}::DOUBLE)${f}`
|
|
282
|
-
: sql`(${x}::DOUBLE - ${x0})${f}`;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function bin2d(q, xp, yp, value, xn, groupby) {
|
|
286
|
-
return q
|
|
287
|
-
.select({
|
|
288
|
-
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
289
|
-
value
|
|
290
|
-
})
|
|
291
|
-
.groupby('index', groupby);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
295
|
-
const w = value.column ? `* ${value.column}` : '';
|
|
296
|
-
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
297
|
-
|
|
298
|
-
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
299
|
-
const a = subq(
|
|
300
|
-
sql`FLOOR(xp)::INTEGER + FLOOR(yp)::INTEGER * ${xn}`,
|
|
301
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
// grid[xu + yv * xn] += (xv - xp) * (yp - yu) * wi;
|
|
305
|
-
const b = subq(
|
|
306
|
-
sql`FLOOR(xp)::INTEGER + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
307
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// grid[xv + yu * xn] += (xp - xu) * (yv - yp) * wi;
|
|
311
|
-
const c = subq(
|
|
312
|
-
sql`FLOOR(xp)::INTEGER + 1 + FLOOR(yp)::INTEGER * ${xn}`,
|
|
313
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
// grid[xv + yv * xn] += (xp - xu) * (yp - yu) * wi;
|
|
317
|
-
const d = subq(
|
|
318
|
-
sql`FLOOR(xp)::INTEGER + 1 + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
319
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
return Query
|
|
323
|
-
.from(Query.unionAll(a, b, c, d))
|
|
324
|
-
.select({ index: 'i', value: sum('w') }, groupby)
|
|
325
|
-
.groupby('index', groupby)
|
|
326
|
-
.having(neq('value', 0));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function tileFloor(value) {
|
|
330
|
-
const floored = Math.floor(value);
|
|
331
|
-
return floored === value ? floored - 1 : floored;
|
|
332
|
-
}
|