@uwdata/mosaic-plot 0.5.0 → 0.6.1
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 +6667 -6136
- 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 +105 -52
- package/src/marks/RasterMark.js +188 -64
- package/src/marks/RasterTileMark.js +54 -100
- package/src/marks/util/arrow.js +55 -0
- package/src/marks/util/bin-expr.js +30 -0
- package/src/marks/util/channel-scale.js +27 -0
- package/src/marks/util/density.js +26 -7
- package/src/marks/util/grid.js +94 -29
- package/src/marks/util/interpolate.js +196 -0
- 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/plot.js +7 -2
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "A Mosaic-powered plotting framework based on Observable Plot.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@observablehq/plot": "^0.6.13",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.6.1",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.6.0",
|
|
34
34
|
"d3": "^7.8.5",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "9e788e6dc5241fa1c54967a25fd9599f97da1a41"
|
|
38
38
|
}
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { Density2DMark } from './marks/Density2DMark.js';
|
|
|
11
11
|
export { GeoMark } from './marks/GeoMark.js';
|
|
12
12
|
export { Grid2DMark } from './marks/Grid2DMark.js';
|
|
13
13
|
export { HexbinMark } from './marks/HexbinMark.js';
|
|
14
|
-
export { RasterMark } from './marks/RasterMark.js';
|
|
14
|
+
export { RasterMark, HeatmapMark } from './marks/RasterMark.js';
|
|
15
15
|
export { RasterTileMark } from './marks/RasterTileMark.js';
|
|
16
16
|
export { RegressionMark } from './marks/RegressionMark.js';
|
|
17
17
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { Query, argmax, argmin, max, min, sql } from '@uwdata/mosaic-sql';
|
|
2
|
-
import {
|
|
2
|
+
import { binExpr } from './util/bin-expr.js';
|
|
3
3
|
import { filteredExtent } from './util/extent.js';
|
|
4
4
|
import { Mark } from './Mark.js';
|
|
5
5
|
|
|
6
6
|
export class ConnectedMark extends Mark {
|
|
7
7
|
constructor(type, source, encodings) {
|
|
8
8
|
const dim = type.endsWith('X') ? 'y' : type.endsWith('Y') ? 'x' : null;
|
|
9
|
-
const req = { [dim]: ['
|
|
10
|
-
|
|
9
|
+
const req = { [dim]: ['min', 'max'] };
|
|
11
10
|
super(type, source, encodings, req);
|
|
12
11
|
this.dim = dim;
|
|
13
12
|
}
|
|
@@ -16,28 +15,28 @@ export class ConnectedMark extends Mark {
|
|
|
16
15
|
const { plot, dim, source, stats } = this;
|
|
17
16
|
const { optimize = true } = source.options || {};
|
|
18
17
|
const q = super.query(filter);
|
|
18
|
+
if (!dim) return q;
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
const ortho = dim === 'x' ? 'y' : 'x';
|
|
21
|
+
const value = this.channelField(ortho)?.as;
|
|
22
|
+
const { field, as } = this.channelField(dim);
|
|
23
|
+
const { type } = stats[field.column];
|
|
24
|
+
const isContinuous = type === 'date' || type === 'number';
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
if (optimize && isContinuous && value) {
|
|
27
|
+
// TODO: handle stacked data!
|
|
24
28
|
const { column } = field;
|
|
25
|
-
const {
|
|
29
|
+
const { max, min } = stats[column];
|
|
26
30
|
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
27
|
-
|
|
28
31
|
const [lo, hi] = filteredExtent(filter, column) || [min, max];
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
q.orderby(as);
|
|
32
|
+
const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
|
|
33
|
+
const cols = q.select()
|
|
34
|
+
.map(c => c.as)
|
|
35
|
+
.filter(c => c !== as && c !== value);
|
|
36
|
+
return m4(q, expr, as, value, cols);
|
|
37
|
+
} else {
|
|
38
|
+
return q.orderby(field);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
return q;
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -47,13 +46,13 @@ export class ConnectedMark extends Mark {
|
|
|
47
46
|
* an efficient version with a single scan and the aggregate function
|
|
48
47
|
* argmin and argmax, following https://arxiv.org/pdf/2306.03714.pdf.
|
|
49
48
|
*/
|
|
50
|
-
function m4(input,
|
|
51
|
-
const
|
|
49
|
+
function m4(input, bin, x, y, cols = []) {
|
|
50
|
+
const pixel = sql`FLOOR(${bin})::INTEGER`;
|
|
52
51
|
|
|
53
52
|
const q = (sel) => Query
|
|
54
53
|
.from(input)
|
|
55
54
|
.select(sel)
|
|
56
|
-
.groupby(
|
|
55
|
+
.groupby(pixel, cols);
|
|
57
56
|
|
|
58
57
|
return Query
|
|
59
58
|
.union(
|
package/src/marks/ContourMark.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { contours
|
|
1
|
+
import { contours } from 'd3';
|
|
2
|
+
import { gridDomainContinuous } from './util/grid.js';
|
|
2
3
|
import { handleParam } from './util/handle-param.js';
|
|
3
4
|
import { Grid2DMark } from './Grid2DMark.js';
|
|
4
5
|
import { channelOption } from './Mark.js';
|
|
@@ -6,7 +7,12 @@ import { channelOption } from './Mark.js';
|
|
|
6
7
|
export class ContourMark extends Grid2DMark {
|
|
7
8
|
constructor(source, options) {
|
|
8
9
|
const { thresholds = 10, ...channels } = options;
|
|
9
|
-
super('geo', source,
|
|
10
|
+
super('geo', source, {
|
|
11
|
+
bandwidth: 20,
|
|
12
|
+
interpolate: 'linear',
|
|
13
|
+
pixelSize: 2,
|
|
14
|
+
...channels
|
|
15
|
+
});
|
|
10
16
|
handleParam(this, 'thresholds', thresholds, () => {
|
|
11
17
|
return this.grids ? this.contours().update() : null
|
|
12
18
|
});
|
|
@@ -17,12 +23,12 @@ export class ContourMark extends Grid2DMark {
|
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
contours() {
|
|
20
|
-
const { bins, densityMap, kde, thresholds,
|
|
26
|
+
const { bins, densityMap, kde, thresholds, plot } = this;
|
|
21
27
|
|
|
22
28
|
let tz = thresholds;
|
|
23
29
|
if (!Array.isArray(tz)) {
|
|
24
|
-
const
|
|
25
|
-
tz = Array.from({length: tz - 1}, (_, i) => (
|
|
30
|
+
const [, hi] = gridDomainContinuous(kde, 'density');
|
|
31
|
+
tz = Array.from({length: tz - 1}, (_, i) => (hi * (i + 1)) / tz);
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
if (densityMap.fill || densityMap.stroke) {
|
|
@@ -45,11 +51,11 @@ export class ContourMark extends Grid2DMark {
|
|
|
45
51
|
const contour = contours().size(bins);
|
|
46
52
|
|
|
47
53
|
// generate contours
|
|
48
|
-
this.data = kde.flatMap(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
this.data = kde.flatMap(cell => tz.map(t => {
|
|
55
|
+
return Object.assign(
|
|
56
|
+
transform(contour.contour(cell.density, t), x, y),
|
|
57
|
+
{ ...cell, density: t }
|
|
58
|
+
);
|
|
53
59
|
}));
|
|
54
60
|
|
|
55
61
|
return this;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Query, and, count, isNull, isBetween, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
|
-
import {
|
|
2
|
+
import { binExpr } from './util/bin-expr.js';
|
|
3
3
|
import { extentX, extentY } from './util/extent.js';
|
|
4
4
|
import { handleParam } from './util/handle-param.js';
|
|
5
5
|
import { RasterMark } from './RasterMark.js';
|
|
@@ -7,26 +7,21 @@ import { RasterMark } from './RasterMark.js';
|
|
|
7
7
|
export class DenseLineMark extends RasterMark {
|
|
8
8
|
constructor(source, options) {
|
|
9
9
|
const { normalize = true, ...rest } = options;
|
|
10
|
-
super(source,
|
|
10
|
+
super(source, rest);
|
|
11
11
|
handleParam(this, 'normalize', normalize);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
query(filter = []) {
|
|
15
|
-
const {
|
|
16
|
-
const [x0, x1] = extentX(this, filter);
|
|
17
|
-
const [y0, y1] = extentY(this, filter);
|
|
15
|
+
const { channels, normalize, source, binPad } = this;
|
|
18
16
|
const [nx, ny] = this.bins = this.binDimensions(this);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const rx = !!plot.getAttribute('xReverse');
|
|
22
|
-
const ry = !!plot.getAttribute('yReverse');
|
|
23
|
-
const x = bin1d(bx, x0, x1, nx, rx, this.binPad);
|
|
24
|
-
const y = bin1d(by, y0, y1, ny, ry, this.binPad);
|
|
17
|
+
const [x] = binExpr(this, 'x', nx, extentX(this, filter), binPad);
|
|
18
|
+
const [y] = binExpr(this, 'y', ny, extentY(this, filter), binPad);
|
|
25
19
|
|
|
26
20
|
const q = Query
|
|
27
21
|
.from(source.table)
|
|
28
22
|
.where(stripXY(this, filter));
|
|
29
23
|
|
|
24
|
+
this.aggr = ['density'];
|
|
30
25
|
const groupby = this.groupby = [];
|
|
31
26
|
const z = [];
|
|
32
27
|
for (const c of channels) {
|
|
@@ -132,7 +127,7 @@ function lineDensity(
|
|
|
132
127
|
? { w: sql`1.0 / COUNT(*) OVER (PARTITION BY ${pointPart})` }
|
|
133
128
|
: null
|
|
134
129
|
)
|
|
135
|
-
.where(and(isBetween('x', [0, xn]), isBetween('y', [0, yn])));
|
|
130
|
+
.where(and(isBetween('x', [0, xn], true), isBetween('y', [0, yn], true)));
|
|
136
131
|
|
|
137
132
|
// sum normalized, rasterized series into output grids
|
|
138
133
|
return Query
|
|
@@ -140,7 +135,7 @@ function lineDensity(
|
|
|
140
135
|
.from('points')
|
|
141
136
|
.select(groupby, {
|
|
142
137
|
index: sql`x + y * ${xn}::INTEGER`,
|
|
143
|
-
|
|
138
|
+
density: normalize ? sum('w') : count()
|
|
144
139
|
})
|
|
145
140
|
.groupby('index', groupby);
|
|
146
141
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { Transient } from '../symbols.js';
|
|
3
|
-
import {
|
|
3
|
+
import { binExpr } from './util/bin-expr.js';
|
|
4
4
|
import { dericheConfig, dericheConv1d } from './util/density.js';
|
|
5
5
|
import { extentX, extentY, xext, yext } from './util/extent.js';
|
|
6
6
|
import { grid1d } from './util/grid.js';
|
|
@@ -30,14 +30,12 @@ export class Density1DMark extends Mark {
|
|
|
30
30
|
query(filter = []) {
|
|
31
31
|
if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
|
|
32
32
|
const { bins, channels, dim, source: { table } } = this;
|
|
33
|
-
const
|
|
34
|
-
const bx =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.channelField('weight') ? 'weight' : null
|
|
40
|
-
);
|
|
33
|
+
const extent = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
|
|
34
|
+
const [x, bx] = binExpr(this, dim, bins, extent);
|
|
35
|
+
const q = markQuery(channels, table, [dim])
|
|
36
|
+
.where(filter.concat(isBetween(bx, extent)));
|
|
37
|
+
const v = this.channelField('weight') ? 'weight' : null;
|
|
38
|
+
return binLinear1d(q, x, v);
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
queryResult(data) {
|
|
@@ -81,8 +79,8 @@ export class Density1DMark extends Mark {
|
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
function binLinear1d(q, p,
|
|
85
|
-
const w =
|
|
82
|
+
function binLinear1d(q, p, density) {
|
|
83
|
+
const w = density ? `* ${density}` : '';
|
|
86
84
|
|
|
87
85
|
const u = q.clone().select({
|
|
88
86
|
p,
|
|
@@ -98,7 +96,7 @@ function binLinear1d(q, p, value) {
|
|
|
98
96
|
|
|
99
97
|
return Query
|
|
100
98
|
.from(Query.unionAll(u, v))
|
|
101
|
-
.select({ index: 'i',
|
|
99
|
+
.select({ index: 'i', density: sum('w') })
|
|
102
100
|
.groupby('index')
|
|
103
|
-
.having(gt('
|
|
101
|
+
.having(gt('density', 0));
|
|
104
102
|
}
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { channelScale } from './util/channel-scale.js';
|
|
2
2
|
import { Grid2DMark } from './Grid2DMark.js';
|
|
3
3
|
import { channelOption } from './Mark.js';
|
|
4
4
|
|
|
5
5
|
export class Density2DMark extends Grid2DMark {
|
|
6
6
|
constructor(source, options) {
|
|
7
|
-
const { type = 'dot',
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const { type = 'dot', ...channels } = options;
|
|
8
|
+
super(type, source, {
|
|
9
|
+
bandwidth: 20,
|
|
10
|
+
interpolate: 'linear',
|
|
11
|
+
pad: 0,
|
|
12
|
+
pixelSize: 2,
|
|
13
|
+
...channels
|
|
14
|
+
});
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
convolve() {
|
|
15
18
|
super.convolve();
|
|
16
|
-
const { bins,
|
|
19
|
+
const { bins, pad, extentX, extentY } = this;
|
|
17
20
|
const [nx, ny] = bins;
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
const scaleX = channelScale(this, 'x');
|
|
22
|
+
const scaleY = channelScale(this, 'y');
|
|
23
|
+
const [x0, x1] = extentX.map(v => scaleX.apply(v));
|
|
24
|
+
const [y0, y1] = extentY.map(v => scaleY.apply(v));
|
|
25
|
+
const deltaX = (x1 - x0) / (nx - pad);
|
|
26
|
+
const deltaY = (y1 - y0) / (ny - pad);
|
|
27
|
+
const offset = pad ? 0 : 0.5;
|
|
28
|
+
this.data = points(
|
|
29
|
+
this.kde, bins, x0, y0, deltaX, deltaY,
|
|
30
|
+
scaleX.invert, scaleY.invert, offset
|
|
31
|
+
);
|
|
24
32
|
return this;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
binDimensions() {
|
|
28
|
-
const { plot, binWidth, binsX, binsY } = this;
|
|
29
|
-
return [
|
|
30
|
-
binsX ?? Math.round(plot.innerWidth() / binWidth),
|
|
31
|
-
binsY ?? Math.round(plot.innerHeight() / binWidth)
|
|
32
|
-
];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
35
|
plotSpecs() {
|
|
36
36
|
const { type, channels, densityMap, data } = this;
|
|
37
37
|
const options = {};
|
|
@@ -50,16 +50,18 @@ export class Density2DMark extends Grid2DMark {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function points(kde, bins, x0, y0, deltaX, deltaY, offset) {
|
|
53
|
+
function points(kde, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
|
|
54
54
|
const scale = 1 / (deltaX * deltaY);
|
|
55
55
|
const [nx, ny] = bins;
|
|
56
56
|
const data = [];
|
|
57
|
-
for (const
|
|
57
|
+
for (const cell of kde) {
|
|
58
|
+
const grid = cell.density;
|
|
58
59
|
for (let k = 0, j = 0; j < ny; ++j) {
|
|
59
60
|
for (let i = 0; i < nx; ++i, ++k) {
|
|
60
61
|
data.push({
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
...cell,
|
|
63
|
+
x: invertX(x0 + (i + offset) * deltaX),
|
|
64
|
+
y: invertY(y0 + (j + offset) * deltaY),
|
|
63
65
|
density: grid[k] * scale
|
|
64
66
|
});
|
|
65
67
|
}
|
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { Query, count,
|
|
1
|
+
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { Transient } from '../symbols.js';
|
|
3
|
-
import {
|
|
3
|
+
import { binExpr } from './util/bin-expr.js';
|
|
4
4
|
import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
5
5
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
6
6
|
import { grid2d } from './util/grid.js';
|
|
7
7
|
import { handleParam } from './util/handle-param.js';
|
|
8
|
+
import {
|
|
9
|
+
interpolateNearest, interpolatorBarycentric, interpolatorRandomWalk
|
|
10
|
+
} from './util/interpolate.js';
|
|
8
11
|
import { Mark } from './Mark.js';
|
|
9
12
|
|
|
13
|
+
export const DENSITY = 'density';
|
|
14
|
+
|
|
10
15
|
export class Grid2DMark extends Mark {
|
|
11
16
|
constructor(type, source, options) {
|
|
12
17
|
const {
|
|
13
|
-
bandwidth =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
bandwidth = 0,
|
|
19
|
+
interpolate = 'none',
|
|
20
|
+
pixelSize = 1,
|
|
21
|
+
pad = 1,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
17
24
|
...channels
|
|
18
25
|
} = options;
|
|
19
26
|
|
|
@@ -24,9 +31,11 @@ export class Grid2DMark extends Mark {
|
|
|
24
31
|
handleParam(this, 'bandwidth', bandwidth, () => {
|
|
25
32
|
return this.grids ? this.convolve().update() : null;
|
|
26
33
|
});
|
|
27
|
-
handleParam(this, '
|
|
28
|
-
handleParam(this, '
|
|
29
|
-
handleParam(this, '
|
|
34
|
+
handleParam(this, 'pixelSize', pixelSize);
|
|
35
|
+
handleParam(this, 'interpolate', interpolate);
|
|
36
|
+
handleParam(this, 'pad', pad);
|
|
37
|
+
handleParam(this, 'width', width);
|
|
38
|
+
handleParam(this, 'height', height);
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
setPlot(plot, index) {
|
|
@@ -43,82 +52,115 @@ export class Grid2DMark extends Mark {
|
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
query(filter = []) {
|
|
46
|
-
const {
|
|
55
|
+
const { interpolate, pad, channels, densityMap, source } = this;
|
|
47
56
|
const [x0, x1] = this.extentX = extentX(this, filter);
|
|
48
57
|
const [y0, y1] = this.extentY = extentY(this, filter);
|
|
49
58
|
const [nx, ny] = this.bins = this.binDimensions(this);
|
|
50
|
-
const bx =
|
|
51
|
-
const by =
|
|
52
|
-
const rx = !!plot.getAttribute('xReverse');
|
|
53
|
-
const ry = !!plot.getAttribute('yReverse');
|
|
54
|
-
const x = bin1d(bx, x0, x1, nx, rx, this.binPad);
|
|
55
|
-
const y = bin1d(by, y0, y1, ny, ry, this.binPad);
|
|
59
|
+
const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
|
|
60
|
+
const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
|
|
56
61
|
|
|
57
62
|
// with padded bins, include the entire domain extent
|
|
58
63
|
// if the bins are flush, exclude the extent max
|
|
59
|
-
const bounds =
|
|
60
|
-
? [isBetween(bx, [x0, x1]), isBetween(by, [y0, y1])]
|
|
61
|
-
: [lte(x0, bx), lt(bx, x1), lte(y0, by), lt(by, y1)];
|
|
64
|
+
const bounds = pad
|
|
65
|
+
? [isBetween(bx, [+x0, +x1]), isBetween(by, [+y0, +y1])]
|
|
66
|
+
: [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
|
|
62
67
|
|
|
63
68
|
const q = Query
|
|
64
69
|
.from(source.table)
|
|
65
70
|
.where(filter.concat(bounds));
|
|
66
71
|
|
|
67
72
|
const groupby = this.groupby = [];
|
|
68
|
-
|
|
73
|
+
const aggrMap = {};
|
|
69
74
|
for (const c of channels) {
|
|
70
75
|
if (Object.hasOwn(c, 'field')) {
|
|
71
76
|
const { as, channel, field } = c;
|
|
72
77
|
if (field.aggregate) {
|
|
73
|
-
|
|
78
|
+
// include custom aggregate
|
|
79
|
+
aggrMap[channel] = field;
|
|
74
80
|
densityMap[channel] = true;
|
|
75
81
|
} else if (channel === 'weight') {
|
|
76
|
-
|
|
82
|
+
// compute weighted density
|
|
83
|
+
aggrMap[DENSITY] = sum(field);
|
|
77
84
|
} else if (channel !== 'x' && channel !== 'y') {
|
|
85
|
+
// add groupby field
|
|
78
86
|
q.select({ [as]: field });
|
|
79
87
|
groupby.push(as);
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
}
|
|
91
|
+
const aggr = this.aggr = Object.keys(aggrMap);
|
|
92
|
+
|
|
93
|
+
// check for incompatible encodings
|
|
94
|
+
if (aggrMap.density && aggr.length > 1) {
|
|
95
|
+
throw new Error('Weight option can not be used with custom aggregates.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// if no aggregates, default to count density
|
|
99
|
+
if (!aggr.length) {
|
|
100
|
+
aggr.push(DENSITY);
|
|
101
|
+
aggrMap.density = count();
|
|
102
|
+
}
|
|
83
103
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
104
|
+
// generate grid binning query
|
|
105
|
+
if (interpolate === 'linear') {
|
|
106
|
+
if (aggr.length > 1) {
|
|
107
|
+
throw new Error('Linear binning not applicable to multiple aggregates.');
|
|
108
|
+
}
|
|
109
|
+
if (!aggrMap.density) {
|
|
110
|
+
throw new Error('Linear binning not applicable to custom aggregates.');
|
|
111
|
+
}
|
|
112
|
+
return binLinear2d(q, x, y, aggrMap[DENSITY], nx, groupby);
|
|
113
|
+
} else {
|
|
114
|
+
return bin2d(q, x, y, aggrMap, nx, groupby);
|
|
115
|
+
}
|
|
87
116
|
}
|
|
88
117
|
|
|
89
118
|
binDimensions() {
|
|
90
|
-
const { plot,
|
|
119
|
+
const { plot, pixelSize, width, height } = this;
|
|
91
120
|
return [
|
|
92
|
-
Math.round(plot.innerWidth() /
|
|
93
|
-
Math.round(plot.innerHeight() /
|
|
121
|
+
width ?? Math.round(plot.innerWidth() / pixelSize),
|
|
122
|
+
height ?? Math.round(plot.innerHeight() / pixelSize)
|
|
94
123
|
];
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
queryResult(data) {
|
|
98
|
-
const [
|
|
99
|
-
|
|
127
|
+
const [w, h] = this.bins;
|
|
128
|
+
const interp = maybeInterpolate(this.interpolate);
|
|
129
|
+
this.grids = grid2d(w, h, data, this.aggr, this.groupby, interp);
|
|
100
130
|
return this.convolve();
|
|
101
131
|
}
|
|
102
132
|
|
|
103
133
|
convolve() {
|
|
104
|
-
const { bandwidth, bins, grids, plot } = this;
|
|
134
|
+
const { aggr, bandwidth, bins, grids, plot } = this;
|
|
105
135
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
136
|
+
// no smoothing as default fallback
|
|
137
|
+
this.kde = this.grids;
|
|
138
|
+
|
|
139
|
+
if (bandwidth > 0) {
|
|
140
|
+
// determine which grid to smooth
|
|
141
|
+
const gridProp = aggr.length === 1 ? aggr[0]
|
|
142
|
+
: aggr.includes(DENSITY) ? DENSITY
|
|
143
|
+
: null;
|
|
144
|
+
|
|
145
|
+
// bail if no compatible grid found
|
|
146
|
+
if (!gridProp) {
|
|
147
|
+
console.warn('No compatible grid found for smoothing.');
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// apply smoothing, bandwidth uses units of screen pixels
|
|
111
152
|
const w = plot.innerWidth();
|
|
112
153
|
const h = plot.innerHeight();
|
|
113
154
|
const [nx, ny] = bins;
|
|
114
|
-
const neg = grids.some(
|
|
155
|
+
const neg = grids.some(cell => cell[gridProp].some(v => v < 0));
|
|
115
156
|
const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
|
|
116
157
|
const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
|
|
117
|
-
this.kde = this.grids.map(
|
|
118
|
-
const
|
|
119
|
-
return
|
|
158
|
+
this.kde = this.grids.map(grid => {
|
|
159
|
+
const density = dericheConv2d(configX, configY, grid[gridProp], bins);
|
|
160
|
+
return { ...grid, [gridProp]: density };
|
|
120
161
|
});
|
|
121
162
|
}
|
|
163
|
+
|
|
122
164
|
return this;
|
|
123
165
|
}
|
|
124
166
|
|
|
@@ -127,6 +169,9 @@ export class Grid2DMark extends Mark {
|
|
|
127
169
|
}
|
|
128
170
|
}
|
|
129
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Extract channels that explicitly encode computed densities.
|
|
174
|
+
*/
|
|
130
175
|
function createDensityMap(channels) {
|
|
131
176
|
const densityMap = {};
|
|
132
177
|
for (const key in channels) {
|
|
@@ -138,25 +183,33 @@ function createDensityMap(channels) {
|
|
|
138
183
|
return densityMap;
|
|
139
184
|
}
|
|
140
185
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
186
|
+
function maybeInterpolate(interpolate = 'none') {
|
|
187
|
+
if (typeof interpolate === 'function') return interpolate;
|
|
188
|
+
switch (`${interpolate}`.toLowerCase()) {
|
|
189
|
+
case 'none':
|
|
190
|
+
case 'linear':
|
|
191
|
+
return undefined; // no special interpolation need
|
|
192
|
+
case 'nearest':
|
|
193
|
+
return interpolateNearest;
|
|
194
|
+
case 'barycentric':
|
|
195
|
+
return interpolatorBarycentric();
|
|
196
|
+
case 'random-walk':
|
|
197
|
+
return interpolatorRandomWalk();
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`invalid interpolate: ${interpolate}`);
|
|
147
200
|
}
|
|
148
201
|
|
|
149
|
-
function bin2d(q, xp, yp,
|
|
202
|
+
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
150
203
|
return q
|
|
151
204
|
.select({
|
|
152
205
|
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
153
|
-
|
|
206
|
+
...aggs
|
|
154
207
|
})
|
|
155
208
|
.groupby('index', groupby);
|
|
156
209
|
}
|
|
157
210
|
|
|
158
|
-
function binLinear2d(q, xp, yp,
|
|
159
|
-
const w =
|
|
211
|
+
function binLinear2d(q, xp, yp, density, xn, groupby) {
|
|
212
|
+
const w = density?.column ? `* ${density.column}` : '';
|
|
160
213
|
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
161
214
|
|
|
162
215
|
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
@@ -185,7 +238,7 @@ function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
|
185
238
|
|
|
186
239
|
return Query
|
|
187
240
|
.from(Query.unionAll(a, b, c, d))
|
|
188
|
-
.select({ index: 'i',
|
|
241
|
+
.select({ index: 'i', density: sum('w') }, groupby)
|
|
189
242
|
.groupby('index', groupby)
|
|
190
|
-
.having(
|
|
243
|
+
.having(neq('density', 0));
|
|
191
244
|
}
|