@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.
Files changed (65) hide show
  1. package/README.md +4 -2
  2. package/dist/vgplot.js +5643 -5842
  3. package/dist/vgplot.min.js +14 -35
  4. package/package.json +8 -10
  5. package/src/api.js +292 -0
  6. package/src/connect.js +14 -0
  7. package/src/context.js +20 -0
  8. package/src/index.js +14 -303
  9. package/src/inputs.js +24 -0
  10. package/src/{directives → plot}/attributes.js +14 -5
  11. package/src/{directives → plot}/interactors.js +8 -6
  12. package/src/{directives → plot}/legends.js +14 -6
  13. package/src/{directives → plot}/marks.js +16 -13
  14. package/src/plot/named-plots.js +49 -0
  15. package/src/plot/plot.js +9 -0
  16. package/src/directives/plot.js +0 -39
  17. package/src/interactors/Highlight.js +0 -101
  18. package/src/interactors/Interval1D.js +0 -90
  19. package/src/interactors/Interval2D.js +0 -102
  20. package/src/interactors/Nearest.js +0 -66
  21. package/src/interactors/PanZoom.js +0 -121
  22. package/src/interactors/Toggle.js +0 -111
  23. package/src/interactors/util/brush.js +0 -45
  24. package/src/interactors/util/close-to.js +0 -9
  25. package/src/interactors/util/get-field.js +0 -4
  26. package/src/interactors/util/invert.js +0 -3
  27. package/src/interactors/util/patchScreenCTM.js +0 -13
  28. package/src/interactors/util/sanitize-styles.js +0 -9
  29. package/src/interactors/util/to-kebab-case.js +0 -9
  30. package/src/layout/index.js +0 -2
  31. package/src/legend.js +0 -64
  32. package/src/marks/ConnectedMark.js +0 -63
  33. package/src/marks/ContourMark.js +0 -89
  34. package/src/marks/DenseLineMark.js +0 -146
  35. package/src/marks/Density1DMark.js +0 -104
  36. package/src/marks/Density2DMark.js +0 -69
  37. package/src/marks/Grid2DMark.js +0 -191
  38. package/src/marks/HexbinMark.js +0 -88
  39. package/src/marks/Mark.js +0 -195
  40. package/src/marks/RasterMark.js +0 -122
  41. package/src/marks/RasterTileMark.js +0 -332
  42. package/src/marks/RegressionMark.js +0 -117
  43. package/src/marks/util/bin-field.js +0 -17
  44. package/src/marks/util/density.js +0 -226
  45. package/src/marks/util/extent.js +0 -56
  46. package/src/marks/util/grid.js +0 -57
  47. package/src/marks/util/handle-param.js +0 -14
  48. package/src/marks/util/is-arrow-table.js +0 -3
  49. package/src/marks/util/is-color.js +0 -18
  50. package/src/marks/util/is-constant-option.js +0 -40
  51. package/src/marks/util/is-symbol.js +0 -20
  52. package/src/marks/util/raster.js +0 -44
  53. package/src/marks/util/stats.js +0 -133
  54. package/src/marks/util/to-data-array.js +0 -58
  55. package/src/plot-attributes.js +0 -211
  56. package/src/plot-renderer.js +0 -161
  57. package/src/plot.js +0 -136
  58. package/src/spec/parse-data.js +0 -69
  59. package/src/spec/parse-spec.js +0 -422
  60. package/src/spec/to-module.js +0 -465
  61. package/src/spec/util.js +0 -43
  62. package/src/symbols.js +0 -3
  63. package/src/transforms/bin.js +0 -81
  64. package/src/transforms/index.js +0 -3
  65. /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
- }
@@ -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
- }