@uwdata/mosaic-plot 0.8.0 → 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 +2217 -1788
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +4 -4
- package/src/index.js +1 -0
- package/src/interactors/Highlight.js +6 -3
- package/src/interactors/Interval1D.js +8 -8
- package/src/interactors/Interval2D.js +10 -15
- package/src/interactors/Nearest.js +77 -34
- package/src/interactors/PanZoom.js +4 -7
- package/src/interactors/Toggle.js +19 -28
- package/src/legend.js +19 -6
- package/src/marks/ErrorBarMark.js +50 -0
- package/src/marks/Grid2DMark.js +1 -1
- package/src/marks/Mark.js +27 -19
- package/src/marks/RasterMark.js +9 -3
- package/src/marks/RasterTileMark.js +8 -2
- package/src/marks/util/is-constant-option.js +2 -1
- package/src/marks/util/permute.js +10 -0
- package/src/marks/util/stats.js +88 -0
- package/src/plot-renderer.js +21 -22
- package/src/transforms/bin.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.14",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.9.0",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.9.0",
|
|
34
34
|
"d3": "^7.9.0",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "89bb9b0dfa747aed691eaeba35379525a6764c61"
|
|
38
38
|
}
|
package/src/index.js
CHANGED
|
@@ -8,6 +8,7 @@ export { ContourMark } from './marks/ContourMark.js';
|
|
|
8
8
|
export { DenseLineMark } from './marks/DenseLineMark.js';
|
|
9
9
|
export { Density1DMark } from './marks/Density1DMark.js';
|
|
10
10
|
export { Density2DMark } from './marks/Density2DMark.js';
|
|
11
|
+
export { ErrorBarMark } from './marks/ErrorBarMark.js';
|
|
11
12
|
export { GeoMark } from './marks/GeoMark.js';
|
|
12
13
|
export { Grid2DMark } from './marks/Grid2DMark.js';
|
|
13
14
|
export { HexbinMark } from './marks/HexbinMark.js';
|
|
@@ -69,7 +69,8 @@ export class Highlight {
|
|
|
69
69
|
for (let i = 0; i < nodes.length; ++i) {
|
|
70
70
|
const node = nodes[i];
|
|
71
71
|
const base = values[i];
|
|
72
|
-
const
|
|
72
|
+
const data = node.__data__;
|
|
73
|
+
const t = test(Array.isArray(data) ? data[0] : data);
|
|
73
74
|
// TODO? handle inherited values / remove attributes
|
|
74
75
|
for (let j = 0; j < channels.length; ++j) {
|
|
75
76
|
const [attr, value] = channels[j];
|
|
@@ -91,9 +92,11 @@ async function predicateFunction(mark, selection) {
|
|
|
91
92
|
|
|
92
93
|
const s = { __: and(pred) };
|
|
93
94
|
const q = mark.query(filter);
|
|
94
|
-
|
|
95
|
+
(q.queries || [q]).forEach(q => {
|
|
96
|
+
q.groupby().length ? q.select(s) : q.$select(s);
|
|
97
|
+
});
|
|
95
98
|
|
|
96
|
-
const data = await mark.coordinator.query(
|
|
99
|
+
const data = await mark.coordinator.query(q);
|
|
97
100
|
const v = data.getChild?.('__');
|
|
98
101
|
return !(data.numRows || data.length) ? (() => false)
|
|
99
102
|
: v ? (i => v.get(i))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { interval } from '@uwdata/mosaic-core';
|
|
2
|
+
import { ascending, min, max, select } from 'd3';
|
|
3
3
|
import { brushX, brushY } from './util/brush.js';
|
|
4
4
|
import { closeTo } from './util/close-to.js';
|
|
5
5
|
import { getField } from './util/get-field.js';
|
|
@@ -52,13 +52,12 @@ export class Interval1D {
|
|
|
52
52
|
|
|
53
53
|
clause(value) {
|
|
54
54
|
const { mark, pixelSize, field, scale } = this;
|
|
55
|
-
return {
|
|
55
|
+
return interval(field, value, {
|
|
56
56
|
source: this,
|
|
57
|
-
schema: { type: 'interval', pixelSize, scales: [scale] },
|
|
58
57
|
clients: this.peers ? mark.plot.markSet : new Set().add(mark),
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
58
|
+
scale,
|
|
59
|
+
pixelSize
|
|
60
|
+
});
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
init(svg, root) {
|
|
@@ -69,6 +68,7 @@ export class Interval1D {
|
|
|
69
68
|
const ry = svg.scale('y').range;
|
|
70
69
|
brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
|
|
71
70
|
|
|
71
|
+
const range = this.value?.map(this.scale.apply).sort(ascending);
|
|
72
72
|
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
73
73
|
root = facets.size() ? facets : select(root ?? svg);
|
|
74
74
|
this.g = root
|
|
@@ -76,7 +76,7 @@ export class Interval1D {
|
|
|
76
76
|
.attr('class', `interval-${channel}`)
|
|
77
77
|
.each(patchScreenCTM)
|
|
78
78
|
.call(brush)
|
|
79
|
-
.call(brush.moveSilent,
|
|
79
|
+
.call(brush.moveSilent, range);
|
|
80
80
|
|
|
81
81
|
if (style) {
|
|
82
82
|
const brushes = this.g.selectAll('rect.selection');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { intervals } from '@uwdata/mosaic-core';
|
|
2
|
+
import { ascending, min, max, select } from 'd3';
|
|
3
3
|
import { brush } from './util/brush.js';
|
|
4
4
|
import { closeTo } from './util/close-to.js';
|
|
5
5
|
import { getField } from './util/get-field.js';
|
|
@@ -7,8 +7,6 @@ import { invert } from './util/invert.js';
|
|
|
7
7
|
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
8
8
|
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
9
9
|
|
|
10
|
-
const asc = (a, b) => a - b;
|
|
11
|
-
|
|
12
10
|
export class Interval2D {
|
|
13
11
|
constructor(mark, {
|
|
14
12
|
selection,
|
|
@@ -44,8 +42,8 @@ export class Interval2D {
|
|
|
44
42
|
let yr = undefined;
|
|
45
43
|
if (extent) {
|
|
46
44
|
const [a, b] = extent;
|
|
47
|
-
xr = [a[0], b[0]].map(v => invert(v, xscale, pixelSize)).sort(
|
|
48
|
-
yr = [a[1], b[1]].map(v => invert(v, yscale, pixelSize)).sort(
|
|
45
|
+
xr = [a[0], b[0]].map(v => invert(v, xscale, pixelSize)).sort(ascending);
|
|
46
|
+
yr = [a[1], b[1]].map(v => invert(v, yscale, pixelSize)).sort(ascending);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
if (!closeTo(xr, value?.[0]) || !closeTo(yr, value?.[1])) {
|
|
@@ -57,15 +55,12 @@ export class Interval2D {
|
|
|
57
55
|
|
|
58
56
|
clause(value) {
|
|
59
57
|
const { mark, pixelSize, xfield, yfield, xscale, yscale } = this;
|
|
60
|
-
return {
|
|
58
|
+
return intervals([xfield, yfield], value, {
|
|
61
59
|
source: this,
|
|
62
|
-
schema: { type: 'interval', pixelSize, scales: [xscale, yscale] },
|
|
63
60
|
clients: this.peers ? mark.plot.markSet : new Set().add(mark),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: null
|
|
68
|
-
};
|
|
61
|
+
scales: [xscale, yscale],
|
|
62
|
+
pixelSize
|
|
63
|
+
});
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
init(svg) {
|
|
@@ -92,8 +87,8 @@ export class Interval2D {
|
|
|
92
87
|
}
|
|
93
88
|
|
|
94
89
|
if (this.value) {
|
|
95
|
-
const [x1, x2] = this.value[0].map(xscale.apply).sort(
|
|
96
|
-
const [y1, y2] = this.value[1].map(yscale.apply).sort(
|
|
90
|
+
const [x1, x2] = this.value[0].map(xscale.apply).sort(ascending);
|
|
91
|
+
const [y1, y2] = this.value[1].map(yscale.apply).sort(ascending);
|
|
97
92
|
this.g.call(brush.moveSilent, [[x1, y1], [x2, y2]]);
|
|
98
93
|
}
|
|
99
94
|
|
|
@@ -1,66 +1,109 @@
|
|
|
1
|
-
import { isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { eq, literal } from '@uwdata/mosaic-sql';
|
|
1
|
+
import { isSelection, points } from '@uwdata/mosaic-core';
|
|
3
2
|
import { select, pointer } from 'd3';
|
|
4
3
|
import { getField } from './util/get-field.js';
|
|
5
4
|
|
|
6
5
|
export class Nearest {
|
|
7
6
|
constructor(mark, {
|
|
8
7
|
selection,
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
pointer,
|
|
9
|
+
channels,
|
|
10
|
+
fields,
|
|
11
|
+
maxRadius = 40
|
|
11
12
|
}) {
|
|
12
13
|
this.mark = mark;
|
|
13
14
|
this.selection = selection;
|
|
14
15
|
this.clients = new Set().add(mark);
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
16
|
+
this.pointer = pointer;
|
|
17
|
+
this.channels = channels || (
|
|
18
|
+
pointer === 'x' ? ['x'] : pointer === 'y' ? ['y'] : ['x', 'y']
|
|
19
|
+
);
|
|
20
|
+
this.fields = fields || this.channels.map(c => getField(mark, [c]));
|
|
21
|
+
this.maxRadius = maxRadius;
|
|
22
|
+
this.valueIndex = -1;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
clause(value) {
|
|
20
|
-
const { clients,
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
source: this,
|
|
24
|
-
schema: { type: 'point' },
|
|
25
|
-
clients,
|
|
26
|
-
value,
|
|
27
|
-
predicate
|
|
28
|
-
};
|
|
26
|
+
const { clients, fields } = this;
|
|
27
|
+
return points(fields, value ? [value] : value, { source: this, clients });
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
init(svg) {
|
|
32
31
|
const that = this;
|
|
33
|
-
const { mark,
|
|
34
|
-
const { data } = mark;
|
|
35
|
-
const
|
|
32
|
+
const { mark, channels, selection, maxRadius } = this;
|
|
33
|
+
const { data: { columns } } = mark;
|
|
34
|
+
const keys = channels.map(c => mark.channelField(c).as);
|
|
35
|
+
const param = !isSelection(selection);
|
|
36
36
|
|
|
37
37
|
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
38
38
|
const root = facets.size() ? facets : select(svg);
|
|
39
|
-
const scale = svg.scale(channel);
|
|
40
|
-
const param = !isSelection(selection);
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
// extract x, y coordinates for data values and determine scale factors
|
|
41
|
+
const xscale = svg.scale('x').apply;
|
|
42
|
+
const yscale = svg.scale('y').apply;
|
|
43
|
+
const X = Array.from(columns[mark.channelField('x').as], xscale);
|
|
44
|
+
const Y = Array.from(columns[mark.channelField('y').as], yscale);
|
|
45
|
+
const sx = this.pointer === 'y' ? 0.01 : 1;
|
|
46
|
+
const sy = this.pointer === 'x' ? 0.01 : 1;
|
|
47
|
+
|
|
48
|
+
// find value nearest to pointer and update param or selection
|
|
49
|
+
// we don't pass undefined values to params, but do allow empty selections
|
|
50
|
+
root.on('pointerenter pointerdown pointermove', function(evt) {
|
|
51
|
+
const [px, py] = pointer(evt, this);
|
|
52
|
+
const i = findNearest(X, Y, px, py, sx, sy, maxRadius);
|
|
53
|
+
if (i !== this.valueIndex) {
|
|
54
|
+
this.valueIndex = i;
|
|
55
|
+
const v = i < 0 ? undefined : keys.map(k => columns[k][i]);
|
|
56
|
+
if (param) {
|
|
57
|
+
if (i > -1) selection.update(v.length > 1 ? v : v[0]);
|
|
58
|
+
} else {
|
|
59
|
+
selection.update(that.clause(v));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
});
|
|
47
63
|
|
|
64
|
+
// if not a selection, we're done
|
|
48
65
|
if (param) return;
|
|
66
|
+
|
|
67
|
+
// clear selection upon pointer exit
|
|
68
|
+
root.on('pointerleave', () => {
|
|
69
|
+
selection.update(that.clause(undefined));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// trigger activation updates
|
|
49
73
|
svg.addEventListener('pointerenter', evt => {
|
|
50
|
-
if (!evt.buttons)
|
|
74
|
+
if (!evt.buttons) {
|
|
75
|
+
const v = this.channels.map(() => 0);
|
|
76
|
+
selection.activate(this.clause(v));
|
|
77
|
+
}
|
|
51
78
|
});
|
|
52
79
|
}
|
|
53
80
|
}
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Find the nearest data point to the pointer. The nearest point
|
|
84
|
+
* is found via Euclidean distance, but with scale factors *sx* and
|
|
85
|
+
* *sy* applied to the x and y distances. For example, to prioritize
|
|
86
|
+
* selection along the x-axis, use *sx* = 1, *sy* = 0.01.
|
|
87
|
+
* @param {number[]} x Array of data point x coordinate values.
|
|
88
|
+
* @param {number[]} y Array of data point y coordinate values.
|
|
89
|
+
* @param {number} px The x coordinate of the pointer.
|
|
90
|
+
* @param {number} py The y coordinate of the pointer.
|
|
91
|
+
* @param {number} sx A scale factor for x coordinate spans.
|
|
92
|
+
* @param {number} sy A scale factor for y coordinate spans.
|
|
93
|
+
* @param {number} maxRadius The maximum pointer distance for selection.
|
|
94
|
+
* @returns {number} An integer index into the data array corresponding
|
|
95
|
+
* to the nearest data point, or -1 if no nearest point is found.
|
|
96
|
+
*/
|
|
97
|
+
function findNearest(x, y, px, py, sx, sy, maxRadius) {
|
|
98
|
+
let dist = maxRadius * maxRadius;
|
|
99
|
+
let nearest = -1;
|
|
100
|
+
for (let i = 0; i < x.length; ++i) {
|
|
101
|
+
const dx = sx * (x[i] - px);
|
|
102
|
+
const dy = sy * (y[i] - py);
|
|
103
|
+
const dd = dx * dx + dy * dy;
|
|
104
|
+
if (dd <= dist) {
|
|
105
|
+
dist = dd;
|
|
106
|
+
nearest = i;
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
return nearest;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { interval, Selection } from '@uwdata/mosaic-core';
|
|
1
2
|
import { select, zoom, ZoomTransform } from 'd3';
|
|
2
|
-
import { Selection } from '@uwdata/mosaic-core';
|
|
3
|
-
import { isBetween } from '@uwdata/mosaic-sql';
|
|
4
3
|
import { getField } from './util/get-field.js';
|
|
5
4
|
|
|
6
5
|
const asc = (a, b) => a - b;
|
|
@@ -49,13 +48,11 @@ export class PanZoom {
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
clause(value, field, scale) {
|
|
52
|
-
return {
|
|
51
|
+
return interval(field, value, {
|
|
53
52
|
source: this,
|
|
54
|
-
schema: { type: 'interval', scales: [scale] },
|
|
55
53
|
clients: this.mark.plot.markSet,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
54
|
+
scale
|
|
55
|
+
});
|
|
59
56
|
}
|
|
60
57
|
|
|
61
58
|
init(svg) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { points } from '@uwdata/mosaic-core';
|
|
2
2
|
|
|
3
3
|
export class Toggle {
|
|
4
4
|
/**
|
|
@@ -14,49 +14,40 @@ export class Toggle {
|
|
|
14
14
|
this.mark = mark;
|
|
15
15
|
this.selection = selection;
|
|
16
16
|
this.peers = peers;
|
|
17
|
-
|
|
17
|
+
const fields = this.fields = [];
|
|
18
|
+
const as = this.as = [];
|
|
19
|
+
channels.forEach(c => {
|
|
18
20
|
const q = c === 'color' ? ['color', 'fill', 'stroke']
|
|
19
21
|
: c === 'x' ? ['x', 'x1', 'x2']
|
|
20
22
|
: c === 'y' ? ['y', 'y1', 'y2']
|
|
21
23
|
: [c];
|
|
22
24
|
for (let i = 0; i < q.length; ++i) {
|
|
23
25
|
const f = mark.channelField(q[i], { exact: true });
|
|
24
|
-
if (f)
|
|
25
|
-
|
|
26
|
-
as
|
|
27
|
-
|
|
26
|
+
if (f) {
|
|
27
|
+
fields.push(f.field?.basis || f.field);
|
|
28
|
+
as.push(f.as);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
throw new Error(`Missing channel: ${c}`);
|
|
30
33
|
});
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
clause(value) {
|
|
34
|
-
const {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (value) {
|
|
38
|
-
const clauses = value.map(vals => {
|
|
39
|
-
const list = vals.map((v, i) => {
|
|
40
|
-
return isNotDistinct(channels[i].field, literal(v));
|
|
41
|
-
});
|
|
42
|
-
return list.length > 1 ? and(list) : list[0];
|
|
43
|
-
});
|
|
44
|
-
predicate = clauses.length > 1 ? or(clauses) : clauses[0];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return {
|
|
37
|
+
const { fields, mark } = this;
|
|
38
|
+
return points(fields, value, {
|
|
48
39
|
source: this,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
value,
|
|
52
|
-
predicate
|
|
53
|
-
};
|
|
40
|
+
clients: this.peers ? mark.plot.markSet : new Set().add(mark)
|
|
41
|
+
});
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
init(svg, selector, accessor) {
|
|
57
|
-
const { mark,
|
|
45
|
+
const { mark, as, selection } = this;
|
|
58
46
|
const { data: { columns = {} } = {} } = mark;
|
|
59
|
-
accessor ??= target =>
|
|
47
|
+
accessor ??= target => as.map(name => {
|
|
48
|
+
const data = target.__data__;
|
|
49
|
+
return columns[name][Array.isArray(data) ? data[0] : data];
|
|
50
|
+
});
|
|
60
51
|
selector ??= `[data-index="${mark.index}"]`;
|
|
61
52
|
const groups = new Set(svg.querySelectorAll(selector));
|
|
62
53
|
|
|
@@ -85,7 +76,7 @@ export class Toggle {
|
|
|
85
76
|
|
|
86
77
|
svg.addEventListener('pointerenter', evt => {
|
|
87
78
|
if (evt.buttons) return;
|
|
88
|
-
this.selection.activate(this.clause([this.
|
|
79
|
+
this.selection.activate(this.clause([this.fields.map(() => 0)]));
|
|
89
80
|
});
|
|
90
81
|
}
|
|
91
82
|
}
|
package/src/legend.js
CHANGED
|
@@ -19,7 +19,7 @@ export class Legend {
|
|
|
19
19
|
|
|
20
20
|
this.element = document.createElement('div');
|
|
21
21
|
this.element.setAttribute('class', 'legend');
|
|
22
|
-
Object.
|
|
22
|
+
Object.defineProperty(this.element, 'value', { value: this });
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
setPlot(plot) {
|
|
@@ -35,8 +35,13 @@ export class Legend {
|
|
|
35
35
|
|
|
36
36
|
update() {
|
|
37
37
|
if (!this.legend) return;
|
|
38
|
-
const {
|
|
39
|
-
const
|
|
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
|
+
|
|
40
45
|
const nodes = this.legend.querySelectorAll(TOGGLE_SELECTOR);
|
|
41
46
|
for (const node of nodes) {
|
|
42
47
|
const selected = curr ? curr.has(node.__data__) : true;
|
|
@@ -103,11 +108,19 @@ function getInteractor(legend, type) {
|
|
|
103
108
|
// otherwise instantiate an appropriate interactor
|
|
104
109
|
const mark = interactorMark(legend);
|
|
105
110
|
if (type === SWATCH) {
|
|
106
|
-
legend.handler = new Toggle(mark, {
|
|
111
|
+
legend.handler = new Toggle(mark, {
|
|
112
|
+
selection,
|
|
113
|
+
channels: [channel],
|
|
114
|
+
peers: false
|
|
115
|
+
});
|
|
107
116
|
selection.addEventListener('value', () => legend.update());
|
|
108
117
|
} else {
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
legend.handler = new Interval1D(mark, {
|
|
119
|
+
selection,
|
|
120
|
+
channel,
|
|
121
|
+
brush: { fill: 'none', stroke: 'currentColor' },
|
|
122
|
+
peers: false
|
|
123
|
+
});
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
return legend.handler;
|
|
@@ -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/Grid2DMark.js
CHANGED
|
@@ -61,7 +61,7 @@ export class Grid2DMark extends Mark {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* @param {import('../plot.js').Plot} plot The plot.
|
|
64
|
-
* @param {number} index
|
|
64
|
+
* @param {number} index
|
|
65
65
|
*/
|
|
66
66
|
setPlot(plot, index) {
|
|
67
67
|
const update = () => { if (this.hasFieldInfo()) this.requestUpdate(); };
|
package/src/marks/Mark.js
CHANGED
|
@@ -91,7 +91,7 @@ export class Mark extends MosaicClient {
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* @param {import('../plot.js').Plot} plot The plot.
|
|
94
|
-
* @param {number} index
|
|
94
|
+
* @param {number} index
|
|
95
95
|
*/
|
|
96
96
|
setPlot(plot, index) {
|
|
97
97
|
this.plot = plot;
|
|
@@ -181,24 +181,8 @@ export class Mark extends MosaicClient {
|
|
|
181
181
|
* @returns {object[]}
|
|
182
182
|
*/
|
|
183
183
|
plotSpecs() {
|
|
184
|
-
const { type, detail, channels } = this;
|
|
185
|
-
|
|
186
|
-
const { numRows: length, values, columns } = this.data || {};
|
|
187
|
-
|
|
188
|
-
// populate plot specification options
|
|
189
|
-
const options = {};
|
|
190
|
-
const side = {};
|
|
191
|
-
for (const c of channels) {
|
|
192
|
-
const obj = detail.has(c.channel) ? side : options;
|
|
193
|
-
obj[c.channel] = channelOption(c, columns);
|
|
194
|
-
}
|
|
195
|
-
if (detail.size) options.channels = side;
|
|
196
|
-
|
|
197
|
-
// if provided raw source values (not objects) pass as-is
|
|
198
|
-
// otherwise we pass columnar data directy in the options
|
|
199
|
-
const data = values ?? (this.data ? { length } : null);
|
|
200
|
-
const spec = [{ type, data, options }];
|
|
201
|
-
return spec;
|
|
184
|
+
const { type, data, detail, channels } = this;
|
|
185
|
+
return markPlotSpec(type, detail, channels, data);
|
|
202
186
|
}
|
|
203
187
|
}
|
|
204
188
|
|
|
@@ -258,3 +242,27 @@ export function markQuery(channels, table, skip = []) {
|
|
|
258
242
|
|
|
259
243
|
return q;
|
|
260
244
|
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generate an array of Plot mark specifications.
|
|
249
|
+
* @returns {object[]}
|
|
250
|
+
*/
|
|
251
|
+
export function markPlotSpec(type, detail, channels, data, options = {}) {
|
|
252
|
+
// @ts-ignore
|
|
253
|
+
const { numRows: length, values, columns } = data ?? {};
|
|
254
|
+
|
|
255
|
+
// populate plot specification options
|
|
256
|
+
const side = {};
|
|
257
|
+
for (const c of channels) {
|
|
258
|
+
const obj = detail.has(c.channel) ? side : options;
|
|
259
|
+
obj[c.channel] = channelOption(c, columns);
|
|
260
|
+
}
|
|
261
|
+
if (detail.size) options.channels = side;
|
|
262
|
+
|
|
263
|
+
// if provided raw source values (not objects) pass as-is
|
|
264
|
+
// otherwise we pass columnar data directy in the options
|
|
265
|
+
const specData = values ?? (data ? { length } : null);
|
|
266
|
+
const spec = [{ type, data: specData, options }];
|
|
267
|
+
return spec;
|
|
268
|
+
}
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ascending } from 'd3';
|
|
|
2
2
|
import { scale } from '@observablehq/plot';
|
|
3
3
|
import { gridDomainContinuous, gridDomainDiscrete } from './util/grid.js';
|
|
4
4
|
import { isColor } from './util/is-color.js';
|
|
5
|
+
import { indices, permute } from './util/permute.js';
|
|
5
6
|
import { alphaScheme, alphaConstant, colorConstant, colorCategory, colorScheme, createCanvas } from './util/raster.js';
|
|
6
7
|
import { DENSITY, Grid2DMark } from './Grid2DMark.js';
|
|
7
8
|
import { Fixed, Transient } from '../symbols.js';
|
|
@@ -46,13 +47,18 @@ export class RasterMark extends Grid2DMark {
|
|
|
46
47
|
const alphaData = columns[alphaProp] ?? [];
|
|
47
48
|
const colorData = columns[colorProp] ?? [];
|
|
48
49
|
|
|
50
|
+
// determine raster order
|
|
51
|
+
const idx = numRows > 1 && colorProp && this.groupby?.includes(colorProp)
|
|
52
|
+
? permute(colorData, this.plot.getAttribute('colorDomain'))
|
|
53
|
+
: indices(numRows);
|
|
54
|
+
|
|
49
55
|
// generate rasters
|
|
50
56
|
this.data = {
|
|
51
57
|
numRows,
|
|
52
58
|
columns: {
|
|
53
59
|
src: Array.from({ length: numRows }, (_, i) => {
|
|
54
|
-
color?.(img.data, w, h, colorData[i]);
|
|
55
|
-
alpha?.(img.data, w, h, alphaData[i]);
|
|
60
|
+
color?.(img.data, w, h, colorData[idx[i]]);
|
|
61
|
+
alpha?.(img.data, w, h, alphaData[idx[i]]);
|
|
56
62
|
ctx.putImageData(img, 0, 0);
|
|
57
63
|
return canvas.toDataURL();
|
|
58
64
|
})
|
|
@@ -196,7 +202,7 @@ function colorScale(mark, prop) {
|
|
|
196
202
|
const domainFixed = domainAttr === Fixed;
|
|
197
203
|
const domainTransient = domainAttr?.[Transient];
|
|
198
204
|
const domain = (!domainFixed && !domainTransient && domainAttr) || (
|
|
199
|
-
flat ? data.sort(ascending)
|
|
205
|
+
flat ? data.slice().sort(ascending)
|
|
200
206
|
: discrete ? gridDomainDiscrete(data)
|
|
201
207
|
: gridDomainContinuous(data)
|
|
202
208
|
);
|
|
@@ -2,6 +2,7 @@ import { coordinator } from '@uwdata/mosaic-core';
|
|
|
2
2
|
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { binExpr } from './util/bin-expr.js';
|
|
4
4
|
import { extentX, extentY } from './util/extent.js';
|
|
5
|
+
import { indices, permute } from './util/permute.js';
|
|
5
6
|
import { createCanvas } from './util/raster.js';
|
|
6
7
|
import { Grid2DMark } from './Grid2DMark.js';
|
|
7
8
|
import { rasterEncoding } from './RasterMark.js';
|
|
@@ -181,13 +182,18 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
181
182
|
const alphaData = columns[alphaProp] ?? [];
|
|
182
183
|
const colorData = columns[colorProp] ?? [];
|
|
183
184
|
|
|
185
|
+
// determine raster order
|
|
186
|
+
const idx = numRows > 1 && colorProp && this.groupby?.includes(colorProp)
|
|
187
|
+
? permute(colorData, this.plot.getAttribute('colorDomain'))
|
|
188
|
+
: indices(numRows);
|
|
189
|
+
|
|
184
190
|
// generate rasters
|
|
185
191
|
this.data = {
|
|
186
192
|
numRows,
|
|
187
193
|
columns: {
|
|
188
194
|
src: Array.from({ length: numRows }, (_, i) => {
|
|
189
|
-
color?.(img.data, w, h, colorData[i]);
|
|
190
|
-
alpha?.(img.data, w, h, alphaData[i]);
|
|
195
|
+
color?.(img.data, w, h, colorData[idx[i]]);
|
|
196
|
+
alpha?.(img.data, w, h, alphaData[idx[i]]);
|
|
191
197
|
ctx.putImageData(img, 0, 0);
|
|
192
198
|
return canvas.toDataURL();
|
|
193
199
|
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function indices(length) {
|
|
2
|
+
return Array.from({ length }, (_, i) => i);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function permute(data, order) {
|
|
6
|
+
const ord = order.reduce((acc, val, i) => (acc[val] = i, acc), {});
|
|
7
|
+
const idx = indices(data.length);
|
|
8
|
+
idx.sort((a, b) => ord[data[a]] - ord[data[b]]);
|
|
9
|
+
return idx;
|
|
10
|
+
}
|