@uwdata/mosaic-plot 0.7.1 → 0.9.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 +4702 -5648
- package/dist/mosaic-plot.min.js +14 -14
- package/package.json +5 -5
- package/src/index.js +1 -0
- package/src/interactors/Highlight.js +6 -3
- package/src/interactors/Interval1D.js +14 -12
- package/src/interactors/Interval2D.js +13 -16
- package/src/interactors/Nearest.js +80 -36
- package/src/interactors/PanZoom.js +7 -9
- package/src/interactors/Toggle.js +29 -37
- package/src/interactors/util/patchScreenCTM.js +2 -0
- package/src/legend.js +150 -29
- package/src/marks/ConnectedMark.js +6 -0
- package/src/marks/ContourMark.js +36 -16
- package/src/marks/DenseLineMark.js +9 -5
- package/src/marks/Density1DMark.js +22 -13
- package/src/marks/Density2DMark.js +33 -18
- package/src/marks/ErrorBarMark.js +50 -0
- package/src/marks/GeoMark.js +7 -8
- package/src/marks/Grid2DMark.js +58 -28
- package/src/marks/HexbinMark.js +10 -2
- package/src/marks/Mark.js +56 -16
- package/src/marks/RasterMark.js +61 -23
- package/src/marks/RasterTileMark.js +39 -20
- package/src/marks/RegressionMark.js +69 -34
- package/src/marks/util/grid.js +94 -86
- package/src/marks/util/handle-param.js +10 -11
- package/src/marks/util/is-constant-option.js +2 -1
- package/src/marks/util/permute.js +10 -0
- package/src/marks/util/stats.js +121 -1
- package/src/marks/util/to-data-columns.js +71 -0
- package/src/plot-attributes.js +11 -3
- package/src/plot-renderer.js +28 -9
- package/src/plot.js +20 -0
- package/src/transforms/bin.js +3 -1
- package/src/marks/util/interpolate.js +0 -205
- package/src/marks/util/to-data-array.js +0 -50
package/src/legend.js
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
+
import { scale } from '@observablehq/plot';
|
|
2
|
+
import { Interval1D } from './interactors/Interval1D.js';
|
|
1
3
|
import { Toggle } from './interactors/Toggle.js';
|
|
2
4
|
|
|
5
|
+
const TOGGLE_SELECTOR = ':scope > div, :scope > span';
|
|
6
|
+
const SWATCH = 'swatch';
|
|
7
|
+
const RAMP = 'ramp';
|
|
8
|
+
|
|
3
9
|
export class Legend {
|
|
4
10
|
constructor(channel, options) {
|
|
5
|
-
const { as, ...rest } = options;
|
|
11
|
+
const { as, field, ...rest } = options;
|
|
6
12
|
this.channel = channel;
|
|
7
13
|
this.options = { label: null, ...rest };
|
|
14
|
+
this.type = null;
|
|
15
|
+
this.handler = null;
|
|
8
16
|
this.selection = as;
|
|
17
|
+
this.field = field;
|
|
18
|
+
this.legend = null;
|
|
9
19
|
|
|
10
20
|
this.element = document.createElement('div');
|
|
11
21
|
this.element.setAttribute('class', 'legend');
|
|
12
|
-
this.element
|
|
22
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
setPlot(plot) {
|
|
16
|
-
|
|
17
|
-
const mark = findMark(plot, channel);
|
|
18
|
-
if (this.selection && mark) {
|
|
19
|
-
this.handler = new Toggle(mark, { selection, channels: [channel] });
|
|
20
|
-
this.selection.addEventListener('value', () => this.update());
|
|
21
|
-
}
|
|
26
|
+
this.plot = plot;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
init(svg) {
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
? options
|
|
29
|
-
: { marginTop: 1, tickSize: 2, height: 28, ...options };
|
|
30
|
-
this.legend = svg.legend(channel, opt);
|
|
31
|
-
|
|
32
|
-
if (handler) {
|
|
33
|
-
handler.init(this.legend, ':scope > div', el => [el.__data__]);
|
|
34
|
-
this.update();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
this.element.replaceChildren(this.legend);
|
|
30
|
+
// createLegend sets this.legend, may set this.handler
|
|
31
|
+
const el = createLegend(this, svg);
|
|
32
|
+
this.element.replaceChildren(el);
|
|
38
33
|
return this.element;
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
update() {
|
|
42
37
|
if (!this.legend) return;
|
|
43
|
-
const {
|
|
44
|
-
const
|
|
45
|
-
|
|
38
|
+
const { selection, handler } = this;
|
|
39
|
+
const { single, value } = selection;
|
|
40
|
+
|
|
41
|
+
// extract currently selected values
|
|
42
|
+
const vals = single ? value : selection.valueFor(handler);
|
|
43
|
+
const curr = vals && vals.length ? new Set(vals.map(v => v[0])) : null;
|
|
44
|
+
|
|
45
|
+
const nodes = this.legend.querySelectorAll(TOGGLE_SELECTOR);
|
|
46
46
|
for (const node of nodes) {
|
|
47
47
|
const selected = curr ? curr.has(node.__data__) : true;
|
|
48
48
|
node.style.opacity = selected ? 1 : 0.2;
|
|
@@ -50,17 +50,138 @@ export class Legend {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
53
|
+
function createLegend(legend, svg) {
|
|
54
|
+
const { channel, options, selection } = legend;
|
|
55
|
+
const scale = svg.scale(channel);
|
|
56
|
+
const type = scale.type === 'ordinal' ? SWATCH : RAMP;
|
|
57
|
+
|
|
58
|
+
// labels for swatch legends are not yet supported by Plot
|
|
59
|
+
// track here: https://github.com/observablehq/plot/issues/834
|
|
60
|
+
// for consistent layout, adjust sizing when there is no label
|
|
61
|
+
const opt = type === SWATCH ? options
|
|
62
|
+
: options.label ? { tickSize: 2, ...options }
|
|
63
|
+
: { tickSize: 2, marginTop: 1, height: 29, ...options };
|
|
64
|
+
|
|
65
|
+
// instantiate new legend element, bind to Legend class
|
|
66
|
+
const el = svg.legend(channel, opt);
|
|
67
|
+
legend.legend = el;
|
|
68
|
+
|
|
69
|
+
// if this is an interactive legend, add a scale lookup function
|
|
70
|
+
// this allows interval interactors to access encoding information
|
|
71
|
+
let interactive = !!selection;
|
|
72
|
+
if (interactive && type === RAMP) {
|
|
73
|
+
const width = opt.width ?? 240; // 240 is default ramp length
|
|
74
|
+
const spatial = spatialScale(scale, width);
|
|
75
|
+
if (spatial) {
|
|
76
|
+
el.scale = function(type) {
|
|
77
|
+
return type === 'x' ? { range: [0, width] }
|
|
78
|
+
: type === 'y' ? { range: [-10, 0] }
|
|
79
|
+
: type === channel ? spatial
|
|
80
|
+
: undefined;
|
|
81
|
+
};
|
|
82
|
+
} else {
|
|
83
|
+
// spatial scale construction failed, disable interaction
|
|
84
|
+
interactive = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// initialize interactors to use updated legend element
|
|
89
|
+
if (interactive) {
|
|
90
|
+
const handler = getInteractor(legend, type);
|
|
91
|
+
if (type === SWATCH) {
|
|
92
|
+
handler.init(el, TOGGLE_SELECTOR, el => [el.__data__]);
|
|
93
|
+
legend.update();
|
|
94
|
+
} else {
|
|
95
|
+
handler.init(el, el.querySelector('g:last-of-type'));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return el;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getInteractor(legend, type) {
|
|
103
|
+
const { channel, handler, selection } = legend;
|
|
104
|
+
|
|
105
|
+
// exit early if already instantiated
|
|
106
|
+
if (handler) return handler;
|
|
107
|
+
|
|
108
|
+
// otherwise instantiate an appropriate interactor
|
|
109
|
+
const mark = interactorMark(legend);
|
|
110
|
+
if (type === SWATCH) {
|
|
111
|
+
legend.handler = new Toggle(mark, {
|
|
112
|
+
selection,
|
|
113
|
+
channels: [channel],
|
|
114
|
+
peers: false
|
|
115
|
+
});
|
|
116
|
+
selection.addEventListener('value', () => legend.update());
|
|
117
|
+
} else {
|
|
118
|
+
legend.handler = new Interval1D(mark, {
|
|
119
|
+
selection,
|
|
120
|
+
channel,
|
|
121
|
+
brush: { fill: 'none', stroke: 'currentColor' },
|
|
122
|
+
peers: false
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return legend.handler;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// generate a faux mark to pass to an interactor
|
|
130
|
+
function interactorMark(legend) {
|
|
131
|
+
const { channel, plot } = legend;
|
|
132
|
+
const field = legend.field ?? findField(plot.marks, channel) ?? 'value';
|
|
133
|
+
if (field) {
|
|
134
|
+
const f = { field };
|
|
135
|
+
return { plot, channelField: c => channel === c ? f : undefined };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// search marks for a backing data field for the legend
|
|
140
|
+
function findField(marks, channel) {
|
|
54
141
|
const channels = channel === 'color' ? ['fill', 'stroke']
|
|
55
142
|
: channel === 'opacity' ? ['opacity', 'fillOpacity', 'strokeOpacity']
|
|
56
143
|
: null;
|
|
57
144
|
if (channels == null) return null;
|
|
58
145
|
for (let i = marks.length - 1; i > -1; --i) {
|
|
59
|
-
for (const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
146
|
+
for (const c of channels) {
|
|
147
|
+
const field = marks[i].channelField(c, { exact: true });
|
|
148
|
+
if (field) return field.field;
|
|
63
149
|
}
|
|
64
150
|
}
|
|
65
151
|
return null;
|
|
66
152
|
}
|
|
153
|
+
|
|
154
|
+
// generate a spatial scale to brush within color or opacity ramps
|
|
155
|
+
function spatialScale(sourceScale, width) {
|
|
156
|
+
// separate out reusable parts of the scale definition
|
|
157
|
+
// eslint-disable-next-line no-unused-vars
|
|
158
|
+
const { apply, invert, interpolate, ...rest } = sourceScale;
|
|
159
|
+
|
|
160
|
+
// extract basic source scale type
|
|
161
|
+
let src = sourceScale.type;
|
|
162
|
+
if (src.startsWith('diverging-')) src = src.slice(11);
|
|
163
|
+
|
|
164
|
+
// determine spatial scale type
|
|
165
|
+
let type;
|
|
166
|
+
switch (src) {
|
|
167
|
+
case 'log':
|
|
168
|
+
case 'pow':
|
|
169
|
+
case 'sqrt':
|
|
170
|
+
case 'symlog':
|
|
171
|
+
type = src;
|
|
172
|
+
break;
|
|
173
|
+
case 'threshold':
|
|
174
|
+
case 'quantize':
|
|
175
|
+
case 'quantile':
|
|
176
|
+
// these scales do not expose an invert method
|
|
177
|
+
// the legends use color ramps with discrete swatches
|
|
178
|
+
// in the future we could try to support toggle-style
|
|
179
|
+
// interactions that map to threshold range selections
|
|
180
|
+
console.warn(`Legends do not yet support ${src} scales.`);
|
|
181
|
+
return null;
|
|
182
|
+
default:
|
|
183
|
+
type = 'linear';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return scale({ x: { ...rest, type, range: [0, width] } });
|
|
187
|
+
}
|
|
@@ -11,6 +11,11 @@ export class ConnectedMark extends Mark {
|
|
|
11
11
|
this.dim = dim;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Return a query specifying the data needed by this Mark client.
|
|
16
|
+
* @param {*} [filter] The filtering criteria to apply in the query.
|
|
17
|
+
* @returns {*} The client query
|
|
18
|
+
*/
|
|
14
19
|
query(filter = []) {
|
|
15
20
|
const { plot, dim, source } = this;
|
|
16
21
|
const { optimize = true } = source.options || {};
|
|
@@ -28,6 +33,7 @@ export class ConnectedMark extends Mark {
|
|
|
28
33
|
const [lo, hi] = filteredExtent(filter, field) || [min, max];
|
|
29
34
|
const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
|
|
30
35
|
const cols = q.select()
|
|
36
|
+
// @ts-ignore
|
|
31
37
|
.map(c => c.as)
|
|
32
38
|
.filter(c => c !== as && c !== value);
|
|
33
39
|
return m4(q, expr, as, value, cols);
|
package/src/marks/ContourMark.js
CHANGED
|
@@ -13,8 +13,11 @@ export class ContourMark extends Grid2DMark {
|
|
|
13
13
|
pixelSize: 2,
|
|
14
14
|
...channels
|
|
15
15
|
});
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
|
|
17
|
+
/** @type {number|number[]} */
|
|
18
|
+
this.thresholds = handleParam(thresholds, value => {
|
|
19
|
+
this.thresholds = value;
|
|
20
|
+
return this.grids ? this.contours().update() : null;
|
|
18
21
|
});
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -23,12 +26,16 @@ export class ContourMark extends Grid2DMark {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
contours() {
|
|
26
|
-
const { bins, densityMap,
|
|
29
|
+
const { bins, densityMap, grids, thresholds, plot } = this;
|
|
30
|
+
const { numRows, columns } = grids;
|
|
27
31
|
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tz =
|
|
32
|
+
let t = thresholds;
|
|
33
|
+
let tz;
|
|
34
|
+
if (Array.isArray(t)) {
|
|
35
|
+
tz = t;
|
|
36
|
+
} else {
|
|
37
|
+
const [, hi] = gridDomainContinuous(columns.density);
|
|
38
|
+
tz = Array.from({length: t - 1}, (_, i) => (hi * (i + 1)) / t);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
if (densityMap.fill || densityMap.stroke) {
|
|
@@ -51,18 +58,27 @@ export class ContourMark extends Grid2DMark {
|
|
|
51
58
|
const contour = contours().size(bins);
|
|
52
59
|
|
|
53
60
|
// generate contours
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const data = this.contourData = Array(numRows * tz.length);
|
|
62
|
+
const { density, ...groupby } = columns;
|
|
63
|
+
const groups = Object.entries(groupby);
|
|
64
|
+
for (let i = 0, k = 0; i < numRows; ++i) {
|
|
65
|
+
const grid = density[i];
|
|
66
|
+
const rest = groups.reduce((o, [name, col]) => (o[name] = col[i], o), {});
|
|
67
|
+
for (let j = 0; j < tz.length; ++j, ++k) {
|
|
68
|
+
// annotate contour geojson with cell groupby fields
|
|
69
|
+
// d3-contour already adds a threshold "value" property
|
|
70
|
+
data[k] = Object.assign(
|
|
71
|
+
transform(contour.contour(grid, tz[j]), x, y),
|
|
72
|
+
rest
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
60
76
|
|
|
61
77
|
return this;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
plotSpecs() {
|
|
65
|
-
const { type, channels, densityMap, data } = this;
|
|
81
|
+
const { type, channels, densityMap, contourData: data } = this;
|
|
66
82
|
const options = {};
|
|
67
83
|
for (const c of channels) {
|
|
68
84
|
const { channel } = c;
|
|
@@ -70,8 +86,12 @@ export class ContourMark extends Grid2DMark {
|
|
|
70
86
|
options[channel] = channelOption(c);
|
|
71
87
|
}
|
|
72
88
|
}
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
// d3-contour adds a threshold "value" property
|
|
90
|
+
// here we ensure requested density values are encoded
|
|
91
|
+
for (const channel in densityMap) {
|
|
92
|
+
if (!densityMap[channel]) continue;
|
|
93
|
+
options[channel] = channelOption({ channel, as: 'value' });
|
|
94
|
+
}
|
|
75
95
|
return [{ type, data, options }];
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -8,14 +8,18 @@ export class DenseLineMark extends RasterMark {
|
|
|
8
8
|
constructor(source, options) {
|
|
9
9
|
const { normalize = true, ...rest } = options;
|
|
10
10
|
super(source, rest);
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
/** @type {boolean} */
|
|
13
|
+
this.normalize = handleParam(normalize, value => {
|
|
14
|
+
return (this.normalize = value, this.requestUpdate());
|
|
15
|
+
});
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
query(filter = []) {
|
|
15
|
-
const { channels, normalize, source,
|
|
16
|
-
const [nx, ny] = this.bins = this.binDimensions(
|
|
17
|
-
const [x] = binExpr(this, 'x', nx, extentX(this, filter),
|
|
18
|
-
const [y] = binExpr(this, 'y', ny, extentY(this, filter),
|
|
19
|
+
const { channels, normalize, source, pad } = this;
|
|
20
|
+
const [nx, ny] = this.bins = this.binDimensions();
|
|
21
|
+
const [x] = binExpr(this, 'x', nx, extentX(this, filter), pad);
|
|
22
|
+
const [y] = binExpr(this, 'y', ny, extentY(this, filter), pad);
|
|
19
23
|
|
|
20
24
|
const q = Query
|
|
21
25
|
.from(source.table)
|
|
@@ -6,6 +6,7 @@ import { extentX, extentY, xext, yext } from './util/extent.js';
|
|
|
6
6
|
import { grid1d } from './util/grid.js';
|
|
7
7
|
import { handleParam } from './util/handle-param.js';
|
|
8
8
|
import { Mark, channelOption, markQuery } from './Mark.js';
|
|
9
|
+
import { toDataColumns } from './util/to-data-columns.js';
|
|
9
10
|
|
|
10
11
|
export class Density1DMark extends Mark {
|
|
11
12
|
constructor(type, source, options) {
|
|
@@ -15,9 +16,15 @@ export class Density1DMark extends Mark {
|
|
|
15
16
|
super(type, source, channels, dim === 'x' ? xext : yext);
|
|
16
17
|
this.dim = dim;
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return this.
|
|
19
|
+
/** @type {number} */
|
|
20
|
+
this.bins = handleParam(bins, value => {
|
|
21
|
+
return (this.bins = value, this.requestUpdate());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
this.bandwidth = handleParam(bandwidth, value => {
|
|
26
|
+
this.bandwidth = value;
|
|
27
|
+
return this.grid ? this.convolve().update() : null;
|
|
21
28
|
});
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -39,7 +46,8 @@ export class Density1DMark extends Mark {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
queryResult(data) {
|
|
42
|
-
|
|
49
|
+
const { columns: { index, density } } = toDataColumns(data);
|
|
50
|
+
this.grid = grid1d(this.bins, index, density);
|
|
43
51
|
return this.convolve();
|
|
44
52
|
}
|
|
45
53
|
|
|
@@ -53,29 +61,30 @@ export class Density1DMark extends Mark {
|
|
|
53
61
|
const result = dericheConv1d(config, grid, bins);
|
|
54
62
|
|
|
55
63
|
// map smoothed grid values to sample data points
|
|
56
|
-
const points = this.data = [];
|
|
57
64
|
const v = dim === 'x' ? 'y' : 'x';
|
|
58
65
|
const b = this.channelField(dim).as;
|
|
59
66
|
const b0 = +lo;
|
|
60
67
|
const delta = (hi - b0) / (bins - 1);
|
|
61
68
|
const scale = 1 / delta;
|
|
69
|
+
|
|
70
|
+
const _b = new Float64Array(bins);
|
|
71
|
+
const _v = new Float64Array(bins);
|
|
62
72
|
for (let i = 0; i < bins; ++i) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
[v]: result[i] * scale
|
|
66
|
-
});
|
|
73
|
+
_b[i] = b0 + i * delta;
|
|
74
|
+
_v[i] = result[i] * scale;
|
|
67
75
|
}
|
|
76
|
+
this.data = { numRows: bins, columns: { [b]: _b, [v]: _v } };
|
|
68
77
|
|
|
69
78
|
return this;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
plotSpecs() {
|
|
73
|
-
const { type, data, channels, dim } = this;
|
|
74
|
-
const options = dim === 'x' ? { y:
|
|
82
|
+
const { type, data: { numRows: length, columns }, channels, dim } = this;
|
|
83
|
+
const options = dim === 'x' ? { y: columns.y } : { x: columns.x };
|
|
75
84
|
for (const c of channels) {
|
|
76
|
-
options[c.channel] = channelOption(c);
|
|
85
|
+
options[c.channel] = channelOption(c, columns);
|
|
77
86
|
}
|
|
78
|
-
return [{ type, data, options }];
|
|
87
|
+
return [{ type, data: { length }, options }];
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -26,46 +26,61 @@ export class Density2DMark extends Grid2DMark {
|
|
|
26
26
|
const deltaY = (y1 - y0) / (ny - pad);
|
|
27
27
|
const offset = pad ? 0 : 0.5;
|
|
28
28
|
this.data = points(
|
|
29
|
-
this.
|
|
29
|
+
this.grids, bins, x0, y0, deltaX, deltaY,
|
|
30
30
|
scaleX.invert, scaleY.invert, offset
|
|
31
31
|
);
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
plotSpecs() {
|
|
36
|
-
const { type, channels, densityMap, data } = this;
|
|
36
|
+
const { type, channels, densityMap, data: { numRows: length, columns } } = this;
|
|
37
37
|
const options = {};
|
|
38
38
|
for (const c of channels) {
|
|
39
39
|
const { channel } = c;
|
|
40
40
|
options[channel] = (channel === 'x' || channel === 'y')
|
|
41
|
-
? channel // use generated x/y data fields
|
|
42
|
-
: channelOption(c);
|
|
41
|
+
? columns[channel] // use generated x/y data fields
|
|
42
|
+
: channelOption(c, columns);
|
|
43
43
|
}
|
|
44
44
|
for (const channel in densityMap) {
|
|
45
45
|
if (densityMap[channel]) {
|
|
46
|
-
options[channel] =
|
|
46
|
+
options[channel] = columns.density;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
return [{ type, data, options }];
|
|
49
|
+
return [{ type, data: { length }, options }];
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function points(
|
|
53
|
+
function points(data, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
|
|
54
54
|
const scale = 1 / (deltaX * deltaY);
|
|
55
55
|
const [nx, ny] = bins;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
const batch = nx * ny;
|
|
57
|
+
const numRows = batch * data.numRows;
|
|
58
|
+
|
|
59
|
+
const x = new Float64Array(numRows);
|
|
60
|
+
const y = new Float64Array(numRows);
|
|
61
|
+
const density = new Float64Array(numRows);
|
|
62
|
+
const columns = { x, y, density };
|
|
63
|
+
const { density: grids, ...rest } = data.columns;
|
|
64
|
+
for (const name in rest) {
|
|
65
|
+
columns[name] = new rest[name].constructor(numRows);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let r = 0;
|
|
69
|
+
for (let row = 0; row < data.numRows; ++row) {
|
|
70
|
+
// copy repeated values in batch
|
|
71
|
+
for (const name in rest) {
|
|
72
|
+
columns[name].fill(rest[name][row], r, r + batch);
|
|
73
|
+
}
|
|
74
|
+
// copy individual grid values
|
|
75
|
+
const grid = grids[row];
|
|
59
76
|
for (let k = 0, j = 0; j < ny; ++j) {
|
|
60
|
-
for (let i = 0; i < nx; ++i, ++k) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
y: invertY(y0 + (j + offset) * deltaY),
|
|
65
|
-
density: grid[k] * scale
|
|
66
|
-
});
|
|
77
|
+
for (let i = 0; i < nx; ++i, ++r, ++k) {
|
|
78
|
+
x[r] = invertX(x0 + (i + offset) * deltaX);
|
|
79
|
+
y[r] = invertY(y0 + (j + offset) * deltaY);
|
|
80
|
+
density[r] = grid[k] * scale;
|
|
67
81
|
}
|
|
68
82
|
}
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
|
|
85
|
+
return { numRows, columns };
|
|
71
86
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { avg, count, stddev } from '@uwdata/mosaic-sql';
|
|
2
|
+
import { erfinv } from './util/stats.js';
|
|
3
|
+
import { Mark, markPlotSpec, markQuery } from './Mark.js';
|
|
4
|
+
import { handleParam } from './util/handle-param.js';
|
|
5
|
+
import { toDataColumns } from './util/to-data-columns.js';
|
|
6
|
+
|
|
7
|
+
export class ErrorBarMark extends Mark {
|
|
8
|
+
constructor(type, source, options) {
|
|
9
|
+
const dim = type.endsWith('X') ? 'y' : 'x';
|
|
10
|
+
const { ci = 0.95, ...channels } = options;
|
|
11
|
+
super(type, source, channels);
|
|
12
|
+
this.dim = dim;
|
|
13
|
+
this.field = this.channelField(dim).field;
|
|
14
|
+
this.channels = this.channels.filter(c => c.channel !== dim);
|
|
15
|
+
|
|
16
|
+
/** @type {number} */
|
|
17
|
+
this.ci = handleParam(ci, value => {
|
|
18
|
+
return (this.ci = value, this.update());
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
query(filter = []) {
|
|
23
|
+
const { channels, field, source: { table } } = this;
|
|
24
|
+
const fields = channels.concat([
|
|
25
|
+
{ field: avg(field), as: '__avg__' },
|
|
26
|
+
{ field: count(field), as: '__n__', },
|
|
27
|
+
{ field: stddev(field), as: '__sd__' }
|
|
28
|
+
]);
|
|
29
|
+
return markQuery(fields, table).where(filter);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
queryResult(data) {
|
|
33
|
+
this.data = toDataColumns(data);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
plotSpecs() {
|
|
38
|
+
const { type, dim, detail, data, ci, channels } = this;
|
|
39
|
+
|
|
40
|
+
// compute confidence interval channels
|
|
41
|
+
const p = Math.SQRT2 * erfinv(ci);
|
|
42
|
+
const { columns: { __avg__: u, __sd__: s, __n__: n } } = data;
|
|
43
|
+
const options = {
|
|
44
|
+
[`${dim}1`]: u.map((u, i) => u - p * s[i] / Math.sqrt(n[i])),
|
|
45
|
+
[`${dim}2`]: u.map((u, i) => u + p * s[i] / Math.sqrt(n[i]))
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return markPlotSpec(type, detail, channels, data, options);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/marks/GeoMark.js
CHANGED
|
@@ -18,16 +18,15 @@ export class GeoMark extends Mark {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
queryResult(data) {
|
|
21
|
-
super.queryResult(data);
|
|
21
|
+
super.queryResult(data); // map to columns, set this.data
|
|
22
22
|
|
|
23
|
-
//
|
|
23
|
+
// look for an explicit geometry field
|
|
24
24
|
const geom = this.channelField('geometry')?.as;
|
|
25
|
-
if (geom
|
|
26
|
-
this.data
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
25
|
+
if (geom) {
|
|
26
|
+
const { columns } = this.data;
|
|
27
|
+
if (typeof columns[geom][0] === 'string') {
|
|
28
|
+
columns[geom] = columns[geom].map(s => JSON.parse(s));
|
|
29
|
+
}
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
return this;
|