@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
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { Query, and, count, isNull, isBetween, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
|
-
import { binField, bin1d } from './util/bin-field.js';
|
|
3
|
-
import { extentX, extentY } from './util/extent.js';
|
|
4
|
-
import { handleParam } from './util/handle-param.js';
|
|
5
|
-
import { RasterMark } from './RasterMark.js';
|
|
6
|
-
|
|
7
|
-
export class DenseLineMark extends RasterMark {
|
|
8
|
-
constructor(source, options) {
|
|
9
|
-
const { normalize = true, ...rest } = options;
|
|
10
|
-
super(source, { bandwidth: 0, ...rest });
|
|
11
|
-
handleParam(this, 'normalize', normalize);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
query(filter = []) {
|
|
15
|
-
const { plot, channels, normalize, source } = this;
|
|
16
|
-
const [x0, x1] = extentX(this, filter);
|
|
17
|
-
const [y0, y1] = extentY(this, filter);
|
|
18
|
-
const [nx, ny] = this.bins = this.binDimensions(this);
|
|
19
|
-
const bx = binField(this, 'x');
|
|
20
|
-
const by = binField(this, 'y');
|
|
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);
|
|
25
|
-
|
|
26
|
-
const q = Query
|
|
27
|
-
.from(source.table)
|
|
28
|
-
.where(stripXY(this, filter));
|
|
29
|
-
|
|
30
|
-
const groupby = this.groupby = [];
|
|
31
|
-
const z = [];
|
|
32
|
-
for (const c of channels) {
|
|
33
|
-
if (Object.hasOwn(c, 'field')) {
|
|
34
|
-
const { channel, field } = c;
|
|
35
|
-
if (channel === 'z') {
|
|
36
|
-
q.select({ [channel]: field });
|
|
37
|
-
z.push('z');
|
|
38
|
-
} else if (channel !== 'x' && channel !== 'y') {
|
|
39
|
-
q.select({ [channel]: field });
|
|
40
|
-
groupby.push(channel);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return lineDensity(q, x, y, z, nx, ny, groupby, normalize);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// strip x, y fields from filter predicate
|
|
50
|
-
// to prevent improper clipping of line segments
|
|
51
|
-
// TODO: improve, perhaps with supporting query utilities
|
|
52
|
-
function stripXY(mark, filter) {
|
|
53
|
-
if (Array.isArray(filter) && !filter.length) return filter;
|
|
54
|
-
|
|
55
|
-
const xc = mark.channelField('x').field.column;
|
|
56
|
-
const yc = mark.channelField('y').field.column;
|
|
57
|
-
const test = p => p.op !== 'BETWEEN'
|
|
58
|
-
|| p.field.column !== xc && p.field.column !== yc;
|
|
59
|
-
const filterAnd = p => p.op === 'AND'
|
|
60
|
-
? and(p.children.filter(c => test(c)))
|
|
61
|
-
: p;
|
|
62
|
-
|
|
63
|
-
return Array.isArray(filter)
|
|
64
|
-
? filter.filter(p => test(p)).map(p => filterAnd(p))
|
|
65
|
-
: filterAnd(filter);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function lineDensity(
|
|
69
|
-
q, x, y, z, xn, yn,
|
|
70
|
-
groupby = [], normalize = true
|
|
71
|
-
) {
|
|
72
|
-
// select x, y points binned to the grid
|
|
73
|
-
q.select({
|
|
74
|
-
x: sql`FLOOR(${x})::INTEGER`,
|
|
75
|
-
y: sql`FLOOR(${y})::INTEGER`
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// select line segment end point pairs
|
|
79
|
-
const groups = groupby.concat(z);
|
|
80
|
-
const pairPart = groups.length ? `PARTITION BY ${groups.join(', ')} ` : '';
|
|
81
|
-
const pairs = Query
|
|
82
|
-
.from(q)
|
|
83
|
-
.select(groups, {
|
|
84
|
-
x0: 'x',
|
|
85
|
-
y0: 'y',
|
|
86
|
-
dx: sql`(lead(x) OVER sw - x)`,
|
|
87
|
-
dy: sql`(lead(y) OVER sw - y)`
|
|
88
|
-
})
|
|
89
|
-
.window({ sw: sql`${pairPart}ORDER BY x ASC` })
|
|
90
|
-
.qualify(and(
|
|
91
|
-
sql`(x0 < ${xn} OR x0 + dx < ${xn})`,
|
|
92
|
-
sql`(y0 < ${yn} OR y0 + dy < ${yn})`,
|
|
93
|
-
sql`(x0 > 0 OR x0 + dx > 0)`,
|
|
94
|
-
sql`(y0 > 0 OR y0 + dy > 0)`
|
|
95
|
-
));
|
|
96
|
-
|
|
97
|
-
// indices to join against for rasterization
|
|
98
|
-
// generate the maximum number of indices needed
|
|
99
|
-
const num = Query
|
|
100
|
-
.select({ x: sql`GREATEST(MAX(ABS(dx)), MAX(ABS(dy)))` })
|
|
101
|
-
.from('pairs');
|
|
102
|
-
const indices = Query.select({ i: sql`UNNEST(range((${num})))::INTEGER` });
|
|
103
|
-
|
|
104
|
-
// rasterize line segments
|
|
105
|
-
const raster = Query.unionAll(
|
|
106
|
-
Query
|
|
107
|
-
.select(groups, {
|
|
108
|
-
x: sql`x0 + i`,
|
|
109
|
-
y: sql`y0 + ROUND(i * dy / dx::FLOAT)::INTEGER`
|
|
110
|
-
})
|
|
111
|
-
.from('pairs', 'indices')
|
|
112
|
-
.where(sql`ABS(dy) <= ABS(dx) AND i < ABS(dx)`),
|
|
113
|
-
Query
|
|
114
|
-
.select(groups, {
|
|
115
|
-
x: sql`x0 + ROUND(SIGN(dy) * i * dx / dy::FLOAT)::INTEGER`,
|
|
116
|
-
y: sql`y0 + SIGN(dy) * i`
|
|
117
|
-
})
|
|
118
|
-
.from('pairs', 'indices')
|
|
119
|
-
.where(sql`ABS(dy) > ABS(dx) AND i < ABS(dy)`),
|
|
120
|
-
Query
|
|
121
|
-
.select(groups, { x: 'x0', y: 'y0' })
|
|
122
|
-
.from('pairs')
|
|
123
|
-
.where(isNull('dx'))
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// filter raster, normalize columns for each series
|
|
127
|
-
const pointPart = ['x'].concat(groups).join(', ');
|
|
128
|
-
const points = Query
|
|
129
|
-
.from('raster')
|
|
130
|
-
.select(groups, 'x', 'y',
|
|
131
|
-
normalize
|
|
132
|
-
? { w: sql`1.0 / COUNT(*) OVER (PARTITION BY ${pointPart})` }
|
|
133
|
-
: null
|
|
134
|
-
)
|
|
135
|
-
.where(and(isBetween('x', [0, xn]), isBetween('y', [0, yn])));
|
|
136
|
-
|
|
137
|
-
// sum normalized, rasterized series into output grids
|
|
138
|
-
return Query
|
|
139
|
-
.with({ pairs, indices, raster, points })
|
|
140
|
-
.from('points')
|
|
141
|
-
.select(groupby, {
|
|
142
|
-
index: sql`x + y * ${xn}::INTEGER`,
|
|
143
|
-
value: normalize ? sum('w') : count()
|
|
144
|
-
})
|
|
145
|
-
.groupby('index', groupby);
|
|
146
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
|
-
import { Transient } from '../symbols.js';
|
|
3
|
-
import { binField, bin1d } from './util/bin-field.js';
|
|
4
|
-
import { dericheConfig, dericheConv1d } from './util/density.js';
|
|
5
|
-
import { extentX, extentY, xext, yext } from './util/extent.js';
|
|
6
|
-
import { grid1d } from './util/grid.js';
|
|
7
|
-
import { handleParam } from './util/handle-param.js';
|
|
8
|
-
import { Mark, channelOption, markQuery } from './Mark.js';
|
|
9
|
-
|
|
10
|
-
export class Density1DMark extends Mark {
|
|
11
|
-
constructor(type, source, options) {
|
|
12
|
-
const { bins = 1024, bandwidth = 20, ...channels } = options;
|
|
13
|
-
const dim = type.endsWith('X') ? 'y' : 'x';
|
|
14
|
-
|
|
15
|
-
super(type, source, channels, dim === 'x' ? xext : yext);
|
|
16
|
-
this.dim = dim;
|
|
17
|
-
|
|
18
|
-
handleParam(this, 'bins', bins);
|
|
19
|
-
handleParam(this, 'bandwidth', bandwidth, () => {
|
|
20
|
-
return this.grid ? this.convolve().update() : null
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
get filterIndexable() {
|
|
25
|
-
const name = this.dim === 'x' ? 'xDomain' : 'yDomain';
|
|
26
|
-
const dom = this.plot.getAttribute(name);
|
|
27
|
-
return dom && !dom[Transient];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
query(filter = []) {
|
|
31
|
-
if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
|
|
32
|
-
const { bins, channels, dim, source: { table } } = this;
|
|
33
|
-
const [lo, hi] = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
|
|
34
|
-
const bx = binField(this, dim);
|
|
35
|
-
return binLinear1d(
|
|
36
|
-
markQuery(channels, table, [dim])
|
|
37
|
-
.where(filter.concat(isBetween(bx, [lo, hi]))),
|
|
38
|
-
bin1d(bx, lo, hi, bins),
|
|
39
|
-
this.channelField('weight') ? 'weight' : null
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
queryResult(data) {
|
|
44
|
-
this.grid = grid1d(this.bins, data);
|
|
45
|
-
return this.convolve();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
convolve() {
|
|
49
|
-
const { bins, bandwidth, dim, grid, plot, extent: [lo, hi] } = this;
|
|
50
|
-
|
|
51
|
-
// perform smoothing
|
|
52
|
-
const neg = grid.some(v => v < 0);
|
|
53
|
-
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
54
|
-
const config = dericheConfig(bandwidth * (bins - 1) / size, neg);
|
|
55
|
-
const result = dericheConv1d(config, grid, bins);
|
|
56
|
-
|
|
57
|
-
// map smoothed grid values to sample data points
|
|
58
|
-
const points = this.data = [];
|
|
59
|
-
const v = dim === 'x' ? 'y' : 'x';
|
|
60
|
-
const b = this.channelField(dim).as;
|
|
61
|
-
const b0 = +lo;
|
|
62
|
-
const delta = (hi - b0) / (bins - 1);
|
|
63
|
-
const scale = 1 / delta;
|
|
64
|
-
for (let i = 0; i < bins; ++i) {
|
|
65
|
-
points.push({
|
|
66
|
-
[b]: b0 + i * delta,
|
|
67
|
-
[v]: result[i] * scale
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return this;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
plotSpecs() {
|
|
75
|
-
const { type, data, channels, dim } = this;
|
|
76
|
-
const options = dim === 'x' ? { y: 'y' } : { x: 'x' };
|
|
77
|
-
for (const c of channels) {
|
|
78
|
-
options[c.channel] = channelOption(c);
|
|
79
|
-
}
|
|
80
|
-
return [{ type, data, options }];
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function binLinear1d(q, p, value) {
|
|
85
|
-
const w = value ? `* ${value}` : '';
|
|
86
|
-
|
|
87
|
-
const u = q.clone().select({
|
|
88
|
-
p,
|
|
89
|
-
i: sql`FLOOR(p)::INTEGER`,
|
|
90
|
-
w: sql`(FLOOR(p) + 1 - p)${w}`
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const v = q.clone().select({
|
|
94
|
-
p,
|
|
95
|
-
i: sql`FLOOR(p)::INTEGER + 1`,
|
|
96
|
-
w: sql`(p - FLOOR(p))${w}`
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return Query
|
|
100
|
-
.from(Query.unionAll(u, v))
|
|
101
|
-
.select({ index: 'i', value: sum('w') })
|
|
102
|
-
.groupby('index')
|
|
103
|
-
.having(gt('value', 0));
|
|
104
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { handleParam } from './util/handle-param.js';
|
|
2
|
-
import { Grid2DMark } from './Grid2DMark.js';
|
|
3
|
-
import { channelOption } from './Mark.js';
|
|
4
|
-
|
|
5
|
-
export class Density2DMark extends Grid2DMark {
|
|
6
|
-
constructor(source, options) {
|
|
7
|
-
const { type = 'dot', binsX, binsY, ...channels } = options;
|
|
8
|
-
channels.binPad = channels.binPad ?? 0;
|
|
9
|
-
super(type, source, channels);
|
|
10
|
-
handleParam(this, 'binsX', binsX);
|
|
11
|
-
handleParam(this, 'binsY', binsY);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
convolve() {
|
|
15
|
-
super.convolve();
|
|
16
|
-
const { bins, binPad, extentX, extentY } = this;
|
|
17
|
-
const [nx, ny] = bins;
|
|
18
|
-
const [x0, x1] = extentX;
|
|
19
|
-
const [y0, y1] = extentY;
|
|
20
|
-
const deltaX = (x1 - x0) / (nx - binPad);
|
|
21
|
-
const deltaY = (y1 - y0) / (ny - binPad);
|
|
22
|
-
const offset = binPad ? 0 : 0.5;
|
|
23
|
-
this.data = points(this.kde, bins, x0, y0, deltaX, deltaY, offset);
|
|
24
|
-
return this;
|
|
25
|
-
}
|
|
26
|
-
|
|
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
|
-
plotSpecs() {
|
|
36
|
-
const { type, channels, densityMap, data } = this;
|
|
37
|
-
const options = {};
|
|
38
|
-
for (const c of channels) {
|
|
39
|
-
const { channel } = c;
|
|
40
|
-
options[channel] = (channel === 'x' || channel === 'y')
|
|
41
|
-
? channel // use generated x/y data fields
|
|
42
|
-
: channelOption(c);
|
|
43
|
-
}
|
|
44
|
-
for (const channel in densityMap) {
|
|
45
|
-
if (densityMap[channel]) {
|
|
46
|
-
options[channel] = 'density';
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return [{ type, data, options }];
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function points(kde, bins, x0, y0, deltaX, deltaY, offset) {
|
|
54
|
-
const scale = 1 / (deltaX * deltaY);
|
|
55
|
-
const [nx, ny] = bins;
|
|
56
|
-
const data = [];
|
|
57
|
-
for (const grid of kde) {
|
|
58
|
-
for (let k = 0, j = 0; j < ny; ++j) {
|
|
59
|
-
for (let i = 0; i < nx; ++i, ++k) {
|
|
60
|
-
data.push({
|
|
61
|
-
x: x0 + (i + offset) * deltaX,
|
|
62
|
-
y: y0 + (j + offset) * deltaY,
|
|
63
|
-
density: grid[k] * scale
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return data;
|
|
69
|
-
}
|
package/src/marks/Grid2DMark.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { Query, count, gt, isBetween, lt, lte, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
|
-
import { Transient } from '../symbols.js';
|
|
3
|
-
import { binField } from './util/bin-field.js';
|
|
4
|
-
import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
5
|
-
import { extentX, extentY, xyext } from './util/extent.js';
|
|
6
|
-
import { grid2d } from './util/grid.js';
|
|
7
|
-
import { handleParam } from './util/handle-param.js';
|
|
8
|
-
import { Mark } from './Mark.js';
|
|
9
|
-
|
|
10
|
-
export class Grid2DMark extends Mark {
|
|
11
|
-
constructor(type, source, options) {
|
|
12
|
-
const {
|
|
13
|
-
bandwidth = 20,
|
|
14
|
-
binType = 'linear',
|
|
15
|
-
binWidth = 2,
|
|
16
|
-
binPad = 1,
|
|
17
|
-
...channels
|
|
18
|
-
} = options;
|
|
19
|
-
|
|
20
|
-
const densityMap = createDensityMap(channels);
|
|
21
|
-
super(type, source, channels, xyext);
|
|
22
|
-
this.densityMap = densityMap;
|
|
23
|
-
|
|
24
|
-
handleParam(this, 'bandwidth', bandwidth, () => {
|
|
25
|
-
return this.grids ? this.convolve().update() : null;
|
|
26
|
-
});
|
|
27
|
-
handleParam(this, 'binWidth', binWidth);
|
|
28
|
-
handleParam(this, 'binType', binType);
|
|
29
|
-
handleParam(this, 'binPad', binPad);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setPlot(plot, index) {
|
|
33
|
-
const update = () => { if (this.stats) this.requestUpdate(); };
|
|
34
|
-
plot.addAttributeListener('domainX', update);
|
|
35
|
-
plot.addAttributeListener('domainY', update);
|
|
36
|
-
return super.setPlot(plot, index);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get filterIndexable() {
|
|
40
|
-
const xdom = this.plot.getAttribute('xDomain');
|
|
41
|
-
const ydom = this.plot.getAttribute('yDomain');
|
|
42
|
-
return xdom && ydom && !xdom[Transient] && !ydom[Transient];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
query(filter = []) {
|
|
46
|
-
const { plot, binType, binPad, channels, densityMap, source } = this;
|
|
47
|
-
const [x0, x1] = this.extentX = extentX(this, filter);
|
|
48
|
-
const [y0, y1] = this.extentY = extentY(this, filter);
|
|
49
|
-
const [nx, ny] = this.bins = this.binDimensions(this);
|
|
50
|
-
const bx = binField(this, 'x');
|
|
51
|
-
const by = binField(this, 'y');
|
|
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
|
-
|
|
57
|
-
// with padded bins, include the entire domain extent
|
|
58
|
-
// if the bins are flush, exclude the extent max
|
|
59
|
-
const bounds = binPad
|
|
60
|
-
? [isBetween(bx, [x0, x1]), isBetween(by, [y0, y1])]
|
|
61
|
-
: [lte(x0, bx), lt(bx, x1), lte(y0, by), lt(by, y1)];
|
|
62
|
-
|
|
63
|
-
const q = Query
|
|
64
|
-
.from(source.table)
|
|
65
|
-
.where(filter.concat(bounds));
|
|
66
|
-
|
|
67
|
-
const groupby = this.groupby = [];
|
|
68
|
-
let agg = count();
|
|
69
|
-
for (const c of channels) {
|
|
70
|
-
if (Object.hasOwn(c, 'field')) {
|
|
71
|
-
const { as, channel, field } = c;
|
|
72
|
-
if (field.aggregate) {
|
|
73
|
-
agg = field;
|
|
74
|
-
densityMap[channel] = true;
|
|
75
|
-
} else if (channel === 'weight') {
|
|
76
|
-
agg = sum(field);
|
|
77
|
-
} else if (channel !== 'x' && channel !== 'y') {
|
|
78
|
-
q.select({ [as]: field });
|
|
79
|
-
groupby.push(as);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return binType === 'linear'
|
|
85
|
-
? binLinear2d(q, x, y, agg, nx, groupby)
|
|
86
|
-
: bin2d(q, x, y, agg, nx, groupby);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
binDimensions() {
|
|
90
|
-
const { plot, binWidth } = this;
|
|
91
|
-
return [
|
|
92
|
-
Math.round(plot.innerWidth() / binWidth),
|
|
93
|
-
Math.round(plot.innerHeight() / binWidth)
|
|
94
|
-
];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
queryResult(data) {
|
|
98
|
-
const [nx, ny] = this.bins;
|
|
99
|
-
this.grids = grid2d(nx, ny, data, this.groupby);
|
|
100
|
-
return this.convolve();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
convolve() {
|
|
104
|
-
const { bandwidth, bins, grids, plot } = this;
|
|
105
|
-
|
|
106
|
-
if (bandwidth <= 0) {
|
|
107
|
-
this.kde = this.grids.map(({ key, grid }) => {
|
|
108
|
-
return (grid.key = key, grid);
|
|
109
|
-
});
|
|
110
|
-
} else {
|
|
111
|
-
const w = plot.innerWidth();
|
|
112
|
-
const h = plot.innerHeight();
|
|
113
|
-
const [nx, ny] = bins;
|
|
114
|
-
const neg = grids.some(({ grid }) => grid.some(v => v < 0));
|
|
115
|
-
const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
|
|
116
|
-
const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
|
|
117
|
-
this.kde = this.grids.map(({ key, grid }) => {
|
|
118
|
-
const k = dericheConv2d(configX, configY, grid, bins);
|
|
119
|
-
return (k.key = key, k);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
return this;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
plotSpecs() {
|
|
126
|
-
throw new Error('Unimplemented. Use a Grid2D mark subclass.');
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function createDensityMap(channels) {
|
|
131
|
-
const densityMap = {};
|
|
132
|
-
for (const key in channels) {
|
|
133
|
-
if (channels[key] === 'density') {
|
|
134
|
-
delete channels[key];
|
|
135
|
-
densityMap[key] = true;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return densityMap;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function bin1d(x, x0, x1, n, reverse, pad) {
|
|
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) {
|
|
150
|
-
return q
|
|
151
|
-
.select({
|
|
152
|
-
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
153
|
-
value
|
|
154
|
-
})
|
|
155
|
-
.groupby('index', groupby);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function binLinear2d(q, xp, yp, value, xn, groupby) {
|
|
159
|
-
const w = value.column ? `* ${value.column}` : '';
|
|
160
|
-
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
161
|
-
|
|
162
|
-
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
163
|
-
const a = subq(
|
|
164
|
-
sql`FLOOR(xp)::INTEGER + FLOOR(yp)::INTEGER * ${xn}`,
|
|
165
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// grid[xu + yv * xn] += (xv - xp) * (yp - yu) * wi;
|
|
169
|
-
const b = subq(
|
|
170
|
-
sql`FLOOR(xp)::INTEGER + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
171
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
// grid[xv + yu * xn] += (xp - xu) * (yv - yp) * wi;
|
|
175
|
-
const c = subq(
|
|
176
|
-
sql`FLOOR(xp)::INTEGER + 1 + FLOOR(yp)::INTEGER * ${xn}`,
|
|
177
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
// grid[xv + yv * xn] += (xp - xu) * (yp - yu) * wi;
|
|
181
|
-
const d = subq(
|
|
182
|
-
sql`FLOOR(xp)::INTEGER + 1 + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
183
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
return Query
|
|
187
|
-
.from(Query.unionAll(a, b, c, d))
|
|
188
|
-
.select({ index: 'i', value: sum('w') }, groupby)
|
|
189
|
-
.groupby('index', groupby)
|
|
190
|
-
.having(gt('value', 0));
|
|
191
|
-
}
|
package/src/marks/HexbinMark.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { Query, isNotNull, sql } from '@uwdata/mosaic-sql';
|
|
2
|
-
import { Transient } from '../symbols.js';
|
|
3
|
-
import { extentX, extentY, xyext } from './util/extent.js';
|
|
4
|
-
import { Mark } from './Mark.js';
|
|
5
|
-
|
|
6
|
-
export class HexbinMark extends Mark {
|
|
7
|
-
constructor(source, options) {
|
|
8
|
-
const { type = 'hexagon', binWidth = 20, ...channels } = options;
|
|
9
|
-
super(type, source, { r: binWidth / 2, clip: true, ...channels }, xyext);
|
|
10
|
-
this.binWidth = binWidth;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
get filterIndexable() {
|
|
14
|
-
const xdom = this.plot.getAttribute('xDomain');
|
|
15
|
-
const ydom = this.plot.getAttribute('yDomain');
|
|
16
|
-
return xdom && ydom && !xdom[Transient] && !ydom[Transient];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
query(filter = []) {
|
|
20
|
-
if (this.hasOwnData()) return null;
|
|
21
|
-
const { plot, binWidth, channels, source } = this;
|
|
22
|
-
|
|
23
|
-
// get x / y extents, may update plot domainX / domainY
|
|
24
|
-
const [x1, x2] = extentX(this, filter);
|
|
25
|
-
const [y1, y2] = extentY(this, filter);
|
|
26
|
-
|
|
27
|
-
// Adjust screen-space coordinates by top/left
|
|
28
|
-
// margins as this is what Observable Plot does.
|
|
29
|
-
// TODO use zero margins when faceted?
|
|
30
|
-
const ox = 0.5 - plot.getAttribute('marginLeft');
|
|
31
|
-
const oy = 0 - plot.getAttribute('marginTop');
|
|
32
|
-
const dx = `${binWidth}::DOUBLE`;
|
|
33
|
-
const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
|
|
34
|
-
const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
|
|
35
|
-
const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
|
|
36
|
-
|
|
37
|
-
// Extract channel information, update top-level query
|
|
38
|
-
// and extract dependent columns for aggregates
|
|
39
|
-
let x, y;
|
|
40
|
-
const aggr = new Set;
|
|
41
|
-
const cols = {};
|
|
42
|
-
for (const c of channels) {
|
|
43
|
-
if (c.channel === 'orderby') {
|
|
44
|
-
q.orderby(c.value); // TODO revisit once groupby is added
|
|
45
|
-
} else if (c.channel === 'x') {
|
|
46
|
-
x = c;
|
|
47
|
-
} else if (c.channel === 'y') {
|
|
48
|
-
y = c;
|
|
49
|
-
} else if (Object.hasOwn(c, 'field')) {
|
|
50
|
-
cols[c.as] = c.field;
|
|
51
|
-
if (c.field.aggregate) {
|
|
52
|
-
c.field.columns.forEach(col => aggr.add(col));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Top-level query; we add a hex binning subquery below
|
|
58
|
-
// Maps binned screen space coordinates back to data
|
|
59
|
-
// values to ensure we get correct data-driven scales
|
|
60
|
-
const q = Query.select({
|
|
61
|
-
[x.as]: sql`${x1}::DOUBLE + ((x + 0.5 * (y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
|
|
62
|
-
[y.as]: sql`${y2}::DOUBLE - (y * ${dy} + ${oy})::DOUBLE / ${yr}`,
|
|
63
|
-
...cols
|
|
64
|
-
}).groupby('x', 'y');
|
|
65
|
-
|
|
66
|
-
// Map x/y channels to screen space
|
|
67
|
-
const xx = `${xr} * (${x.field} - ${x1}::DOUBLE)`;
|
|
68
|
-
const yy = `${yr} * (${y2}::DOUBLE - ${y.field})`;
|
|
69
|
-
|
|
70
|
-
// Perform hex binning of x/y coordinates
|
|
71
|
-
// TODO add groupby dims
|
|
72
|
-
const hex = Query
|
|
73
|
-
.select({
|
|
74
|
-
py: sql`(${yy} - ${oy}) / ${dy}`,
|
|
75
|
-
pj: sql`ROUND(py)::INTEGER`,
|
|
76
|
-
px: sql`(${xx} - ${ox}) / ${dx} - 0.5 * (pj & 1)`,
|
|
77
|
-
pi: sql`ROUND(px)::INTEGER`,
|
|
78
|
-
tt: sql`ABS(py-pj) * 3 > 1 AND (px-pi)**2 + (py-pj)**2 > (px - pi - 0.5 * CASE WHEN px < pi THEN -1 ELSE 1 END)**2 + (py - pj - CASE WHEN py < pj THEN -1 ELSE 1 END)**2`,
|
|
79
|
-
x: sql`CASE WHEN tt THEN (pi + (CASE WHEN px < pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE pi END`,
|
|
80
|
-
y: sql`CASE WHEN tt THEN (pj + CASE WHEN py < pj THEN -1 ELSE 1 END)::INTEGER ELSE pj END`
|
|
81
|
-
})
|
|
82
|
-
.select(Array.from(aggr))
|
|
83
|
-
.from(source.table)
|
|
84
|
-
.where(isNotNull(x.field), isNotNull(y.field), filter)
|
|
85
|
-
|
|
86
|
-
return q.from(hex);
|
|
87
|
-
}
|
|
88
|
-
}
|