@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/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",
|
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"prebuild": "rimraf dist && mkdir dist",
|
|
25
25
|
"build": "node ../../esbuild.js mosaic-plot",
|
|
26
|
-
"lint": "eslint src test
|
|
26
|
+
"lint": "eslint src test",
|
|
27
27
|
"test": "mocha 'test/**/*-test.js'",
|
|
28
28
|
"prepublishOnly": "npm run test && npm run lint && npm run build"
|
|
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';
|
|
@@ -11,7 +11,7 @@ export class Interval1D {
|
|
|
11
11
|
constructor(mark, {
|
|
12
12
|
channel,
|
|
13
13
|
selection,
|
|
14
|
-
field,
|
|
14
|
+
field = undefined,
|
|
15
15
|
pixelSize = 1,
|
|
16
16
|
peers = true,
|
|
17
17
|
brush: style
|
|
@@ -52,16 +52,15 @@ 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
|
-
init(svg) {
|
|
63
|
+
init(svg, root) {
|
|
65
64
|
const { brush, channel, style } = this;
|
|
66
65
|
this.scale = svg.scale(channel);
|
|
67
66
|
|
|
@@ -69,14 +68,15 @@ 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
|
|
75
75
|
.append('g')
|
|
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');
|
|
@@ -85,6 +85,8 @@ export class Interval1D {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
svg.addEventListener('pointerenter',
|
|
88
|
+
svg.addEventListener('pointerenter', evt => {
|
|
89
|
+
if (!evt.buttons) this.activate();
|
|
90
|
+
});
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -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,11 +87,13 @@ 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
|
|
|
100
|
-
svg.addEventListener('pointerenter',
|
|
95
|
+
svg.addEventListener('pointerenter', evt => {
|
|
96
|
+
if (!evt.buttons) this.activate();
|
|
97
|
+
});
|
|
101
98
|
}
|
|
102
99
|
}
|
|
@@ -1,66 +1,110 @@
|
|
|
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;
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
|
|
67
|
+
// clear selection upon pointer exit
|
|
68
|
+
root.on('pointerleave', () => {
|
|
69
|
+
selection.update(that.clause(undefined));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// trigger activation updates
|
|
73
|
+
svg.addEventListener('pointerenter', evt => {
|
|
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
|
-
|
|
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;
|
|
63
107
|
}
|
|
64
|
-
}
|
|
65
|
-
return
|
|
108
|
+
}
|
|
109
|
+
return nearest;
|
|
66
110
|
}
|
|
@@ -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) {
|
|
@@ -86,8 +83,9 @@ export class PanZoom {
|
|
|
86
83
|
|
|
87
84
|
if (panx || pany) {
|
|
88
85
|
let enter = false;
|
|
89
|
-
element.addEventListener('
|
|
86
|
+
element.addEventListener('pointerenter', evt => {
|
|
90
87
|
if (enter) return; else enter = true;
|
|
88
|
+
if (evt.buttons) return; // don't activate if mouse down
|
|
91
89
|
if (panx) {
|
|
92
90
|
const { xscale, xfield } = this;
|
|
93
91
|
xsel.activate(this.clause(xscale.domain, xfield, xscale));
|
|
@@ -97,7 +95,7 @@ export class PanZoom {
|
|
|
97
95
|
ysel.activate(this.clause(yscale.domain, yfield, yscale));
|
|
98
96
|
}
|
|
99
97
|
});
|
|
100
|
-
element.addEventListener('
|
|
98
|
+
element.addEventListener('pointerleave', () => enter = false);
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
101
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { points } from '@uwdata/mosaic-core';
|
|
2
2
|
|
|
3
3
|
export class Toggle {
|
|
4
|
+
/**
|
|
5
|
+
* @param {*} mark The mark to interact with.
|
|
6
|
+
* @param {*} options The interactor options.
|
|
7
|
+
*/
|
|
4
8
|
constructor(mark, {
|
|
5
9
|
selection,
|
|
6
10
|
channels,
|
|
@@ -10,54 +14,41 @@ export class Toggle {
|
|
|
10
14
|
this.mark = mark;
|
|
11
15
|
this.selection = selection;
|
|
12
16
|
this.peers = peers;
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
const fields = this.fields = [];
|
|
18
|
+
const as = this.as = [];
|
|
19
|
+
channels.forEach(c => {
|
|
20
|
+
const q = c === 'color' ? ['color', 'fill', 'stroke']
|
|
15
21
|
: c === 'x' ? ['x', 'x1', 'x2']
|
|
16
22
|
: c === 'y' ? ['y', 'y1', 'y2']
|
|
17
23
|
: [c];
|
|
18
24
|
for (let i = 0; i < q.length; ++i) {
|
|
19
|
-
const f = mark.channelField(q[i]);
|
|
20
|
-
if (f)
|
|
21
|
-
|
|
22
|
-
as
|
|
23
|
-
|
|
25
|
+
const f = mark.channelField(q[i], { exact: true });
|
|
26
|
+
if (f) {
|
|
27
|
+
fields.push(f.field?.basis || f.field);
|
|
28
|
+
as.push(f.as);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
throw new Error(`Missing channel: ${c}`);
|
|
26
33
|
});
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
clause(value) {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (value) {
|
|
34
|
-
const clauses = value.map(vals => {
|
|
35
|
-
const list = vals.map((v, i) => {
|
|
36
|
-
return isNotDistinct(channels[i].field, literal(v));
|
|
37
|
-
});
|
|
38
|
-
return list.length > 1 ? and(list) : list[0];
|
|
39
|
-
});
|
|
40
|
-
predicate = clauses.length > 1 ? or(clauses) : clauses[0];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {
|
|
37
|
+
const { fields, mark } = this;
|
|
38
|
+
return points(fields, value, {
|
|
44
39
|
source: this,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
value,
|
|
48
|
-
predicate
|
|
49
|
-
};
|
|
40
|
+
clients: this.peers ? mark.plot.markSet : new Set().add(mark)
|
|
41
|
+
});
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
init(svg, selector, accessor) {
|
|
53
|
-
const { mark,
|
|
54
|
-
const { data } = mark;
|
|
55
|
-
accessor
|
|
56
|
-
const
|
|
57
|
-
return
|
|
45
|
+
const { mark, as, selection } = this;
|
|
46
|
+
const { data: { columns = {} } = {} } = mark;
|
|
47
|
+
accessor ??= target => as.map(name => {
|
|
48
|
+
const data = target.__data__;
|
|
49
|
+
return columns[name][Array.isArray(data) ? data[0] : data];
|
|
58
50
|
});
|
|
59
|
-
|
|
60
|
-
selector = selector || `[data-index="${mark.index}"]`;
|
|
51
|
+
selector ??= `[data-index="${mark.index}"]`;
|
|
61
52
|
const groups = new Set(svg.querySelectorAll(selector));
|
|
62
53
|
|
|
63
54
|
svg.addEventListener('pointerdown', evt => {
|
|
@@ -67,7 +58,7 @@ export class Toggle {
|
|
|
67
58
|
|
|
68
59
|
if (isTargetElement(groups, target)) {
|
|
69
60
|
const point = accessor(target);
|
|
70
|
-
if (evt.shiftKey && state?.length) {
|
|
61
|
+
if ((evt.shiftKey || evt.metaKey) && state?.length) {
|
|
71
62
|
value = state.filter(s => neq(s, point));
|
|
72
63
|
if (value.length === state.length) value.push(point);
|
|
73
64
|
} else if (state?.length === 1 && !neq(state[0], point)) {
|
|
@@ -83,8 +74,9 @@ export class Toggle {
|
|
|
83
74
|
}
|
|
84
75
|
});
|
|
85
76
|
|
|
86
|
-
svg.addEventListener('pointerenter',
|
|
87
|
-
|
|
77
|
+
svg.addEventListener('pointerenter', evt => {
|
|
78
|
+
if (evt.buttons) return;
|
|
79
|
+
this.selection.activate(this.clause([this.fields.map(() => 0)]));
|
|
88
80
|
});
|
|
89
81
|
}
|
|
90
82
|
}
|