@uwdata/mosaic-plot 0.10.0 → 0.12.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 +9240 -16968
- package/dist/mosaic-plot.min.js +15 -24
- package/package.json +6 -6
- package/src/index.js +2 -1
- package/src/interactors/Highlight.js +9 -7
- package/src/interactors/Interval1D.js +4 -11
- package/src/interactors/Interval2D.js +8 -15
- package/src/interactors/Nearest.js +50 -18
- package/src/interactors/Region.js +108 -0
- package/src/interactors/Toggle.js +6 -23
- package/src/interactors/util/brush.js +35 -3
- package/src/interactors/util/get-datum.js +15 -0
- package/src/interactors/util/get-field.js +37 -2
- package/src/interactors/util/intersect.js +267 -0
- package/src/interactors/util/neq.js +14 -0
- package/src/interactors/util/parse-path.js +79 -0
- package/src/marks/ConnectedMark.js +10 -33
- package/src/marks/DenseLineMark.js +8 -88
- package/src/marks/Density1DMark.js +4 -26
- package/src/marks/ErrorBarMark.js +7 -8
- package/src/marks/Grid2DMark.js +11 -52
- package/src/marks/HexbinMark.js +53 -23
- package/src/marks/Mark.js +29 -26
- package/src/marks/RasterMark.js +1 -0
- package/src/marks/RasterTileMark.js +2 -2
- package/src/marks/RegressionMark.js +3 -3
- package/src/marks/util/bin-expr.js +4 -9
- package/src/marks/util/extent.js +9 -12
- package/src/marks/util/stats.js +1 -1
- package/src/plot-renderer.js +23 -48
- package/src/plot.js +1 -1
- package/src/transforms/bin.js +49 -38
- package/src/transforms/time-interval.js +1 -1
- package/src/transforms/index.js +0 -3
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk } from '@observablehq/plot';
|
|
2
2
|
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
3
|
-
import { Query, count, isBetween, lt, lte,
|
|
3
|
+
import { ExprNode, Query, bin2d, binLinear2d, collectColumns, count, isAggregateExpression, isBetween, lt, lte, sum } from '@uwdata/mosaic-sql';
|
|
4
4
|
import { Transient } from '../symbols.js';
|
|
5
5
|
import { binExpr } from './util/bin-expr.js';
|
|
6
6
|
import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
@@ -70,14 +70,14 @@ export class Grid2DMark extends Mark {
|
|
|
70
70
|
super.setPlot(plot, index);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
get
|
|
73
|
+
get filterStable() {
|
|
74
74
|
const xdom = this.plot.getAttribute('xDomain');
|
|
75
75
|
const ydom = this.plot.getAttribute('yDomain');
|
|
76
76
|
return xdom && ydom && !xdom[Transient] && !ydom[Transient];
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
query(filter = []) {
|
|
80
|
-
const { interpolate, pad, channels, densityMap
|
|
80
|
+
const { interpolate, pad, channels, densityMap } = this;
|
|
81
81
|
const [x0, x1] = this.extentX = extentX(this, filter);
|
|
82
82
|
const [y0, y1] = this.extentY = extentY(this, filter);
|
|
83
83
|
const [nx, ny] = this.bins = this.binDimensions();
|
|
@@ -91,15 +91,17 @@ export class Grid2DMark extends Mark {
|
|
|
91
91
|
: [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
|
|
92
92
|
|
|
93
93
|
const q = Query
|
|
94
|
-
.from(
|
|
94
|
+
.from(this.sourceTable())
|
|
95
95
|
.where(filter.concat(bounds));
|
|
96
96
|
|
|
97
|
+
/** @type {string[]} */
|
|
97
98
|
const groupby = this.groupby = [];
|
|
99
|
+
/** @type {Record<string, ExprNode>} */
|
|
98
100
|
const aggrMap = {};
|
|
99
101
|
for (const c of channels) {
|
|
100
102
|
if (Object.hasOwn(c, 'field')) {
|
|
101
103
|
const { as, channel, field } = c;
|
|
102
|
-
if (field
|
|
104
|
+
if (isAggregateExpression(field)) {
|
|
103
105
|
// include custom aggregate
|
|
104
106
|
aggrMap[channel] = field;
|
|
105
107
|
densityMap[channel] = true;
|
|
@@ -123,7 +125,7 @@ export class Grid2DMark extends Mark {
|
|
|
123
125
|
// if no aggregates, default to count density
|
|
124
126
|
if (!aggr.length) {
|
|
125
127
|
aggr.push(DENSITY);
|
|
126
|
-
aggrMap
|
|
128
|
+
aggrMap[DENSITY] = count();
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
// generate grid binning query
|
|
@@ -131,10 +133,11 @@ export class Grid2DMark extends Mark {
|
|
|
131
133
|
if (aggr.length > 1) {
|
|
132
134
|
throw new Error('Linear binning not applicable to multiple aggregates.');
|
|
133
135
|
}
|
|
134
|
-
if (!aggrMap
|
|
136
|
+
if (!aggrMap[DENSITY]) {
|
|
135
137
|
throw new Error('Linear binning not applicable to custom aggregates.');
|
|
136
138
|
}
|
|
137
|
-
|
|
139
|
+
const weight = collectColumns(aggrMap[DENSITY])[0];
|
|
140
|
+
return binLinear2d(q, x, y, weight, nx, groupby);
|
|
138
141
|
} else {
|
|
139
142
|
return bin2d(q, x, y, aggrMap, nx, groupby);
|
|
140
143
|
}
|
|
@@ -228,47 +231,3 @@ function maybeInterpolate(interpolate = 'none') {
|
|
|
228
231
|
}
|
|
229
232
|
throw new Error(`invalid interpolate: ${interpolate}`);
|
|
230
233
|
}
|
|
231
|
-
|
|
232
|
-
function bin2d(q, xp, yp, aggs, xn, groupby) {
|
|
233
|
-
return q
|
|
234
|
-
.select({
|
|
235
|
-
index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
|
|
236
|
-
...aggs
|
|
237
|
-
})
|
|
238
|
-
.groupby('index', groupby);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function binLinear2d(q, xp, yp, density, xn, groupby) {
|
|
242
|
-
const w = density?.column ? `* ${density.column}` : '';
|
|
243
|
-
const subq = (i, w) => q.clone().select({ xp, yp, i, w });
|
|
244
|
-
|
|
245
|
-
// grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
|
|
246
|
-
const a = subq(
|
|
247
|
-
sql`FLOOR(xp)::INTEGER + FLOOR(yp)::INTEGER * ${xn}`,
|
|
248
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// grid[xu + yv * xn] += (xv - xp) * (yp - yu) * wi;
|
|
252
|
-
const b = subq(
|
|
253
|
-
sql`FLOOR(xp)::INTEGER + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
254
|
-
sql`(FLOOR(xp)::INTEGER + 1 - xp) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// grid[xv + yu * xn] += (xp - xu) * (yv - yp) * wi;
|
|
258
|
-
const c = subq(
|
|
259
|
-
sql`FLOOR(xp)::INTEGER + 1 + FLOOR(yp)::INTEGER * ${xn}`,
|
|
260
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
// grid[xv + yv * xn] += (xp - xu) * (yp - yu) * wi;
|
|
264
|
-
const d = subq(
|
|
265
|
-
sql`FLOOR(xp)::INTEGER + 1 + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
|
|
266
|
-
sql`(xp - FLOOR(xp)::INTEGER) * (yp - FLOOR(yp)::INTEGER)${w}`
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
return Query
|
|
270
|
-
.from(Query.unionAll(a, b, c, d))
|
|
271
|
-
.select({ index: 'i', density: sum('w') }, groupby)
|
|
272
|
-
.groupby('index', groupby)
|
|
273
|
-
.having(neq('density', 0));
|
|
274
|
-
}
|
package/src/marks/HexbinMark.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Query, isNotNull,
|
|
1
|
+
import { Query, abs, add, and, bitAnd, cond, div, float64, gt, int32, isAggregateExpression, isNotNull, lt, mul, neq, pow, round, sub } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { Transient } from '../symbols.js';
|
|
3
3
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
4
4
|
import { Mark } from './Mark.js';
|
|
@@ -15,7 +15,7 @@ export class HexbinMark extends Mark {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
get
|
|
18
|
+
get filterStable() {
|
|
19
19
|
const xdom = this.plot.getAttribute('xDomain');
|
|
20
20
|
const ydom = this.plot.getAttribute('yDomain');
|
|
21
21
|
return xdom && ydom && !xdom[Transient] && !ydom[Transient];
|
|
@@ -23,24 +23,24 @@ export class HexbinMark extends Mark {
|
|
|
23
23
|
|
|
24
24
|
query(filter = []) {
|
|
25
25
|
if (this.hasOwnData()) return null;
|
|
26
|
-
const { plot, binWidth, channels
|
|
26
|
+
const { plot, binWidth, channels } = this;
|
|
27
27
|
|
|
28
28
|
// Extract channel information, update top-level query
|
|
29
29
|
// and extract dependent columns for aggregates
|
|
30
|
-
let
|
|
30
|
+
let xc, yc;
|
|
31
31
|
const dims = new Set;
|
|
32
32
|
const cols = {};
|
|
33
33
|
for (const c of channels) {
|
|
34
34
|
if (c.channel === 'orderby') {
|
|
35
35
|
// ignore ordering, as we will aggregate
|
|
36
36
|
} else if (c.channel === 'x') {
|
|
37
|
-
|
|
37
|
+
xc = c;
|
|
38
38
|
} else if (c.channel === 'y') {
|
|
39
|
-
|
|
39
|
+
yc = c;
|
|
40
40
|
} else if (Object.hasOwn(c, 'field')) {
|
|
41
41
|
const { as, field } = c;
|
|
42
42
|
cols[as] = field;
|
|
43
|
-
if (!field
|
|
43
|
+
if (!isAggregateExpression(field)) {
|
|
44
44
|
dims.add(as);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -54,33 +54,63 @@ export class HexbinMark extends Mark {
|
|
|
54
54
|
// margins as this is what Observable Plot does.
|
|
55
55
|
const ox = 0.5 - plot.getAttribute('marginLeft');
|
|
56
56
|
const oy = 0 - plot.getAttribute('marginTop');
|
|
57
|
-
const dx =
|
|
58
|
-
const dy =
|
|
59
|
-
const xr =
|
|
60
|
-
const yr =
|
|
57
|
+
const dx = float64(binWidth);
|
|
58
|
+
const dy = float64(binWidth * (1.5 / Math.sqrt(3)));
|
|
59
|
+
const xr = float64(plot.innerWidth() / (x2 - x1));
|
|
60
|
+
const yr = float64(plot.innerHeight() / (y2 - y1));
|
|
61
|
+
|
|
62
|
+
// column references
|
|
63
|
+
const x ='_x';
|
|
64
|
+
const y = '_y';
|
|
65
|
+
const px = '_px';
|
|
66
|
+
const py = '_py';
|
|
67
|
+
const pi = '_pi';
|
|
68
|
+
const pj = '_pj';
|
|
69
|
+
const tt = '_tt';
|
|
61
70
|
|
|
62
71
|
// Top-level query maps from screen space back to data values.
|
|
63
72
|
// Doing so ensures that Plot generates correct data-driven scales.
|
|
64
73
|
return Query.select({
|
|
65
|
-
[
|
|
66
|
-
|
|
74
|
+
[xc.as]: add(
|
|
75
|
+
float64(x1),
|
|
76
|
+
div(add(mul(add(x, mul(0.5, bitAnd(y, 1))), dx), ox), xr)
|
|
77
|
+
),
|
|
78
|
+
[yc.as]: sub(float64(y2), div(add(mul(y, dy), oy), yr)),
|
|
67
79
|
...cols
|
|
68
80
|
})
|
|
69
|
-
.groupby(
|
|
81
|
+
.groupby(x, y, ...dims)
|
|
70
82
|
.from(
|
|
71
83
|
// Subquery performs hex binning in screen space and also passes
|
|
72
84
|
// original columns through (the DB should optimize this).
|
|
73
85
|
Query.select({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
[py]: div(mul(yr, sub(sub(y2, yc.field), oy)), dy),
|
|
87
|
+
[pj]: int32(round(py)),
|
|
88
|
+
[px]: sub(
|
|
89
|
+
div(sub(mul(xr, sub(xc.field, x1)), ox), dx),
|
|
90
|
+
mul(0.5, bitAnd(pj, 1))
|
|
91
|
+
),
|
|
92
|
+
[pi]: int32(round(px)),
|
|
93
|
+
[tt]: and(
|
|
94
|
+
gt(mul(abs(sub(py, pj)), 3), 1),
|
|
95
|
+
gt(
|
|
96
|
+
add(pow(sub(px, pi), 2), pow(sub(py, pj), 2)),
|
|
97
|
+
add(
|
|
98
|
+
pow(sub(sub(px, pi), mul(0.5, cond(lt(px, pi), -1, 1))), 2),
|
|
99
|
+
pow(sub(sub(py, pj), cond(lt(py, pj), -1, 1)), 2)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
[x]: cond(tt,
|
|
104
|
+
int32(add(
|
|
105
|
+
add(pi, cond(lt(px, pi), -0.5, 0.5)),
|
|
106
|
+
cond(neq(bitAnd(pj, 1), 0), 0.5, -0.5)
|
|
107
|
+
)),
|
|
108
|
+
pi
|
|
109
|
+
),
|
|
110
|
+
[y]: cond(tt, int32(add(pj, cond(lt(py, pj), -1, 1))), pj)
|
|
81
111
|
}, '*')
|
|
82
|
-
.from(
|
|
83
|
-
.where(isNotNull(
|
|
112
|
+
.from(this.sourceTable())
|
|
113
|
+
.where(isNotNull(xc.field), isNotNull(yc.field), filter)
|
|
84
114
|
);
|
|
85
115
|
}
|
|
86
116
|
}
|
package/src/marks/Mark.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query,
|
|
1
|
+
import { isParam, MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
|
|
2
|
+
import { Query, SelectQuery, collectParams, column, isAggregateExpression, isColumnRef, isNode, isParamLike } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { isColor } from './util/is-color.js';
|
|
4
4
|
import { isConstantOption } from './util/is-constant-option.js';
|
|
5
5
|
import { isSymbol } from './util/is-symbol.js';
|
|
@@ -15,7 +15,7 @@ const isFieldObject = (channel, field) => {
|
|
|
15
15
|
const fieldEntry = (channel, field) => ({
|
|
16
16
|
channel,
|
|
17
17
|
field,
|
|
18
|
-
as: field
|
|
18
|
+
as: isColumnRef(field) ? field.column : channel
|
|
19
19
|
});
|
|
20
20
|
const valueEntry = (channel, value) => ({ channel, value });
|
|
21
21
|
|
|
@@ -28,16 +28,18 @@ export class Mark extends MosaicClient {
|
|
|
28
28
|
super(source?.options?.filterBy);
|
|
29
29
|
this.type = type;
|
|
30
30
|
this.reqs = reqs;
|
|
31
|
-
|
|
32
31
|
this.source = source;
|
|
33
|
-
if (isDataArray(this.source)) {
|
|
34
|
-
this.data = toDataColumns(this.source);
|
|
35
|
-
}
|
|
36
32
|
|
|
37
33
|
const channels = this.channels = [];
|
|
38
34
|
const detail = this.detail = new Set;
|
|
39
35
|
const params = this.params = new Set;
|
|
40
36
|
|
|
37
|
+
if (isDataArray(source)) {
|
|
38
|
+
this.data = toDataColumns(source);
|
|
39
|
+
} else if (isParam(source?.table)) {
|
|
40
|
+
params.add(source.table);
|
|
41
|
+
}
|
|
42
|
+
|
|
41
43
|
const process = (channel, entry) => {
|
|
42
44
|
const type = typeof entry;
|
|
43
45
|
if (channel === 'channels') {
|
|
@@ -62,20 +64,16 @@ export class Mark extends MosaicClient {
|
|
|
62
64
|
channels.push(fieldEntry(channel, column(entry)));
|
|
63
65
|
}
|
|
64
66
|
} else if (isParamLike(entry)) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
c.value = value;
|
|
76
|
-
return this.update();
|
|
77
|
-
});
|
|
78
|
-
}
|
|
67
|
+
const c = valueEntry(channel, entry.value);
|
|
68
|
+
channels.push(c);
|
|
69
|
+
entry.addEventListener('value', value => {
|
|
70
|
+
// update immediately, the value is simply passed to Plot
|
|
71
|
+
c.value = value;
|
|
72
|
+
return this.update();
|
|
73
|
+
});
|
|
74
|
+
} else if (isNode(entry)) {
|
|
75
|
+
collectParams(entry).forEach(p => params.add(p));
|
|
76
|
+
channels.push(fieldEntry(channel, entry))
|
|
79
77
|
} else if (type === 'object' && isFieldObject(channel, entry)) {
|
|
80
78
|
channels.push(fieldEntry(channel, entry));
|
|
81
79
|
} else if (entry !== undefined) {
|
|
@@ -99,6 +97,11 @@ export class Mark extends MosaicClient {
|
|
|
99
97
|
if (this.source?.table) this.queryPending();
|
|
100
98
|
}
|
|
101
99
|
|
|
100
|
+
sourceTable() {
|
|
101
|
+
const table = this.source?.table;
|
|
102
|
+
return table ? (isParam(table) ? table.value : table) : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
102
105
|
hasOwnData() {
|
|
103
106
|
return this.source == null || isDataArray(this.source);
|
|
104
107
|
}
|
|
@@ -121,7 +124,7 @@ export class Mark extends MosaicClient {
|
|
|
121
124
|
fields() {
|
|
122
125
|
if (this.hasOwnData()) return null;
|
|
123
126
|
|
|
124
|
-
const {
|
|
127
|
+
const { channels, reqs } = this;
|
|
125
128
|
const fields = new Map;
|
|
126
129
|
for (const { channel, field } of channels) {
|
|
127
130
|
if (!field) continue;
|
|
@@ -132,6 +135,7 @@ export class Mark extends MosaicClient {
|
|
|
132
135
|
reqs[channel]?.forEach(s => entry.add(s));
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
const table = this.sourceTable();
|
|
135
139
|
return Array.from(fields, ([c, s]) => ({ table, column: c, stats: s }));
|
|
136
140
|
}
|
|
137
141
|
|
|
@@ -154,8 +158,7 @@ export class Mark extends MosaicClient {
|
|
|
154
158
|
*/
|
|
155
159
|
query(filter = []) {
|
|
156
160
|
if (this.hasOwnData()) return null;
|
|
157
|
-
|
|
158
|
-
return markQuery(channels, table).where(filter);
|
|
161
|
+
return markQuery(this.channels, this.sourceTable()).where(filter);
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
queryPending() {
|
|
@@ -211,7 +214,7 @@ export function channelOption(c, columns) {
|
|
|
211
214
|
* @param {*} table the table to query.
|
|
212
215
|
* @param {*} skip an optional array of channels to skip.
|
|
213
216
|
* Mark subclasses can skip channels that require special handling.
|
|
214
|
-
* @returns {
|
|
217
|
+
* @returns {SelectQuery} a Query instance
|
|
215
218
|
*/
|
|
216
219
|
export function markQuery(channels, table, skip = []) {
|
|
217
220
|
const q = Query.from({ source: table });
|
|
@@ -225,7 +228,7 @@ export function markQuery(channels, table, skip = []) {
|
|
|
225
228
|
if (channel === 'orderby') {
|
|
226
229
|
q.orderby(c.value);
|
|
227
230
|
} else if (field) {
|
|
228
|
-
if (field
|
|
231
|
+
if (isAggregateExpression(field)) {
|
|
229
232
|
aggr = true;
|
|
230
233
|
} else {
|
|
231
234
|
if (dims.has(as)) continue;
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -173,6 +173,7 @@ function alphaScale(mark, prop) {
|
|
|
173
173
|
opacity: {
|
|
174
174
|
type: plot.getAttribute('opacityScale'),
|
|
175
175
|
domain,
|
|
176
|
+
range: plot.getAttribute('opacityRange'),
|
|
176
177
|
clamp: plot.getAttribute('opacityClamp'),
|
|
177
178
|
nice: plot.getAttribute('opacityNice'),
|
|
178
179
|
reverse: plot.getAttribute('opacityReverse'),
|
|
@@ -36,7 +36,7 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
tileQuery(extent) {
|
|
39
|
-
const { interpolate, pad, channels, densityMap
|
|
39
|
+
const { interpolate, pad, channels, densityMap } = this;
|
|
40
40
|
const [[x0, x1], [y0, y1]] = extent;
|
|
41
41
|
const [nx, ny] = this.bins;
|
|
42
42
|
const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
|
|
@@ -49,7 +49,7 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
49
49
|
: [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
|
|
50
50
|
|
|
51
51
|
const q = Query
|
|
52
|
-
.from(
|
|
52
|
+
.from(this.sourceTable())
|
|
53
53
|
.where(bounds);
|
|
54
54
|
|
|
55
55
|
const groupby = this.groupby = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { range } from 'd3';
|
|
2
2
|
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
3
3
|
import {
|
|
4
|
-
Query, max, min,
|
|
4
|
+
Query, max, min, float64, isNotNull,
|
|
5
5
|
regrIntercept, regrSlope, regrCount,
|
|
6
6
|
regrSYY, regrSXX, regrAvgX
|
|
7
7
|
} from '@uwdata/mosaic-sql';
|
|
@@ -43,8 +43,8 @@ export class RegressionMark extends Mark {
|
|
|
43
43
|
ssy: regrSYY(y, x),
|
|
44
44
|
ssx: regrSXX(y, x),
|
|
45
45
|
xm: regrAvgX(y, x),
|
|
46
|
-
x0:
|
|
47
|
-
x1:
|
|
46
|
+
x0: float64(min(x).where(isNotNull(y))),
|
|
47
|
+
x1: float64(max(x).where(isNotNull(y)))
|
|
48
48
|
})
|
|
49
49
|
.select(groupby)
|
|
50
50
|
.groupby(groupby);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bin1d } from '@uwdata/mosaic-sql';
|
|
2
2
|
import { channelScale } from './channel-scale.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Generates a SQL expression for 1D pixel-level binning.
|
|
6
6
|
* Adjusts for scale transformations (log, sqrt, ...).
|
|
7
7
|
* Returns a [binExpression, field] array, where field is the
|
|
8
|
-
* input value
|
|
9
|
-
* name. For time data, fields are mapped to
|
|
8
|
+
* input value being binned. Often the field is just a column
|
|
9
|
+
* name. For time data, fields are mapped to millisecond timestamps.
|
|
10
10
|
*/
|
|
11
11
|
export function binExpr(mark, channel, n, extent, pad = 1, expr) {
|
|
12
12
|
// get base expression, the channel field unless otherwise given
|
|
@@ -21,10 +21,5 @@ export function binExpr(mark, channel, n, extent, pad = 1, expr) {
|
|
|
21
21
|
const [lo, hi] = extent.map(v => apply(v));
|
|
22
22
|
const v = sqlApply(expr);
|
|
23
23
|
const f = type === 'time' || type === 'utc' ? v : expr;
|
|
24
|
-
|
|
25
|
-
const s = d !== 1 ? ` * ${d}::DOUBLE` : '';
|
|
26
|
-
const bin = reverse
|
|
27
|
-
? sql`(${hi} - ${v}::DOUBLE)${s}`
|
|
28
|
-
: sql`(${v}::DOUBLE - ${lo})${s}`;
|
|
29
|
-
return [bin, f];
|
|
24
|
+
return [bin1d(v, lo, hi, n - pad, reverse), f];
|
|
30
25
|
}
|
package/src/marks/util/extent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { scaleLinear } from 'd3';
|
|
2
2
|
import { Fixed, Transient } from '../../symbols.js';
|
|
3
|
+
import { BetweenOpNode, walk } from '@uwdata/mosaic-sql';
|
|
3
4
|
|
|
4
5
|
export const xext = { x: ['min', 'max'] };
|
|
5
6
|
export const yext = { y: ['min', 'max'] };
|
|
@@ -36,19 +37,15 @@ export function filteredExtent(filter, column) {
|
|
|
36
37
|
|
|
37
38
|
let lo;
|
|
38
39
|
let hi;
|
|
39
|
-
const visitor = (type, clause) => {
|
|
40
|
-
if (type === 'BETWEEN' && `${clause.field}` === column) {
|
|
41
|
-
const { range } = clause;
|
|
42
|
-
if (range && (lo == null || range[0] < lo)) lo = range[0];
|
|
43
|
-
if (range && (hi == null || range[1] > hi)) hi = range[1];
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
40
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
[filter].flat().forEach(p => walk(p, (node) => {
|
|
42
|
+
if (node instanceof BetweenOpNode && `${node.expr}` === column) {
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
const extent = (node.extent ?? []).map(v => v?.value ?? v);
|
|
45
|
+
if (lo == null || extent[0] < lo) lo = extent[0];
|
|
46
|
+
if (hi == null || extent[1] > hi) hi = extent[1];
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
52
49
|
|
|
53
50
|
return lo != null && hi != null && lo !== hi ? [lo, hi] : undefined;
|
|
54
51
|
}
|
package/src/marks/util/stats.js
CHANGED
|
@@ -172,7 +172,7 @@ export function qt(p, dof) {
|
|
|
172
172
|
export function erfinv(x) {
|
|
173
173
|
// Implementation from "Approximating the erfinv function" by Mike Giles,
|
|
174
174
|
// GPU Computing Gems, volume 2, 2010.
|
|
175
|
-
// Ported from Apache Commons Math,
|
|
175
|
+
// Ported from Apache Commons Math, https://www.apache.org/licenses/LICENSE-2.0
|
|
176
176
|
|
|
177
177
|
// beware that the logarithm argument must be
|
|
178
178
|
// computed as (1.0 - x) * (1.0 + x),
|
package/src/plot-renderer.js
CHANGED
|
@@ -102,54 +102,42 @@ function inferLabel(key, spec, marks) {
|
|
|
102
102
|
const fields = marks.map(mark => mark.channelField(key)?.field);
|
|
103
103
|
if (fields.every(x => x == null)) return; // no columns found
|
|
104
104
|
|
|
105
|
-
// check for consistent
|
|
106
|
-
let
|
|
107
|
-
let candLabel;
|
|
108
|
-
let type;
|
|
105
|
+
// check for consistent label
|
|
106
|
+
let candidate;
|
|
109
107
|
for (let i = 0; i < fields.length; ++i) {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
108
|
+
const label = fieldLabel(fields[i]);
|
|
109
|
+
if (label === undefined) {
|
|
112
110
|
continue;
|
|
113
|
-
} else if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} else if (candLabel !== label) {
|
|
118
|
-
candLabel = undefined;
|
|
119
|
-
} else if (candCol !== column) {
|
|
120
|
-
candCol = undefined;
|
|
111
|
+
} else if (candidate === undefined) {
|
|
112
|
+
candidate = label;
|
|
113
|
+
} else if (candidate !== label) {
|
|
114
|
+
candidate = undefined;
|
|
121
115
|
}
|
|
122
116
|
}
|
|
123
|
-
let candidate = candLabel || candCol;
|
|
124
117
|
if (candidate === undefined) return;
|
|
125
118
|
|
|
126
|
-
// adjust candidate label formatting
|
|
127
|
-
if ((type === 'number' || type === 'date') && (key === 'x' || key === 'y')) {
|
|
128
|
-
if (scale.percent) candidate = `${candidate} (%)`;
|
|
129
|
-
const order = (key === 'x' ? 1 : -1) * (scale.reverse ? -1 : 1);
|
|
130
|
-
if (key === 'x' || scale.labelAnchor === 'center') {
|
|
131
|
-
candidate = (key === 'x') === order < 0 ? `← ${candidate}` : `${candidate} →`;
|
|
132
|
-
} else {
|
|
133
|
-
candidate = `${order < 0 ? '↑ ' : '↓ '}${candidate}`;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
119
|
// add label to spec
|
|
138
120
|
spec[key] = { ...scale, label: candidate };
|
|
139
121
|
}
|
|
140
122
|
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
123
|
+
function fieldLabel(field) {
|
|
124
|
+
if (!field) return undefined;
|
|
125
|
+
switch (field.type) {
|
|
126
|
+
case 'COLUMN_REF': return field.column;
|
|
127
|
+
case 'CAST': return fieldLabel(field.expr);
|
|
128
|
+
case 'FUNCTION':
|
|
129
|
+
if (field.name === 'make_date') return 'date';
|
|
130
|
+
break;
|
|
149
131
|
}
|
|
132
|
+
return exprLabel(field);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function exprLabel(field) {
|
|
136
|
+
const s = `${field}`.replaceAll('"', '').replaceAll('(*)', '()');
|
|
137
|
+
return s.endsWith('()') ? s.slice(0, -2) : s;
|
|
150
138
|
}
|
|
151
139
|
|
|
152
|
-
function
|
|
140
|
+
function annotatePlot(svg, indices) {
|
|
153
141
|
let index = -1;
|
|
154
142
|
for (const child of svg.children) {
|
|
155
143
|
const aria = child.getAttribute('aria-label') || '';
|
|
@@ -161,16 +149,3 @@ function annotateMarks(svg, indices) {
|
|
|
161
149
|
}
|
|
162
150
|
}
|
|
163
151
|
}
|
|
164
|
-
|
|
165
|
-
function getType(data, channel) {
|
|
166
|
-
if (!data) return;
|
|
167
|
-
const { columns } = data;
|
|
168
|
-
const col = columns[channel] ?? columns[channel+'1'] ?? columns[channel+'2'];
|
|
169
|
-
if (col) {
|
|
170
|
-
for (const v of col) {
|
|
171
|
-
if (v != null) {
|
|
172
|
-
return v instanceof Date ? 'date' : typeof v;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
package/src/plot.js
CHANGED
|
@@ -132,7 +132,7 @@ export class Plot {
|
|
|
132
132
|
params.set(param, [mark]);
|
|
133
133
|
param.addEventListener('value', () => {
|
|
134
134
|
return Promise.allSettled(
|
|
135
|
-
params.get(param).map(mark => mark.
|
|
135
|
+
params.get(param).map(mark => mark.initialize())
|
|
136
136
|
);
|
|
137
137
|
});
|
|
138
138
|
}
|