@uwdata/mosaic-plot 0.8.0 → 0.10.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 +2150 -1439
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +6 -6
- 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 +79 -33
- package/src/interactors/PanZoom.js +4 -7
- package/src/interactors/Toggle.js +19 -28
- package/src/legend.js +26 -8
- package/src/marks/Density1DMark.js +1 -1
- package/src/marks/ErrorBarMark.js +50 -0
- package/src/marks/Grid2DMark.js +2 -2
- package/src/marks/HexbinMark.js +39 -49
- package/src/marks/Mark.js +28 -21
- package/src/marks/RasterMark.js +9 -3
- package/src/marks/RasterTileMark.js +8 -2
- package/src/marks/RegressionMark.js +2 -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-attributes.js +1 -0
- package/src/plot-renderer.js +21 -22
- package/src/plot.js +14 -2
- package/src/transforms/bin-step.js +43 -0
- package/src/transforms/bin.js +40 -48
- package/src/transforms/time-interval.js +53 -0
- package/src/marks/util/to-data-columns.js +0 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "A Mosaic-powered plotting framework based on Observable Plot.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"mosaic"
|
|
11
11
|
],
|
|
12
12
|
"license": "BSD-3-Clause",
|
|
13
|
-
"author": "Jeffrey Heer (
|
|
13
|
+
"author": "Jeffrey Heer (https://idl.uw.edu)",
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "src/index.js",
|
|
16
16
|
"module": "src/index.js",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@observablehq/plot": "^0.6.
|
|
32
|
-
"@uwdata/mosaic-core": "^0.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.
|
|
31
|
+
"@observablehq/plot": "^0.6.15",
|
|
32
|
+
"@uwdata/mosaic-core": "^0.10.0",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.10.0",
|
|
34
34
|
"d3": "^7.9.0",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "94fc4f0d4efc622001f6afd6714d1e9dda745be2"
|
|
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 { clauseInterval } 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 clauseInterval(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 { clauseIntervals } 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 clauseIntervals([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,112 @@
|
|
|
1
|
-
import { isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { eq, literal } from '@uwdata/mosaic-sql';
|
|
1
|
+
import { clausePoints, isSelection } 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 {
|
|
26
|
+
const { clients, fields } = this;
|
|
27
|
+
return clausePoints(fields, value ? [value] : value, {
|
|
23
28
|
source: this,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
value,
|
|
27
|
-
predicate
|
|
28
|
-
};
|
|
29
|
+
clients
|
|
30
|
+
});
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
init(svg) {
|
|
32
34
|
const that = this;
|
|
33
|
-
const { mark,
|
|
34
|
-
const { data } = mark;
|
|
35
|
-
const
|
|
35
|
+
const { mark, channels, selection, maxRadius } = this;
|
|
36
|
+
const { data: { columns } } = mark;
|
|
37
|
+
const keys = channels.map(c => mark.channelField(c).as);
|
|
38
|
+
const param = !isSelection(selection);
|
|
36
39
|
|
|
37
40
|
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
38
41
|
const root = facets.size() ? facets : select(svg);
|
|
39
|
-
const scale = svg.scale(channel);
|
|
40
|
-
const param = !isSelection(selection);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// extract x, y coordinates for data values and determine scale factors
|
|
44
|
+
const xscale = svg.scale('x').apply;
|
|
45
|
+
const yscale = svg.scale('y').apply;
|
|
46
|
+
const X = Array.from(columns[mark.channelField('x').as], xscale);
|
|
47
|
+
const Y = Array.from(columns[mark.channelField('y').as], yscale);
|
|
48
|
+
const sx = this.pointer === 'y' ? 0.01 : 1;
|
|
49
|
+
const sy = this.pointer === 'x' ? 0.01 : 1;
|
|
50
|
+
|
|
51
|
+
// find value nearest to pointer and update param or selection
|
|
52
|
+
// we don't pass undefined values to params, but do allow empty selections
|
|
53
|
+
root.on('pointerenter pointerdown pointermove', function(evt) {
|
|
54
|
+
const [px, py] = pointer(evt, this);
|
|
55
|
+
const i = findNearest(X, Y, px, py, sx, sy, maxRadius);
|
|
56
|
+
if (i !== this.valueIndex) {
|
|
57
|
+
this.valueIndex = i;
|
|
58
|
+
const v = i < 0 ? undefined : keys.map(k => columns[k][i]);
|
|
59
|
+
if (param) {
|
|
60
|
+
if (i > -1) selection.update(v.length > 1 ? v : v[0]);
|
|
61
|
+
} else {
|
|
62
|
+
selection.update(that.clause(v));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
});
|
|
47
66
|
|
|
67
|
+
// if not a selection, we're done
|
|
48
68
|
if (param) return;
|
|
69
|
+
|
|
70
|
+
// clear selection upon pointer exit
|
|
71
|
+
root.on('pointerleave', () => {
|
|
72
|
+
selection.update(that.clause(undefined));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// trigger activation updates
|
|
49
76
|
svg.addEventListener('pointerenter', evt => {
|
|
50
|
-
if (!evt.buttons)
|
|
77
|
+
if (!evt.buttons) {
|
|
78
|
+
const v = this.channels.map(() => 0);
|
|
79
|
+
selection.activate(this.clause(v));
|
|
80
|
+
}
|
|
51
81
|
});
|
|
52
82
|
}
|
|
53
83
|
}
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Find the nearest data point to the pointer. The nearest point
|
|
87
|
+
* is found via Euclidean distance, but with scale factors *sx* and
|
|
88
|
+
* *sy* applied to the x and y distances. For example, to prioritize
|
|
89
|
+
* selection along the x-axis, use *sx* = 1, *sy* = 0.01.
|
|
90
|
+
* @param {number[]} x Array of data point x coordinate values.
|
|
91
|
+
* @param {number[]} y Array of data point y coordinate values.
|
|
92
|
+
* @param {number} px The x coordinate of the pointer.
|
|
93
|
+
* @param {number} py The y coordinate of the pointer.
|
|
94
|
+
* @param {number} sx A scale factor for x coordinate spans.
|
|
95
|
+
* @param {number} sy A scale factor for y coordinate spans.
|
|
96
|
+
* @param {number} maxRadius The maximum pointer distance for selection.
|
|
97
|
+
* @returns {number} An integer index into the data array corresponding
|
|
98
|
+
* to the nearest data point, or -1 if no nearest point is found.
|
|
99
|
+
*/
|
|
100
|
+
function findNearest(x, y, px, py, sx, sy, maxRadius) {
|
|
101
|
+
let dist = maxRadius * maxRadius;
|
|
102
|
+
let nearest = -1;
|
|
103
|
+
for (let i = 0; i < x.length; ++i) {
|
|
104
|
+
const dx = sx * (x[i] - px);
|
|
105
|
+
const dy = sy * (y[i] - py);
|
|
106
|
+
const dd = dx * dx + dy * dy;
|
|
107
|
+
if (dd <= dist) {
|
|
108
|
+
dist = dd;
|
|
109
|
+
nearest = i;
|
|
64
110
|
}
|
|
65
111
|
}
|
|
66
112
|
return nearest;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import { Selection, clauseInterval } 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 clauseInterval(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 { clausePoints } 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 clausePoints(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
|
@@ -10,7 +10,7 @@ export class Legend {
|
|
|
10
10
|
constructor(channel, options) {
|
|
11
11
|
const { as, field, ...rest } = options;
|
|
12
12
|
this.channel = channel;
|
|
13
|
-
this.options =
|
|
13
|
+
this.options = rest;
|
|
14
14
|
this.type = null;
|
|
15
15
|
this.handler = null;
|
|
16
16
|
this.selection = as;
|
|
@@ -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;
|
|
@@ -46,10 +51,15 @@ export class Legend {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function createLegend(legend, svg) {
|
|
49
|
-
const { channel,
|
|
54
|
+
const { channel, plot, selection } = legend;
|
|
50
55
|
const scale = svg.scale(channel);
|
|
51
56
|
const type = scale.type === 'ordinal' ? SWATCH : RAMP;
|
|
52
57
|
|
|
58
|
+
const options = {
|
|
59
|
+
label: plot.getAttribute(`${channel}Label`) ?? null,
|
|
60
|
+
...legend.options
|
|
61
|
+
};
|
|
62
|
+
|
|
53
63
|
// labels for swatch legends are not yet supported by Plot
|
|
54
64
|
// track here: https://github.com/observablehq/plot/issues/834
|
|
55
65
|
// for consistent layout, adjust sizing when there is no label
|
|
@@ -103,11 +113,19 @@ function getInteractor(legend, type) {
|
|
|
103
113
|
// otherwise instantiate an appropriate interactor
|
|
104
114
|
const mark = interactorMark(legend);
|
|
105
115
|
if (type === SWATCH) {
|
|
106
|
-
legend.handler = new Toggle(mark, {
|
|
116
|
+
legend.handler = new Toggle(mark, {
|
|
117
|
+
selection,
|
|
118
|
+
channels: [channel],
|
|
119
|
+
peers: false
|
|
120
|
+
});
|
|
107
121
|
selection.addEventListener('value', () => legend.update());
|
|
108
122
|
} else {
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
legend.handler = new Interval1D(mark, {
|
|
124
|
+
selection,
|
|
125
|
+
channel,
|
|
126
|
+
brush: { fill: 'none', stroke: 'currentColor' },
|
|
127
|
+
peers: false
|
|
128
|
+
});
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
return legend.handler;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
1
2
|
import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
|
|
2
3
|
import { Transient } from '../symbols.js';
|
|
3
4
|
import { binExpr } from './util/bin-expr.js';
|
|
@@ -6,7 +7,6 @@ import { extentX, extentY, xext, yext } from './util/extent.js';
|
|
|
6
7
|
import { grid1d } from './util/grid.js';
|
|
7
8
|
import { handleParam } from './util/handle-param.js';
|
|
8
9
|
import { Mark, channelOption, markQuery } from './Mark.js';
|
|
9
|
-
import { toDataColumns } from './util/to-data-columns.js';
|
|
10
10
|
|
|
11
11
|
export class Density1DMark extends Mark {
|
|
12
12
|
constructor(type, source, options) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
2
|
+
import { avg, count, stddev } from '@uwdata/mosaic-sql';
|
|
3
|
+
import { erfinv } from './util/stats.js';
|
|
4
|
+
import { Mark, markPlotSpec, markQuery } from './Mark.js';
|
|
5
|
+
import { handleParam } from './util/handle-param.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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk } from '@observablehq/plot';
|
|
2
|
+
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
2
3
|
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
4
|
import { Transient } from '../symbols.js';
|
|
4
5
|
import { binExpr } from './util/bin-expr.js';
|
|
@@ -6,7 +7,6 @@ import { dericheConfig, dericheConv2d } from './util/density.js';
|
|
|
6
7
|
import { extentX, extentY, xyext } from './util/extent.js';
|
|
7
8
|
import { grid2d } from './util/grid.js';
|
|
8
9
|
import { handleParam } from './util/handle-param.js';
|
|
9
|
-
import { toDataColumns } from './util/to-data-columns.js';
|
|
10
10
|
import { Mark } from './Mark.js';
|
|
11
11
|
|
|
12
12
|
export const DENSITY = 'density';
|
|
@@ -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(); };
|