@uwdata/mosaic-plot 0.12.0 → 0.12.2
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 +2 -0
- package/dist/mosaic-plot.js +124 -50
- package/dist/mosaic-plot.min.js +8 -8
- package/package.json +4 -4
- package/src/marks/Density1DMark.js +68 -21
- package/src/marks/HexbinMark.js +5 -2
- package/src/marks/Mark.js +7 -3
- package/src/marks/util/grid.js +36 -7
- package/src/marks/util/is-constant-option.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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.16",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.12.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.12.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.12.2",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.12.2",
|
|
34
34
|
"d3": "^7.9.0",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "0ca741d840b98039255f26a5ceedf10be66f790e"
|
|
38
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
2
2
|
import { binLinear1d, isBetween } from '@uwdata/mosaic-sql';
|
|
3
|
+
import { max, sum } from 'd3';
|
|
3
4
|
import { Transient } from '../symbols.js';
|
|
4
5
|
import { binExpr } from './util/bin-expr.js';
|
|
5
6
|
import { dericheConfig, dericheConv1d } from './util/density.js';
|
|
@@ -8,9 +9,17 @@ import { grid1d } from './util/grid.js';
|
|
|
8
9
|
import { handleParam } from './util/handle-param.js';
|
|
9
10
|
import { Mark, channelOption, markQuery } from './Mark.js';
|
|
10
11
|
|
|
12
|
+
const GROUPBY = { fill: 1, stroke: 1, z: 1 };
|
|
13
|
+
|
|
11
14
|
export class Density1DMark extends Mark {
|
|
12
15
|
constructor(type, source, options) {
|
|
13
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
bins = 1024,
|
|
18
|
+
bandwidth = 20,
|
|
19
|
+
normalize = false,
|
|
20
|
+
stack = false,
|
|
21
|
+
...channels
|
|
22
|
+
} = options;
|
|
14
23
|
const dim = type.endsWith('X') ? 'y' : 'x';
|
|
15
24
|
|
|
16
25
|
super(type, source, channels, dim === 'x' ? xext : yext);
|
|
@@ -24,7 +33,17 @@ export class Density1DMark extends Mark {
|
|
|
24
33
|
/** @type {number} */
|
|
25
34
|
this.bandwidth = handleParam(bandwidth, value => {
|
|
26
35
|
this.bandwidth = value;
|
|
27
|
-
return this.
|
|
36
|
+
return this.grids ? this.convolve().update() : null;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** @type {string | boolean} */
|
|
40
|
+
this.normalize = handleParam(normalize, value => {
|
|
41
|
+
return (this.normalize = value, this.convolve().update());
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** @type {boolean} */
|
|
45
|
+
this.stack = handleParam(stack, value => {
|
|
46
|
+
return (this.stack = value, this.update());
|
|
28
47
|
});
|
|
29
48
|
}
|
|
30
49
|
|
|
@@ -42,48 +61,76 @@ export class Density1DMark extends Mark {
|
|
|
42
61
|
const q = markQuery(channels, this.sourceTable(), [dim])
|
|
43
62
|
.where(filter.concat(isBetween(bx, extent)));
|
|
44
63
|
const v = this.channelField('weight') ? 'weight' : null;
|
|
45
|
-
|
|
64
|
+
const g = this.groupby = channels.flatMap(c => {
|
|
65
|
+
return (GROUPBY[c.channel] && c.field) ? c.as : [];
|
|
66
|
+
});
|
|
67
|
+
return binLinear1d(q, x, v, g);
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
queryResult(data) {
|
|
49
|
-
const
|
|
50
|
-
this.
|
|
71
|
+
const c = toDataColumns(data).columns;
|
|
72
|
+
this.grids = grid1d(this.bins, c.index, c.density, c, this.groupby);
|
|
51
73
|
return this.convolve();
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
convolve() {
|
|
55
|
-
const {
|
|
77
|
+
const {
|
|
78
|
+
bins, bandwidth, normalize, dim, grids, groupby, plot, extent: [lo, hi]
|
|
79
|
+
} = this;
|
|
80
|
+
|
|
81
|
+
const cols = grids.columns;
|
|
82
|
+
const numGrids = grids.numRows;
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
const
|
|
84
|
+
const b = this.channelField(dim).as;
|
|
85
|
+
const v = dim === 'x' ? 'y' : 'x';
|
|
59
86
|
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
87
|
+
const neg = cols._grid.some(grid => grid.some(v => v < 0));
|
|
60
88
|
const config = dericheConfig(bandwidth * (bins - 1) / size, neg);
|
|
61
|
-
const result = dericheConv1d(config, grid, bins);
|
|
62
89
|
|
|
63
|
-
// map smoothed grid values to sample data points
|
|
64
|
-
const v = dim === 'x' ? 'y' : 'x';
|
|
65
|
-
const b = this.channelField(dim).as;
|
|
66
90
|
const b0 = +lo;
|
|
67
91
|
const delta = (hi - b0) / (bins - 1);
|
|
68
|
-
const scale = 1 / delta;
|
|
69
92
|
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
const numRows = bins * numGrids;
|
|
94
|
+
const _b = new Float64Array(numRows);
|
|
95
|
+
const _v = new Float64Array(numRows);
|
|
96
|
+
const _g = groupby.reduce((m, name) => (m[name] = Array(numRows), m), {});
|
|
97
|
+
|
|
98
|
+
for (let k = 0, g = 0; g < numGrids; ++g) {
|
|
99
|
+
// fill in groupby values
|
|
100
|
+
groupby.forEach(name => _g[name].fill(cols[name][g], k, k + bins));
|
|
101
|
+
|
|
102
|
+
// perform smoothing, map smoothed grid values to sample data points
|
|
103
|
+
const grid = cols._grid[g];
|
|
104
|
+
const result = dericheConv1d(config, grid, bins);
|
|
105
|
+
const scale = 1 / norm(grid, result, delta, normalize);
|
|
106
|
+
for (let i = 0; i < bins; ++i, ++k) {
|
|
107
|
+
_b[k] = b0 + i * delta;
|
|
108
|
+
_v[k] = result[i] * scale;
|
|
109
|
+
}
|
|
75
110
|
}
|
|
76
|
-
this.data = { numRows: bins, columns: { [b]: _b, [v]: _v } };
|
|
77
111
|
|
|
112
|
+
this.data = { numRows, columns: { [b]: _b, [v]: _v, ..._g } };
|
|
78
113
|
return this;
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
plotSpecs() {
|
|
82
|
-
const { type, data: { numRows: length, columns }, channels, dim } = this;
|
|
83
|
-
|
|
117
|
+
const { type, data: { numRows: length, columns }, channels, dim, stack } = this;
|
|
118
|
+
|
|
119
|
+
// control if Plot's implicit stack transform is applied
|
|
120
|
+
// no stacking is done if x2/y2 are used instead of x/y
|
|
121
|
+
const _ = type.startsWith('area') && !stack ? '2' : '';
|
|
122
|
+
const options = dim === 'x' ? { [`y${_}`]: columns.y } : { [`x${_}`]: columns.x };
|
|
123
|
+
|
|
84
124
|
for (const c of channels) {
|
|
85
125
|
options[c.channel] = channelOption(c, columns);
|
|
86
126
|
}
|
|
87
127
|
return [{ type, data: { length }, options }];
|
|
88
128
|
}
|
|
89
129
|
}
|
|
130
|
+
|
|
131
|
+
function norm(grid, smoothed, delta, type) {
|
|
132
|
+
const value = type === true || type === 'sum' ? sum(grid)
|
|
133
|
+
: type === 'max' ? max(smoothed)
|
|
134
|
+
: delta;
|
|
135
|
+
return value || 1;
|
|
136
|
+
}
|
package/src/marks/HexbinMark.js
CHANGED
|
@@ -75,7 +75,10 @@ export class HexbinMark extends Mark {
|
|
|
75
75
|
float64(x1),
|
|
76
76
|
div(add(mul(add(x, mul(0.5, bitAnd(y, 1))), dx), ox), xr)
|
|
77
77
|
),
|
|
78
|
-
[yc.as]: sub(
|
|
78
|
+
[yc.as]: sub(
|
|
79
|
+
float64(y2),
|
|
80
|
+
div(add(mul(y, dy), oy), yr)
|
|
81
|
+
),
|
|
79
82
|
...cols
|
|
80
83
|
})
|
|
81
84
|
.groupby(x, y, ...dims)
|
|
@@ -83,7 +86,7 @@ export class HexbinMark extends Mark {
|
|
|
83
86
|
// Subquery performs hex binning in screen space and also passes
|
|
84
87
|
// original columns through (the DB should optimize this).
|
|
85
88
|
Query.select({
|
|
86
|
-
[py]: div(mul(yr, sub(
|
|
89
|
+
[py]: div(sub(mul(yr, sub(y2, yc.field)), oy), dy),
|
|
87
90
|
[pj]: int32(round(py)),
|
|
88
91
|
[px]: sub(
|
|
89
92
|
div(sub(mul(xr, sub(xc.field, x1)), ox), dx),
|
package/src/marks/Mark.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isParam, MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Query, SelectQuery, collectParams, column, isAggregateExpression, isColumnRef, isNode, isParamLike } from '@uwdata/mosaic-sql';
|
|
2
|
+
import { Query, SelectQuery, collectParams, column, isAggregateExpression, isColumnParam, 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: isColumnRef(field) ? field.column : channel
|
|
18
|
+
as: isColumnRef(field) && !isColumnParam(field) ? field.column : channel
|
|
19
19
|
});
|
|
20
20
|
const valueEntry = (channel, value) => ({ channel, value });
|
|
21
21
|
|
|
@@ -136,7 +136,11 @@ export class Mark extends MosaicClient {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
const table = this.sourceTable();
|
|
139
|
-
return Array.from(fields, ([c, s]) => ({
|
|
139
|
+
return Array.from(fields, ([c, s]) => ({
|
|
140
|
+
table,
|
|
141
|
+
column: c,
|
|
142
|
+
stats: Array.from(s)
|
|
143
|
+
}));
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
fieldInfo(info) {
|
package/src/marks/util/grid.js
CHANGED
|
@@ -24,15 +24,44 @@ export function array(size, proto = []) {
|
|
|
24
24
|
* @param {number} size The grid size.
|
|
25
25
|
* @param {Arrayish} index The grid indices for sample points.
|
|
26
26
|
* @param {Arrayish} value The sample point values.
|
|
27
|
-
* @
|
|
27
|
+
* @param {Record<string,Arrayish>} columns Named column arrays with groupby values.
|
|
28
|
+
* @param {string[]} groupby The names of columns to group by.
|
|
29
|
+
* @returns {{
|
|
30
|
+
* numRows: number;
|
|
31
|
+
* columns: { [key:string]: Arrayish }
|
|
32
|
+
* }} Named column arrays of generated grid values.
|
|
28
33
|
*/
|
|
29
|
-
export function grid1d(size, index, value) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
export function grid1d(size, index, value, columns, groupby) {
|
|
35
|
+
const numRows = index.length;
|
|
36
|
+
const result = {};
|
|
37
|
+
const cells = [];
|
|
38
|
+
|
|
39
|
+
// if grouped, generate per-row group indices
|
|
40
|
+
if (groupby?.length) {
|
|
41
|
+
const group = new Int32Array(numRows);
|
|
42
|
+
const gvalues = groupby.map(name => columns[name]);
|
|
43
|
+
const cellMap = {};
|
|
44
|
+
for (let row = 0; row < numRows; ++row) {
|
|
45
|
+
const key = gvalues.map(group => group[row]);
|
|
46
|
+
group[row] = cellMap[key] ??= cells.push(key) - 1;
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < groupby.length; ++i) {
|
|
49
|
+
result[groupby[i]] = cells.map(cell => cell[i]);
|
|
50
|
+
}
|
|
51
|
+
const G = result._grid = cells.map(() => array(size, value));
|
|
52
|
+
for (let row = 0; row < numRows; ++row) {
|
|
53
|
+
G[group[row]][index[row]] = value[row];
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
cells.push([]); // single group
|
|
57
|
+
const [G] = result._grid = [array(size, value)]
|
|
58
|
+
for (let row = 0; row < numRows; ++row) {
|
|
59
|
+
G[index[row]] = value[row];
|
|
60
|
+
}
|
|
34
61
|
}
|
|
35
|
-
|
|
62
|
+
|
|
63
|
+
// @ts-ignore
|
|
64
|
+
return { numRows: cells.length, columns: result };
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
/**
|