@uwdata/mosaic-plot 0.6.0 → 0.7.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/LICENSE +19 -0
- package/dist/mosaic-plot.js +6206 -5928
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +4 -4
- package/src/interactors/Interval1D.js +1 -1
- package/src/interactors/Interval2D.js +2 -2
- package/src/interactors/PanZoom.js +2 -2
- package/src/interactors/util/get-field.js +2 -2
- package/src/legend.js +4 -2
- package/src/marks/ConnectedMark.js +5 -8
- package/src/marks/DenseLineMark.js +11 -4
- package/src/marks/Grid2DMark.js +23 -3
- package/src/marks/Mark.js +45 -24
- package/src/marks/RasterMark.js +1 -1
- package/src/marks/RasterTileMark.js +1 -1
- package/src/marks/util/channel-scale.js +1 -2
- package/src/marks/util/density.js +26 -7
- package/src/marks/util/extent.js +3 -5
- package/src/marks/util/grid.js +50 -58
- package/src/marks/util/interpolate.js +205 -0
- package/src/marks/util/is-color.js +3 -0
- package/src/marks/util/to-data-array.js +2 -2
- package/src/plot-renderer.js +9 -13
- package/src/plot.js +7 -2
- package/src/transforms/bin.js +2 -2
- package/src/marks/util/arrow.js +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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.7.0",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.7.0",
|
|
34
34
|
"d3": "^7.8.5",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "4680b922f15579b7b527f31507ed71a12230ec35"
|
|
38
38
|
}
|
|
@@ -21,7 +21,7 @@ export class Interval1D {
|
|
|
21
21
|
this.pixelSize = pixelSize || 1;
|
|
22
22
|
this.selection = selection;
|
|
23
23
|
this.peers = peers;
|
|
24
|
-
this.field = field || getField(mark,
|
|
24
|
+
this.field = field || getField(mark, channel);
|
|
25
25
|
this.style = style && sanitizeStyles(style);
|
|
26
26
|
this.brush = channel === 'y' ? brushY() : brushX();
|
|
27
27
|
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
@@ -22,8 +22,8 @@ export class Interval2D {
|
|
|
22
22
|
this.pixelSize = pixelSize || 1;
|
|
23
23
|
this.selection = selection;
|
|
24
24
|
this.peers = peers;
|
|
25
|
-
this.xfield = xfield || getField(mark,
|
|
26
|
-
this.yfield = yfield || getField(mark,
|
|
25
|
+
this.xfield = xfield || getField(mark, 'x');
|
|
26
|
+
this.yfield = yfield || getField(mark, 'y');
|
|
27
27
|
this.style = style && sanitizeStyles(style);
|
|
28
28
|
this.brush = brush();
|
|
29
29
|
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
@@ -18,8 +18,8 @@ export class PanZoom {
|
|
|
18
18
|
this.mark = mark;
|
|
19
19
|
this.xsel = x;
|
|
20
20
|
this.ysel = y;
|
|
21
|
-
this.xfield = xfield || getField(mark,
|
|
22
|
-
this.yfield = yfield || getField(mark,
|
|
21
|
+
this.xfield = xfield || getField(mark, 'x');
|
|
22
|
+
this.yfield = yfield || getField(mark, 'y');
|
|
23
23
|
this.zoom = extent(zoom, [0, Infinity], [1, 1]);
|
|
24
24
|
this.panx = this.xsel && panx;
|
|
25
25
|
this.pany = this.ysel && pany;
|
package/src/legend.js
CHANGED
|
@@ -56,8 +56,10 @@ function findMark({ marks }, channel) {
|
|
|
56
56
|
: null;
|
|
57
57
|
if (channels == null) return null;
|
|
58
58
|
for (let i = marks.length - 1; i > -1; --i) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
for (const channel of channels) {
|
|
60
|
+
if (marks[i].channelField(channel, { exact: true })) {
|
|
61
|
+
return marks[i];
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
return null;
|
|
@@ -6,29 +6,26 @@ import { Mark } from './Mark.js';
|
|
|
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]: ['min', 'max'] };
|
|
9
|
+
const req = dim ? { [dim]: ['min', 'max'] } : undefined;
|
|
10
10
|
super(type, source, encodings, req);
|
|
11
11
|
this.dim = dim;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
query(filter = []) {
|
|
15
|
-
const { plot, dim, source
|
|
15
|
+
const { plot, dim, source } = this;
|
|
16
16
|
const { optimize = true } = source.options || {};
|
|
17
17
|
const q = super.query(filter);
|
|
18
18
|
if (!dim) return q;
|
|
19
19
|
|
|
20
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];
|
|
21
|
+
const value = this.channelField(ortho, { exact: true })?.as;
|
|
22
|
+
const { field, as, type, min, max } = this.channelField(dim);
|
|
24
23
|
const isContinuous = type === 'date' || type === 'number';
|
|
25
24
|
|
|
26
25
|
if (optimize && isContinuous && value) {
|
|
27
26
|
// TODO: handle stacked data!
|
|
28
|
-
const { column } = field;
|
|
29
|
-
const { max, min } = stats[column];
|
|
30
27
|
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
31
|
-
const [lo, hi] = filteredExtent(filter,
|
|
28
|
+
const [lo, hi] = filteredExtent(filter, field) || [min, max];
|
|
32
29
|
const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
|
|
33
30
|
const cols = q.select()
|
|
34
31
|
.map(c => c.as)
|
|
@@ -47,10 +47,17 @@ export class DenseLineMark extends RasterMark {
|
|
|
47
47
|
function stripXY(mark, filter) {
|
|
48
48
|
if (Array.isArray(filter) && !filter.length) return filter;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
50
|
+
// get column expressions for x and y encoding channels
|
|
51
|
+
const { column: xc } = mark.channelField('x');
|
|
52
|
+
const { column: yc } = mark.channelField('y');
|
|
53
|
+
|
|
54
|
+
// test if a range predicate filters the x or y channels
|
|
55
|
+
const test = p => {
|
|
56
|
+
const col = `${p.field}`;
|
|
57
|
+
return p.op !== 'BETWEEN' || col !== xc && col !== yc;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// filter boolean 'and' operations
|
|
54
61
|
const filterAnd = p => p.op === 'AND'
|
|
55
62
|
? and(p.children.filter(c => test(c)))
|
|
56
63
|
: p;
|
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -5,6 +5,9 @@ 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
|
|
|
10
13
|
export const DENSITY = 'density';
|
|
@@ -36,7 +39,7 @@ export class Grid2DMark extends Mark {
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
setPlot(plot, index) {
|
|
39
|
-
const update = () => { if (this.
|
|
42
|
+
const update = () => { if (this.hasFieldInfo()) this.requestUpdate(); };
|
|
40
43
|
plot.addAttributeListener('domainX', update);
|
|
41
44
|
plot.addAttributeListener('domainY', update);
|
|
42
45
|
return super.setPlot(plot, index);
|
|
@@ -121,8 +124,9 @@ export class Grid2DMark extends Mark {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
queryResult(data) {
|
|
124
|
-
const [
|
|
125
|
-
|
|
127
|
+
const [w, h] = this.bins;
|
|
128
|
+
const interp = maybeInterpolate(this.interpolate);
|
|
129
|
+
this.grids = grid2d(w, h, data, this.aggr, this.groupby, interp);
|
|
126
130
|
return this.convolve();
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -179,6 +183,22 @@ function createDensityMap(channels) {
|
|
|
179
183
|
return densityMap;
|
|
180
184
|
}
|
|
181
185
|
|
|
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}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
182
202
|
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
183
203
|
return q
|
|
184
204
|
.select({
|
package/src/marks/Mark.js
CHANGED
|
@@ -19,6 +19,8 @@ const fieldEntry = (channel, field) => ({
|
|
|
19
19
|
});
|
|
20
20
|
const valueEntry = (channel, value) => ({ channel, value });
|
|
21
21
|
|
|
22
|
+
// checks if a data source is an explicit array of values
|
|
23
|
+
// as opposed to a database table refernece
|
|
22
24
|
export const isDataArray = source => Array.isArray(source);
|
|
23
25
|
|
|
24
26
|
export class Mark extends MosaicClient {
|
|
@@ -94,45 +96,47 @@ export class Mark extends MosaicClient {
|
|
|
94
96
|
return this.source == null || isDataArray(this.source);
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
hasFieldInfo() {
|
|
100
|
+
return !!this._fieldInfo;
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
channel(channel) {
|
|
98
104
|
return this.channels.find(c => c.channel === channel);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
channelField(
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
107
|
+
channelField(channel, { exact } = {}) {
|
|
108
|
+
const c = exact
|
|
109
|
+
? this.channel(channel)
|
|
110
|
+
: this.channels.find(c => c.channel.startsWith(channel));
|
|
111
|
+
return c?.field ? c : null;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
fields() {
|
|
111
115
|
if (this.hasOwnData()) return null;
|
|
112
|
-
const { source: { table }, channels, reqs } = this;
|
|
113
116
|
|
|
117
|
+
const { source: { table }, channels, reqs } = this;
|
|
114
118
|
const fields = new Map;
|
|
115
119
|
for (const { channel, field } of channels) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
reqs[channel]?.forEach(s => entry.add(s));
|
|
123
|
-
field.stats?.forEach(s => entry.add(s));
|
|
124
|
-
}
|
|
120
|
+
if (!field) continue;
|
|
121
|
+
const stats = field.stats?.stats || [];
|
|
122
|
+
const key = field.stats?.column ?? field;
|
|
123
|
+
const entry = fields.get(key) ?? fields.set(key, new Set).get(key);
|
|
124
|
+
stats.forEach(s => entry.add(s));
|
|
125
|
+
reqs[channel]?.forEach(s => entry.add(s));
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
127
|
+
|
|
128
|
+
return Array.from(fields, ([c, s]) => ({ table, column: c, stats: s }));
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
fieldInfo(info) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
const lookup = Object.fromEntries(info.map(x => [x.column, x]));
|
|
133
|
+
for (const entry of this.channels) {
|
|
134
|
+
const { field } = entry;
|
|
135
|
+
if (field) {
|
|
136
|
+
Object.assign(entry, lookup[field.stats?.column ?? field]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this._fieldInfo = true;
|
|
136
140
|
return this;
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -169,6 +173,13 @@ export class Mark extends MosaicClient {
|
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Helper method for setting a channel option in a Plot specification.
|
|
178
|
+
* Checks if a constant value or a data field is needed.
|
|
179
|
+
* Also avoids misinterpretation of data values as color names.
|
|
180
|
+
* @param {*} c a visual encoding channel spec
|
|
181
|
+
* @returns the Plot channel option
|
|
182
|
+
*/
|
|
172
183
|
export function channelOption(c) {
|
|
173
184
|
// use a scale override for color channels to sidestep
|
|
174
185
|
// https://github.com/observablehq/plot/issues/1593
|
|
@@ -177,6 +188,16 @@ export function channelOption(c) {
|
|
|
177
188
|
: c.as;
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Default query construction for a mark.
|
|
193
|
+
* Tracks aggregates by checking fields for an aggregate flag.
|
|
194
|
+
* If aggregates are found, groups by all non-aggregate fields.
|
|
195
|
+
* @param {*} channels array of visual encoding channel specs.
|
|
196
|
+
* @param {*} table the table to query.
|
|
197
|
+
* @param {*} skip an optional array of channels to skip.
|
|
198
|
+
* Mark subclasses can skip channels that require special handling.
|
|
199
|
+
* @returns a Query instance
|
|
200
|
+
*/
|
|
180
201
|
export function markQuery(channels, table, skip = []) {
|
|
181
202
|
const q = Query.from({ source: table });
|
|
182
203
|
const dims = new Set;
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -23,7 +23,7 @@ export class RasterMark extends Grid2DMark {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
setPlot(plot, index) {
|
|
26
|
-
const update = () => { if (this.
|
|
26
|
+
const update = () => { if (this.hasFieldInfo()) this.rasterize(); };
|
|
27
27
|
plot.addAttributeListener('schemeColor', update);
|
|
28
28
|
super.setPlot(plot, index);
|
|
29
29
|
}
|
|
@@ -18,7 +18,7 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
setPlot(plot, index) {
|
|
21
|
-
const update = () => { if (this.
|
|
21
|
+
const update = () => { if (this.hasFieldInfo()) this.rasterize(); };
|
|
22
22
|
plot.addAttributeListener('schemeColor', update);
|
|
23
23
|
super.setPlot(plot, index);
|
|
24
24
|
}
|
|
@@ -5,8 +5,7 @@ export function channelScale(mark, channel) {
|
|
|
5
5
|
|
|
6
6
|
let scaleType = plot.getAttribute(`${channel}Scale`);
|
|
7
7
|
if (!scaleType) {
|
|
8
|
-
const {
|
|
9
|
-
const { type } = mark.stats[field.column];
|
|
8
|
+
const { type } = mark.channelField(channel);
|
|
10
9
|
scaleType = type === 'date' ? 'time' : 'linear';
|
|
11
10
|
}
|
|
12
11
|
|
|
@@ -96,7 +96,7 @@ export function dericheConv2d(cx, cy, grid, [nx, ny]) {
|
|
|
96
96
|
// allocate buffers
|
|
97
97
|
const yc = new Float64Array(Math.max(nx, ny)); // causal
|
|
98
98
|
const ya = new Float64Array(Math.max(nx, ny)); // anticausal
|
|
99
|
-
const h = new Float64Array(5);
|
|
99
|
+
const h = new Float64Array(5); // q + 1
|
|
100
100
|
const d = new Float64Array(grid.length);
|
|
101
101
|
|
|
102
102
|
// convolve rows
|
|
@@ -119,7 +119,7 @@ export function dericheConv1d(
|
|
|
119
119
|
stride = 1,
|
|
120
120
|
y_causal = new Float64Array(N),
|
|
121
121
|
y_anticausal = new Float64Array(N),
|
|
122
|
-
h = new Float64Array(5),
|
|
122
|
+
h = new Float64Array(5), // q + 1
|
|
123
123
|
d = y_causal,
|
|
124
124
|
init = dericheInitZeroPad
|
|
125
125
|
) {
|
|
@@ -153,6 +153,7 @@ export function dericheConv1d(
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// initialize the anticausal filter on the right boundary
|
|
156
|
+
// dest, src, N, stride, b, p, a, q, sum, h
|
|
156
157
|
init(
|
|
157
158
|
y_anticausal, src, N, -stride,
|
|
158
159
|
c.b_anticausal, 4, c.a, 4, c.sum_anticausal, h, c.sigma
|
|
@@ -176,7 +177,7 @@ export function dericheConv1d(
|
|
|
176
177
|
|
|
177
178
|
// sum the causal and anticausal responses to obtain the final result
|
|
178
179
|
if (c.negative) {
|
|
179
|
-
// do not threshold if the input grid includes
|
|
180
|
+
// do not threshold if the input grid includes negative values
|
|
180
181
|
for (n = 0, i = 0; n < N; ++n, i += stride) {
|
|
181
182
|
d[i] = y_causal[n] + y_anticausal[N - n - 1];
|
|
182
183
|
}
|
|
@@ -190,13 +191,16 @@ export function dericheConv1d(
|
|
|
190
191
|
return d;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
export function dericheInitZeroPad(
|
|
194
|
+
export function dericheInitZeroPad(
|
|
195
|
+
dest, src, N, stride, b, p, a, q,
|
|
196
|
+
sum, h, sigma, tol = 0.5
|
|
197
|
+
) {
|
|
194
198
|
const stride_N = Math.abs(stride) * N;
|
|
195
199
|
const off = stride < 0 ? stride_N + stride : 0;
|
|
196
200
|
let i, n, m;
|
|
197
201
|
|
|
198
202
|
// compute the first q taps of the impulse response, h_0, ..., h_{q-1}
|
|
199
|
-
for (n = 0; n
|
|
203
|
+
for (n = 0; n <= q; ++n) {
|
|
200
204
|
h[n] = (n <= p) ? b[n] : 0;
|
|
201
205
|
for (m = 1; m <= q && m <= n; ++m) {
|
|
202
206
|
h[n] -= a[m] * h[n - m];
|
|
@@ -214,12 +218,27 @@ export function dericheInitZeroPad(dest, src, N, stride, b, p, a, q, sum, h) {
|
|
|
214
218
|
}
|
|
215
219
|
}
|
|
216
220
|
|
|
217
|
-
// dest_m = dest_m + h_{n+m} src_{-n}
|
|
218
221
|
const cur = src[off];
|
|
219
|
-
|
|
222
|
+
const max_iter = Math.ceil(sigma * 10);
|
|
223
|
+
for (n = 0; n < max_iter; ++n) {
|
|
224
|
+
/* dest_m = dest_m + h_{n+m} src_{-n} */
|
|
220
225
|
for (m = 0; m < q; ++m) {
|
|
221
226
|
dest[m] += h[m] * cur;
|
|
222
227
|
}
|
|
228
|
+
|
|
229
|
+
sum -= Math.abs(h[0]);
|
|
230
|
+
if (sum <= tol) break;
|
|
231
|
+
|
|
232
|
+
/* Compute the next impulse response tap, h_{n+q} */
|
|
233
|
+
h[q] = (n + q <= p) ? b[n + q] : 0;
|
|
234
|
+
for (m = 1; m <= q; ++m) {
|
|
235
|
+
h[q] -= a[m] * h[q - m];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* Shift the h array for the next iteration */
|
|
239
|
+
for (m = 0; m < q; ++m) {
|
|
240
|
+
h[m] = h[m + 1];
|
|
241
|
+
}
|
|
223
242
|
}
|
|
224
243
|
|
|
225
244
|
return;
|
package/src/marks/util/extent.js
CHANGED
|
@@ -6,16 +6,14 @@ export const yext = { y: ['min', 'max'] };
|
|
|
6
6
|
export const xyext = { ...xext, ...yext };
|
|
7
7
|
|
|
8
8
|
export function plotExtent(mark, filter, channel, domainAttr, niceAttr) {
|
|
9
|
-
const { plot
|
|
9
|
+
const { plot } = mark;
|
|
10
10
|
const domain = plot.getAttribute(domainAttr);
|
|
11
11
|
const nice = plot.getAttribute(niceAttr);
|
|
12
12
|
|
|
13
13
|
if (Array.isArray(domain) && !domain[Transient]) {
|
|
14
14
|
return domain;
|
|
15
15
|
} else {
|
|
16
|
-
const {
|
|
17
|
-
const { column } = field;
|
|
18
|
-
const { min, max } = stats[column];
|
|
16
|
+
const { column, min, max } = mark.channelField(channel);
|
|
19
17
|
const dom = filteredExtent(filter, column) || (nice
|
|
20
18
|
? scaleLinear().domain([min, max]).nice().domain()
|
|
21
19
|
: [min, max]);
|
|
@@ -39,7 +37,7 @@ export function filteredExtent(filter, column) {
|
|
|
39
37
|
let lo;
|
|
40
38
|
let hi;
|
|
41
39
|
const visitor = (type, clause) => {
|
|
42
|
-
if (type === 'BETWEEN' && clause.field
|
|
40
|
+
if (type === 'BETWEEN' && `${clause.field}` === column) {
|
|
43
41
|
const { range } = clause;
|
|
44
42
|
if (range && (lo == null || range[0] < lo)) lo = range[0];
|
|
45
43
|
if (range && (hi == null || range[1] > hi)) hi = range[1];
|
package/src/marks/util/grid.js
CHANGED
|
@@ -1,48 +1,24 @@
|
|
|
1
1
|
import { InternSet, ascending } from 'd3';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
convertArrowArrayType,
|
|
4
|
+
convertArrowColumn,
|
|
5
|
+
isArrowTable
|
|
6
|
+
} from '@uwdata/mosaic-core';
|
|
3
7
|
|
|
4
8
|
function arrayType(values, name = 'density') {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
case INTEGER:
|
|
9
|
-
case FLOAT:
|
|
10
|
-
case DECIMAL:
|
|
11
|
-
return Float64Array;
|
|
12
|
-
default:
|
|
13
|
-
return Array;
|
|
14
|
-
}
|
|
15
|
-
} else {
|
|
16
|
-
return typeof values[0][name] === 'number' ? Float64Array : Array;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function grid1d(n, values) {
|
|
21
|
-
const Type = arrayType(values);
|
|
22
|
-
return valuesToGrid(new Type(n), values);
|
|
9
|
+
return isArrowTable(values)
|
|
10
|
+
? convertArrowArrayType(values.getChild(name).type)
|
|
11
|
+
: typeof values[0]?.[name] === 'number' ? Float64Array : Array;
|
|
23
12
|
}
|
|
24
13
|
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
// generate grids per group
|
|
28
|
-
return groupedValuesToGrids(m * n, values, aggr, groupby);
|
|
29
|
-
} else {
|
|
30
|
-
const cell = {};
|
|
31
|
-
aggr.forEach(name => {
|
|
32
|
-
const Type = arrayType(values, name);
|
|
33
|
-
cell[name] = valuesToGrid(new Type(m * n), values, name);
|
|
34
|
-
});
|
|
35
|
-
return [cell];
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function valuesToGrid(grid, values, name = 'density') {
|
|
14
|
+
export function grid1d(n, values, name = 'density') {
|
|
15
|
+
const grid = new (arrayType(values))(n);
|
|
40
16
|
if (isArrowTable(values)) {
|
|
41
17
|
// optimize access for Arrow tables
|
|
42
18
|
const numRows = values.numRows;
|
|
43
19
|
if (numRows === 0) return grid;
|
|
44
|
-
const index = values.getChild('index')
|
|
45
|
-
const value = values.getChild(name)
|
|
20
|
+
const index = convertArrowColumn(values.getChild('index'));
|
|
21
|
+
const value = convertArrowColumn(values.getChild(name));
|
|
46
22
|
for (let row = 0; row < numRows; ++row) {
|
|
47
23
|
grid[index[row]] = value[row];
|
|
48
24
|
}
|
|
@@ -55,30 +31,35 @@ function valuesToGrid(grid, values, name = 'density') {
|
|
|
55
31
|
return grid;
|
|
56
32
|
}
|
|
57
33
|
|
|
58
|
-
function
|
|
34
|
+
export function grid2d(w, h, values, aggr, groupby = [], interpolate) {
|
|
35
|
+
const size = w * h;
|
|
59
36
|
const Types = aggr.map(name => arrayType(values, name));
|
|
60
37
|
const numAggr = aggr.length;
|
|
61
38
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
groupby.forEach((name, i) => cell[name] = key[i]);
|
|
68
|
-
aggr.forEach((name, i) => cell[name] = new Types[i](size));
|
|
69
|
-
}
|
|
39
|
+
// grid data tuples
|
|
40
|
+
const createCell = (key) => {
|
|
41
|
+
const cell = {};
|
|
42
|
+
groupby.forEach((name, i) => cell[name] = key[i]);
|
|
43
|
+
aggr.forEach((name, i) => cell[name] = new Types[i](size));
|
|
70
44
|
return cell;
|
|
71
45
|
};
|
|
46
|
+
const cellMap = {};
|
|
47
|
+
const baseCell = groupby.length ? null : (cellMap[[]] = createCell([]));
|
|
48
|
+
const getCell = groupby.length
|
|
49
|
+
? key => cellMap[key] ?? (cellMap[key] = createCell(key))
|
|
50
|
+
: () => baseCell;
|
|
72
51
|
|
|
73
|
-
if
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (numRows === 0) return [];
|
|
52
|
+
// early exit if empty query result
|
|
53
|
+
const numRows = values.numRows;
|
|
54
|
+
if (numRows === 0) return Object.values(cellMap);
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
56
|
+
// extract arrays from arrow table
|
|
57
|
+
const index = convertArrowColumn(values.getChild('index'));
|
|
58
|
+
const value = aggr.map(name => convertArrowColumn(values.getChild(name)));
|
|
59
|
+
const groups = groupby.map(name => values.getChild(name));
|
|
81
60
|
|
|
61
|
+
if (!interpolate) {
|
|
62
|
+
// if no interpolation, copy values over
|
|
82
63
|
for (let row = 0; row < numRows; ++row) {
|
|
83
64
|
const key = groups.map(vec => vec.get(row));
|
|
84
65
|
const cell = getCell(key);
|
|
@@ -87,14 +68,25 @@ function groupedValuesToGrids(size, values, aggr, groupby) {
|
|
|
87
68
|
}
|
|
88
69
|
}
|
|
89
70
|
} else {
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (let
|
|
95
|
-
|
|
71
|
+
// prepare index arrays, then interpolate grid values
|
|
72
|
+
const X = index.map(k => k % w);
|
|
73
|
+
const Y = index.map(k => Math.floor(k / w));
|
|
74
|
+
if (groupby.length) {
|
|
75
|
+
for (let row = 0; row < numRows; ++row) {
|
|
76
|
+
const key = groups.map(vec => vec.get(row));
|
|
77
|
+
const cell = getCell(key);
|
|
78
|
+
if (!cell.index) { cell.index = []; }
|
|
79
|
+
cell.index.push(row);
|
|
96
80
|
}
|
|
81
|
+
} else {
|
|
82
|
+
baseCell.index = index.map((_, i) => i);
|
|
97
83
|
}
|
|
84
|
+
Object.values(cellMap).forEach(cell => {
|
|
85
|
+
for (let i = 0; i < numAggr; ++i) {
|
|
86
|
+
interpolate(cell.index, w, h, X, Y, value[i], cell[aggr[i]]);
|
|
87
|
+
}
|
|
88
|
+
delete cell.index;
|
|
89
|
+
})
|
|
98
90
|
}
|
|
99
91
|
|
|
100
92
|
return Object.values(cellMap);
|