@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.0",
|
|
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": "51517b28e916e355f4ce0dc6e98aef3a1db3f7b2"
|
|
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,23 @@
|
|
|
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
8
|
import { Mark } from './Mark.js';
|
|
9
9
|
|
|
10
|
+
export const DENSITY = 'density';
|
|
11
|
+
|
|
10
12
|
export class Grid2DMark extends Mark {
|
|
11
13
|
constructor(type, source, options) {
|
|
12
14
|
const {
|
|
13
|
-
bandwidth =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
bandwidth = 0,
|
|
16
|
+
interpolate = 'none',
|
|
17
|
+
pixelSize = 1,
|
|
18
|
+
pad = 1,
|
|
19
|
+
width,
|
|
20
|
+
height,
|
|
17
21
|
...channels
|
|
18
22
|
} = options;
|
|
19
23
|
|
|
@@ -24,9 +28,11 @@ export class Grid2DMark extends Mark {
|
|
|
24
28
|
handleParam(this, 'bandwidth', bandwidth, () => {
|
|
25
29
|
return this.grids ? this.convolve().update() : null;
|
|
26
30
|
});
|
|
27
|
-
handleParam(this, '
|
|
28
|
-
handleParam(this, '
|
|
29
|
-
handleParam(this, '
|
|
31
|
+
handleParam(this, 'pixelSize', pixelSize);
|
|
32
|
+
handleParam(this, 'interpolate', interpolate);
|
|
33
|
+
handleParam(this, 'pad', pad);
|
|
34
|
+
handleParam(this, 'width', width);
|
|
35
|
+
handleParam(this, 'height', height);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
setPlot(plot, index) {
|
|
@@ -43,82 +49,114 @@ export class Grid2DMark extends Mark {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
query(filter = []) {
|
|
46
|
-
const {
|
|
52
|
+
const { interpolate, pad, channels, densityMap, source } = this;
|
|
47
53
|
const [x0, x1] = this.extentX = extentX(this, filter);
|
|
48
54
|
const [y0, y1] = this.extentY = extentY(this, filter);
|
|
49
55
|
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);
|
|
56
|
+
const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
|
|
57
|
+
const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
|
|
56
58
|
|
|
57
59
|
// with padded bins, include the entire domain extent
|
|
58
60
|
// 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)];
|
|
61
|
+
const bounds = pad
|
|
62
|
+
? [isBetween(bx, [+x0, +x1]), isBetween(by, [+y0, +y1])]
|
|
63
|
+
: [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
|
|
62
64
|
|
|
63
65
|
const q = Query
|
|
64
66
|
.from(source.table)
|
|
65
67
|
.where(filter.concat(bounds));
|
|
66
68
|
|
|
67
69
|
const groupby = this.groupby = [];
|
|
68
|
-
|
|
70
|
+
const aggrMap = {};
|
|
69
71
|
for (const c of channels) {
|
|
70
72
|
if (Object.hasOwn(c, 'field')) {
|
|
71
73
|
const { as, channel, field } = c;
|
|
72
74
|
if (field.aggregate) {
|
|
73
|
-
|
|
75
|
+
// include custom aggregate
|
|
76
|
+
aggrMap[channel] = field;
|
|
74
77
|
densityMap[channel] = true;
|
|
75
78
|
} else if (channel === 'weight') {
|
|
76
|
-
|
|
79
|
+
// compute weighted density
|
|
80
|
+
aggrMap[DENSITY] = sum(field);
|
|
77
81
|
} else if (channel !== 'x' && channel !== 'y') {
|
|
82
|
+
// add groupby field
|
|
78
83
|
q.select({ [as]: field });
|
|
79
84
|
groupby.push(as);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
const aggr = this.aggr = Object.keys(aggrMap);
|
|
89
|
+
|
|
90
|
+
// check for incompatible encodings
|
|
91
|
+
if (aggrMap.density && aggr.length > 1) {
|
|
92
|
+
throw new Error('Weight option can not be used with custom aggregates.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// if no aggregates, default to count density
|
|
96
|
+
if (!aggr.length) {
|
|
97
|
+
aggr.push(DENSITY);
|
|
98
|
+
aggrMap.density = count();
|
|
99
|
+
}
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
// generate grid binning query
|
|
102
|
+
if (interpolate === 'linear') {
|
|
103
|
+
if (aggr.length > 1) {
|
|
104
|
+
throw new Error('Linear binning not applicable to multiple aggregates.');
|
|
105
|
+
}
|
|
106
|
+
if (!aggrMap.density) {
|
|
107
|
+
throw new Error('Linear binning not applicable to custom aggregates.');
|
|
108
|
+
}
|
|
109
|
+
return binLinear2d(q, x, y, aggrMap[DENSITY], nx, groupby);
|
|
110
|
+
} else {
|
|
111
|
+
return bin2d(q, x, y, aggrMap, nx, groupby);
|
|
112
|
+
}
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
binDimensions() {
|
|
90
|
-
const { plot,
|
|
116
|
+
const { plot, pixelSize, width, height } = this;
|
|
91
117
|
return [
|
|
92
|
-
Math.round(plot.innerWidth() /
|
|
93
|
-
Math.round(plot.innerHeight() /
|
|
118
|
+
width ?? Math.round(plot.innerWidth() / pixelSize),
|
|
119
|
+
height ?? Math.round(plot.innerHeight() / pixelSize)
|
|
94
120
|
];
|
|
95
121
|
}
|
|
96
122
|
|
|
97
123
|
queryResult(data) {
|
|
98
124
|
const [nx, ny] = this.bins;
|
|
99
|
-
this.grids = grid2d(nx, ny, data, this.groupby);
|
|
125
|
+
this.grids = grid2d(nx, ny, data, this.aggr, this.groupby);
|
|
100
126
|
return this.convolve();
|
|
101
127
|
}
|
|
102
128
|
|
|
103
129
|
convolve() {
|
|
104
|
-
const { bandwidth, bins, grids, plot } = this;
|
|
130
|
+
const { aggr, bandwidth, bins, grids, plot } = this;
|
|
105
131
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
// no smoothing as default fallback
|
|
133
|
+
this.kde = this.grids;
|
|
134
|
+
|
|
135
|
+
if (bandwidth > 0) {
|
|
136
|
+
// determine which grid to smooth
|
|
137
|
+
const gridProp = aggr.length === 1 ? aggr[0]
|
|
138
|
+
: aggr.includes(DENSITY) ? DENSITY
|
|
139
|
+
: null;
|
|
140
|
+
|
|
141
|
+
// bail if no compatible grid found
|
|
142
|
+
if (!gridProp) {
|
|
143
|
+
console.warn('No compatible grid found for smoothing.');
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// apply smoothing, bandwidth uses units of screen pixels
|
|
111
148
|
const w = plot.innerWidth();
|
|
112
149
|
const h = plot.innerHeight();
|
|
113
150
|
const [nx, ny] = bins;
|
|
114
|
-
const neg = grids.some(
|
|
151
|
+
const neg = grids.some(cell => cell[gridProp].some(v => v < 0));
|
|
115
152
|
const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
|
|
116
153
|
const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
|
|
117
|
-
this.kde = this.grids.map(
|
|
118
|
-
const
|
|
119
|
-
return
|
|
154
|
+
this.kde = this.grids.map(grid => {
|
|
155
|
+
const density = dericheConv2d(configX, configY, grid[gridProp], bins);
|
|
156
|
+
return { ...grid, [gridProp]: density };
|
|
120
157
|
});
|
|
121
158
|
}
|
|
159
|
+
|
|
122
160
|
return this;
|
|
123
161
|
}
|
|
124
162
|
|
|
@@ -127,6 +165,9 @@ export class Grid2DMark extends Mark {
|
|
|
127
165
|
}
|
|
128
166
|
}
|
|
129
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Extract channels that explicitly encode computed densities.
|
|
170
|
+
*/
|
|
130
171
|
function createDensityMap(channels) {
|
|
131
172
|
const densityMap = {};
|
|
132
173
|
for (const key in channels) {
|
|
@@ -138,25 +179,17 @@ function createDensityMap(channels) {
|
|
|
138
179
|
return densityMap;
|
|
139
180
|
}
|
|
140
181
|
|
|
141
|
-
function
|
|
142
|
-
const d = (n - pad) / (x1 - x0);
|
|
143
|
-
const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
144
|
-
return reverse
|
|
145
|
-
? sql`(${x1} - ${x}::DOUBLE)${f}`
|
|
146
|
-
: sql`(${x}::DOUBLE - ${x0})${f}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function bin2d(q, xp, yp, value, xn, groupby) {
|
|
182
|
+
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
150
183
|
return q
|
|
151
184
|
.select({
|
|
152
185
|
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
153
|
-
|
|
186
|
+
...aggs
|
|
154
187
|
})
|
|
155
188
|
.groupby('index', groupby);
|
|
156
189
|
}
|
|
157
190
|
|
|
158
|
-
function binLinear2d(q, xp, yp,
|
|
159
|
-
const w =
|
|
191
|
+
function binLinear2d(q, xp, yp, density, xn, groupby) {
|
|
192
|
+
const w = density?.column ? `* ${density.column}` : '';
|
|
160
193
|
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
161
194
|
|
|
162
195
|
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
@@ -185,7 +218,7 @@ function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
|
185
218
|
|
|
186
219
|
return Query
|
|
187
220
|
.from(Query.unionAll(a, b, c, d))
|
|
188
|
-
.select({ index: 'i',
|
|
221
|
+
.select({ index: 'i', density: sum('w') }, groupby)
|
|
189
222
|
.groupby('index', groupby)
|
|
190
|
-
.having(
|
|
223
|
+
.having(neq('density', 0));
|
|
191
224
|
}
|