@uwdata/mosaic-plot 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/mosaic-plot.js +3694 -2393
- package/dist/mosaic-plot.min.js +14 -14
- package/package.json +4 -4
- package/src/index.js +2 -1
- package/src/interactors/Highlight.js +8 -6
- package/src/interactors/Interval1D.js +4 -11
- package/src/interactors/Interval2D.js +8 -15
- package/src/interactors/Nearest.js +39 -8
- package/src/interactors/Region.js +108 -0
- package/src/interactors/Toggle.js +6 -23
- package/src/interactors/util/brush.js +35 -3
- package/src/interactors/util/get-datum.js +15 -0
- package/src/interactors/util/get-field.js +37 -2
- package/src/interactors/util/intersect.js +267 -0
- package/src/interactors/util/neq.js +14 -0
- package/src/interactors/util/parse-path.js +79 -0
- package/src/marks/ConnectedMark.js +10 -33
- package/src/marks/DenseLineMark.js +8 -88
- package/src/marks/Density1DMark.js +4 -26
- package/src/marks/ErrorBarMark.js +7 -8
- package/src/marks/Grid2DMark.js +11 -52
- package/src/marks/HexbinMark.js +56 -23
- package/src/marks/Mark.js +29 -26
- package/src/marks/RasterMark.js +1 -0
- package/src/marks/RasterTileMark.js +2 -2
- package/src/marks/RegressionMark.js +3 -3
- package/src/marks/util/bin-expr.js +4 -9
- package/src/marks/util/extent.js +9 -12
- package/src/plot-renderer.js +23 -48
- package/src/transforms/bin.js +49 -38
- package/src/transforms/index.js +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "A Mosaic-powered plotting framework based on Observable Plot.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@observablehq/plot": "^0.6.16",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.12.1",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.12.1",
|
|
34
34
|
"d3": "^7.9.0",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "fe3a7c34352da54ec36a1ebf557846f46a649782"
|
|
38
38
|
}
|
package/src/index.js
CHANGED
|
@@ -22,10 +22,11 @@ export { Interval1D } from './interactors/Interval1D.js';
|
|
|
22
22
|
export { Interval2D } from './interactors/Interval2D.js';
|
|
23
23
|
export { Nearest } from './interactors/Nearest.js';
|
|
24
24
|
export { PanZoom } from './interactors/PanZoom.js';
|
|
25
|
+
export { Region } from './interactors/Region.js';
|
|
25
26
|
export { Toggle } from './interactors/Toggle.js';
|
|
26
27
|
|
|
27
28
|
// legend
|
|
28
29
|
export { Legend } from './legend.js';
|
|
29
30
|
|
|
30
31
|
// transforms
|
|
31
|
-
export { bin } from './transforms/
|
|
32
|
+
export { bin } from './transforms/bin.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { throttle } from '@uwdata/mosaic-core';
|
|
2
|
-
import { and } from '@uwdata/mosaic-sql';
|
|
2
|
+
import { and, isAggregateExpression } from '@uwdata/mosaic-sql';
|
|
3
|
+
import { getDatum } from './util/get-datum.js';
|
|
3
4
|
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
4
5
|
|
|
5
6
|
function configureMark(mark) {
|
|
@@ -13,7 +14,7 @@ function configureMark(mark) {
|
|
|
13
14
|
if (channel === 'orderby') {
|
|
14
15
|
ordered = true;
|
|
15
16
|
} else if (field) {
|
|
16
|
-
if (field
|
|
17
|
+
if (isAggregateExpression(field)) {
|
|
17
18
|
aggregate = true;
|
|
18
19
|
} else {
|
|
19
20
|
if (dims.has(as)) continue;
|
|
@@ -49,7 +50,9 @@ export class Highlight {
|
|
|
49
50
|
this.svg = svg;
|
|
50
51
|
const values = this.values = [];
|
|
51
52
|
const index = this.mark.index;
|
|
52
|
-
const
|
|
53
|
+
const g = `g[data-index="${index}"]`;
|
|
54
|
+
const selector = `${g} > *:not(g), ${g} > g > *`;
|
|
55
|
+
const nodes = this.nodes = svg.querySelectorAll(selector);
|
|
53
56
|
|
|
54
57
|
const { channels } = this;
|
|
55
58
|
for (let i = 0; i < nodes.length; ++i) {
|
|
@@ -69,8 +72,7 @@ export class Highlight {
|
|
|
69
72
|
for (let i = 0; i < nodes.length; ++i) {
|
|
70
73
|
const node = nodes[i];
|
|
71
74
|
const base = values[i];
|
|
72
|
-
const
|
|
73
|
-
const t = test(Array.isArray(data) ? data[0] : data);
|
|
75
|
+
const t = test(getDatum(node));
|
|
74
76
|
// TODO? handle inherited values / remove attributes
|
|
75
77
|
for (let j = 0; j < channels.length; ++j) {
|
|
76
78
|
const [attr, value] = channels[j];
|
|
@@ -93,7 +95,7 @@ async function predicateFunction(mark, selection) {
|
|
|
93
95
|
const s = { __: and(pred) };
|
|
94
96
|
const q = mark.query(filter);
|
|
95
97
|
(q.queries || [q]).forEach(q => {
|
|
96
|
-
q.
|
|
98
|
+
q._groupby.length ? q.select(s) : q.setSelect(s);
|
|
97
99
|
});
|
|
98
100
|
|
|
99
101
|
const data = await mark.coordinator.query(q);
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { clauseInterval } from '@uwdata/mosaic-core';
|
|
2
|
-
import { ascending, min, max
|
|
3
|
-
import { brushX, brushY } from './util/brush.js';
|
|
2
|
+
import { ascending, min, max } from 'd3';
|
|
3
|
+
import { brushGroups, brushX, brushY } from './util/brush.js';
|
|
4
4
|
import { closeTo } from './util/close-to.js';
|
|
5
5
|
import { getField } from './util/get-field.js';
|
|
6
6
|
import { invert } from './util/invert.js';
|
|
7
|
-
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
8
7
|
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
9
8
|
|
|
10
9
|
export class Interval1D {
|
|
@@ -63,18 +62,12 @@ export class Interval1D {
|
|
|
63
62
|
init(svg, root) {
|
|
64
63
|
const { brush, channel, style } = this;
|
|
65
64
|
this.scale = svg.scale(channel);
|
|
65
|
+
const range = this.value?.map(this.scale.apply).sort(ascending);
|
|
66
66
|
|
|
67
67
|
const rx = svg.scale('x').range;
|
|
68
68
|
const ry = svg.scale('y').range;
|
|
69
69
|
brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
|
|
70
|
-
|
|
71
|
-
const range = this.value?.map(this.scale.apply).sort(ascending);
|
|
72
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
73
|
-
root = facets.size() ? facets : select(root ?? svg);
|
|
74
|
-
this.g = root
|
|
75
|
-
.append('g')
|
|
76
|
-
.attr('class', `interval-${channel}`)
|
|
77
|
-
.each(patchScreenCTM)
|
|
70
|
+
this.g = brushGroups(svg, root, min(rx), min(ry), `interval-${channel}`)
|
|
78
71
|
.call(brush)
|
|
79
72
|
.call(brush.moveSilent, range);
|
|
80
73
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { clauseIntervals } from '@uwdata/mosaic-core';
|
|
2
|
-
import { ascending, min, max
|
|
3
|
-
import { brush } from './util/brush.js';
|
|
2
|
+
import { ascending, min, max } from 'd3';
|
|
3
|
+
import { brush, brushGroups } from './util/brush.js';
|
|
4
4
|
import { closeTo } from './util/close-to.js';
|
|
5
5
|
import { getField } from './util/get-field.js';
|
|
6
6
|
import { invert } from './util/invert.js';
|
|
7
|
-
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
8
7
|
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
9
8
|
|
|
10
9
|
export class Interval2D {
|
|
@@ -38,6 +37,7 @@ export class Interval2D {
|
|
|
38
37
|
|
|
39
38
|
publish(extent) {
|
|
40
39
|
const { value, pixelSize, xscale, yscale } = this;
|
|
40
|
+
|
|
41
41
|
let xr = undefined;
|
|
42
42
|
let yr = undefined;
|
|
43
43
|
if (extent) {
|
|
@@ -64,20 +64,13 @@ export class Interval2D {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
init(svg) {
|
|
67
|
-
const { brush, style } = this;
|
|
67
|
+
const { brush, style, value } = this;
|
|
68
68
|
const xscale = this.xscale = svg.scale('x');
|
|
69
69
|
const yscale = this.yscale = svg.scale('y');
|
|
70
70
|
const rx = xscale.range;
|
|
71
71
|
const ry = yscale.range;
|
|
72
72
|
brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
|
|
73
|
-
|
|
74
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
75
|
-
const root = facets.size() ? facets : select(svg);
|
|
76
|
-
this.g = root
|
|
77
|
-
.append('g')
|
|
78
|
-
.attr('class', `interval-xy`)
|
|
79
|
-
.each(patchScreenCTM)
|
|
80
|
-
.call(brush);
|
|
73
|
+
this.g = brushGroups(svg, null, min(rx), min(ry), 'interval-xy').call(brush);
|
|
81
74
|
|
|
82
75
|
if (style) {
|
|
83
76
|
const brushes = this.g.selectAll('rect.selection');
|
|
@@ -86,9 +79,9 @@ export class Interval2D {
|
|
|
86
79
|
}
|
|
87
80
|
}
|
|
88
81
|
|
|
89
|
-
if (
|
|
90
|
-
const [x1, x2] =
|
|
91
|
-
const [y1, y2] =
|
|
82
|
+
if (value) {
|
|
83
|
+
const [x1, x2] = value[0].map(xscale.apply).sort(ascending);
|
|
84
|
+
const [y1, y2] = value[1].map(yscale.apply).sort(ascending);
|
|
92
85
|
this.g.call(brush.moveSilent, [[x1, y1], [x2, y2]]);
|
|
93
86
|
}
|
|
94
87
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { clausePoint, clausePoints, isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { select, pointer } from 'd3';
|
|
2
|
+
import { select, pointer, min } from 'd3';
|
|
3
3
|
import { getField } from './util/get-field.js';
|
|
4
4
|
|
|
5
5
|
export class Nearest {
|
|
@@ -39,17 +39,13 @@ export class Nearest {
|
|
|
39
39
|
const keys = channels.map(c => mark.channelField(c).as);
|
|
40
40
|
const param = !isSelection(selection);
|
|
41
41
|
|
|
42
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
43
|
-
const root = facets.size() ? facets : select(svg);
|
|
44
|
-
|
|
45
42
|
// extract x, y coordinates for data values and determine scale factors
|
|
46
|
-
const
|
|
47
|
-
const yscale = svg.scale('y').apply;
|
|
48
|
-
const X = Array.from(columns[mark.channelField('x').as], xscale);
|
|
49
|
-
const Y = Array.from(columns[mark.channelField('y').as], yscale);
|
|
43
|
+
const [X, Y] = calculateXY(svg, mark);
|
|
50
44
|
const sx = this.pointer === 'y' ? 0.01 : 1;
|
|
51
45
|
const sy = this.pointer === 'x' ? 0.01 : 1;
|
|
52
46
|
|
|
47
|
+
const root = select(svg);
|
|
48
|
+
|
|
53
49
|
// find value nearest to pointer and update param or selection
|
|
54
50
|
// we don't pass undefined values to params, but do allow empty selections
|
|
55
51
|
root.on('pointerenter pointerdown pointermove', function(evt) {
|
|
@@ -83,6 +79,41 @@ export class Nearest {
|
|
|
83
79
|
}
|
|
84
80
|
}
|
|
85
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Extract x, y coordinates for data values.
|
|
84
|
+
*/
|
|
85
|
+
function calculateXY(svg, mark) {
|
|
86
|
+
const { data: { columns } } = mark;
|
|
87
|
+
const data = c => columns[mark.channelField(c)?.as];
|
|
88
|
+
const scale = c => svg.scale(c);
|
|
89
|
+
|
|
90
|
+
const sx = svg.scale('x');
|
|
91
|
+
const sy = svg.scale('y');
|
|
92
|
+
const sfx = scale('fx')?.apply;
|
|
93
|
+
const sfy = scale('fy')?.apply;
|
|
94
|
+
|
|
95
|
+
const X = Array.from(data('x'), sx.apply);
|
|
96
|
+
const Y = Array.from(data('y'), sy.apply);
|
|
97
|
+
|
|
98
|
+
// as needed, adjust coordinates by facets
|
|
99
|
+
if (sfx) {
|
|
100
|
+
const dx = min(sx.range);
|
|
101
|
+
const FX = data('fx');
|
|
102
|
+
for (let i = 0; i < FX.length; ++i) {
|
|
103
|
+
X[i] += sfx(FX[i]) - dx;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (sfy) {
|
|
107
|
+
const dy = min(sy.range);
|
|
108
|
+
const FY = data('fy');
|
|
109
|
+
for (let i = 0; i < FY.length; ++i) {
|
|
110
|
+
Y[i] += sfy(FY[i]) - dy;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [X, Y];
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
/**
|
|
87
118
|
* Find the nearest data point to the pointer. The nearest point
|
|
88
119
|
* is found via Euclidean distance, but with scale factors *sx* and
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { clausePoints } from '@uwdata/mosaic-core';
|
|
2
|
+
import { select } from 'd3';
|
|
3
|
+
import { brush } from './util/brush.js';
|
|
4
|
+
import { getFields } from './util/get-field.js';
|
|
5
|
+
import { intersect } from './util/intersect.js';
|
|
6
|
+
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
7
|
+
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
8
|
+
import { neqSome } from './util/neq.js';
|
|
9
|
+
import { getDatum } from './util/get-datum.js';
|
|
10
|
+
|
|
11
|
+
export class Region {
|
|
12
|
+
constructor(mark, {
|
|
13
|
+
channels,
|
|
14
|
+
selection,
|
|
15
|
+
peers = true,
|
|
16
|
+
brush: style = {
|
|
17
|
+
fill: 'none',
|
|
18
|
+
stroke: 'currentColor',
|
|
19
|
+
strokeDasharray: '1,1'
|
|
20
|
+
}
|
|
21
|
+
}) {
|
|
22
|
+
this.mark = mark;
|
|
23
|
+
this.selection = selection;
|
|
24
|
+
this.peers = peers;
|
|
25
|
+
|
|
26
|
+
this.style = style && sanitizeStyles(style);
|
|
27
|
+
this.brush = brush();
|
|
28
|
+
this.brush.on('brush end', evt => this.publish(evt.selection));
|
|
29
|
+
this.extent = null;
|
|
30
|
+
this.groups = null;
|
|
31
|
+
|
|
32
|
+
const { fields, as } = getFields(mark, channels);
|
|
33
|
+
this.fields = fields;
|
|
34
|
+
this.as = as;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
reset() {
|
|
38
|
+
this.value = undefined;
|
|
39
|
+
this.extent = null;
|
|
40
|
+
if (this.g) this.brush.reset(this.g);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
activate() {
|
|
44
|
+
this.selection.activate(this.clause([this.fields.map(() => 0)]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
clause(value) {
|
|
48
|
+
const { fields, mark } = this;
|
|
49
|
+
return clausePoints(fields, value, {
|
|
50
|
+
source: this,
|
|
51
|
+
clients: this.peers ? mark.plot.markSet : new Set().add(mark)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
publish(extent) {
|
|
56
|
+
const { as, group, mark, svg } = this;
|
|
57
|
+
let value;
|
|
58
|
+
|
|
59
|
+
// extract channel values for points
|
|
60
|
+
if (extent) {
|
|
61
|
+
const { data: { columns = {} } = {} } = mark;
|
|
62
|
+
const map = new Map;
|
|
63
|
+
intersect(svg, group, extent).forEach(el => {
|
|
64
|
+
const index = getDatum(el);
|
|
65
|
+
const vals = as.map(name => columns[name][index]);
|
|
66
|
+
map.set(vals.join('|'), vals); // deduplicate values
|
|
67
|
+
});
|
|
68
|
+
value = Array.from(map.values());
|
|
69
|
+
}
|
|
70
|
+
this.extent = extent;
|
|
71
|
+
|
|
72
|
+
if (neqSome(value, this.value)) {
|
|
73
|
+
this.value = value;
|
|
74
|
+
this.selection.update(this.clause(value));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
init(svg) {
|
|
79
|
+
const { brush, extent, mark, style } = this;
|
|
80
|
+
this.svg = svg;
|
|
81
|
+
|
|
82
|
+
const w = svg.width.baseVal.value;
|
|
83
|
+
const h = svg.height.baseVal.value;
|
|
84
|
+
brush.extent([[0, 0], [w, h]]);
|
|
85
|
+
|
|
86
|
+
// isolate eligible mark group
|
|
87
|
+
this.group = svg.querySelector(`[data-index="${mark.index}"]`);
|
|
88
|
+
|
|
89
|
+
// create a single brush, regardless of facets
|
|
90
|
+
this.g = select(svg)
|
|
91
|
+
.append('g')
|
|
92
|
+
.attr('class', `region-xy`)
|
|
93
|
+
.each(patchScreenCTM)
|
|
94
|
+
.call(brush)
|
|
95
|
+
.call(brush.moveSilent, extent);
|
|
96
|
+
|
|
97
|
+
if (style) {
|
|
98
|
+
const brushes = this.g.selectAll('rect.selection');
|
|
99
|
+
for (const name in style) {
|
|
100
|
+
brushes.attr(name, style[name]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
svg.addEventListener('pointerenter', evt => {
|
|
105
|
+
if (!evt.buttons) this.activate();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { clausePoints } from '@uwdata/mosaic-core';
|
|
2
|
+
import { getDatum } from './util/get-datum.js';
|
|
3
|
+
import { neq, neqSome } from './util/neq.js';
|
|
2
4
|
|
|
3
5
|
export class Toggle {
|
|
4
6
|
/**
|
|
@@ -44,12 +46,10 @@ export class Toggle {
|
|
|
44
46
|
init(svg, selector, accessor) {
|
|
45
47
|
const { mark, as, selection } = this;
|
|
46
48
|
const { data: { columns = {} } = {} } = mark;
|
|
47
|
-
accessor ??= target => as.map(name =>
|
|
48
|
-
|
|
49
|
-
return columns[name][Array.isArray(data) ? data[0] : data];
|
|
50
|
-
});
|
|
49
|
+
accessor ??= target => as.map(name => columns[name][getDatum(target)]);
|
|
50
|
+
|
|
51
51
|
selector ??= `[data-index="${mark.index}"]`;
|
|
52
|
-
const groups =
|
|
52
|
+
const groups = Array.from(svg.querySelectorAll(selector));
|
|
53
53
|
|
|
54
54
|
svg.addEventListener('pointerdown', evt => {
|
|
55
55
|
const state = selection.single ? selection.value : this.value;
|
|
@@ -82,22 +82,5 @@ export class Toggle {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function isTargetElement(groups, node) {
|
|
85
|
-
return groups.
|
|
86
|
-
|| groups.has(node.parentNode)
|
|
87
|
-
|| groups.has(node.parentNode?.parentNode);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function neqSome(a, b) {
|
|
91
|
-
return (a == null || b == null)
|
|
92
|
-
? (a != null || b != null)
|
|
93
|
-
: (a.length !== b.length || a.some((x, i) => neq(x, b[i])));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function neq(a, b) {
|
|
97
|
-
const n = a.length;
|
|
98
|
-
if (b.length !== n) return true;
|
|
99
|
-
for (let i = 0; i < n; ++i) {
|
|
100
|
-
if (a[i] !== b[i]) return true;
|
|
101
|
-
}
|
|
102
|
-
return false;
|
|
85
|
+
return groups.some(g => g.contains(node));
|
|
103
86
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
} from 'd3';
|
|
1
|
+
import { brush as d3_brush, brushX as d3_brushX, brushY as d3_brushY, select } from 'd3';
|
|
2
|
+
import { patchScreenCTM } from './patchScreenCTM.js';
|
|
4
3
|
|
|
5
4
|
function wrap(brush) {
|
|
6
5
|
const brushOn = brush.on;
|
|
@@ -43,3 +42,36 @@ export function brushX() {
|
|
|
43
42
|
export function brushY() {
|
|
44
43
|
return wrap(d3_brushY());
|
|
45
44
|
}
|
|
45
|
+
|
|
46
|
+
export function brushGroups(svg, root, dx, dy, className) {
|
|
47
|
+
let groups = select(root ?? svg)
|
|
48
|
+
.append('g')
|
|
49
|
+
.attr('class', className)
|
|
50
|
+
|
|
51
|
+
// if the plot is faceted, create per-facet brush groups
|
|
52
|
+
const fx = svg.scale('fx');
|
|
53
|
+
const fy = svg.scale('fy');
|
|
54
|
+
if (fx || fy) {
|
|
55
|
+
const X = fx?.domain.map(v => fx.apply(v) - dx);
|
|
56
|
+
const Y = fy?.domain.map(v => fy.apply(v) - dy);
|
|
57
|
+
if (X && Y) {
|
|
58
|
+
for (let i = 0; i < X.length; ++i) {
|
|
59
|
+
for (let j = 0; j < Y.length; ++j) {
|
|
60
|
+
groups.append('g').attr('transform', `translate(${X[i]}, ${Y[j]})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else if (X) {
|
|
64
|
+
for (let i = 0; i < X.length; ++i) {
|
|
65
|
+
groups.append('g').attr('transform', `translate(${X[i]}, 0})`);
|
|
66
|
+
}
|
|
67
|
+
} else if (Y) {
|
|
68
|
+
for (let j = 0; j < Y.length; ++j) {
|
|
69
|
+
groups.append('g').attr('transform', `translate(0, ${Y[j]})`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
groups = groups.selectAll('g');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// return brush groups, with screen transform fix
|
|
76
|
+
return groups.each(patchScreenCTM);
|
|
77
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the bound datum value on a Plot-generated SVG element.
|
|
3
|
+
* Following D3, bound data is assigned to the `__data__` property.
|
|
4
|
+
* However, the target mark may be wrapped within a hyperlink `a` tag.
|
|
5
|
+
* @param {Element} el A DOM element.
|
|
6
|
+
* @returns {*} The bound datum.
|
|
7
|
+
*/
|
|
8
|
+
export function getDatum(el) {
|
|
9
|
+
if (el.tagName === 'a') {
|
|
10
|
+
el = el.children[0];
|
|
11
|
+
}
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
const data = el.__data__;
|
|
14
|
+
return Array.isArray(data) ? data[0] : data;
|
|
15
|
+
}
|
|
@@ -1,4 +1,39 @@
|
|
|
1
|
+
import { isNode } from '@uwdata/mosaic-sql';
|
|
2
|
+
|
|
3
|
+
function extractField(field) {
|
|
4
|
+
if (isNode(field)) {
|
|
5
|
+
if (field.type === 'COLUMN_REF') {
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
return field.column;
|
|
8
|
+
} else if (field.type === 'AGGREGATE') {
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
return field.args[0] ?? field;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return field;
|
|
14
|
+
}
|
|
15
|
+
|
|
1
16
|
export function getField(mark, channel) {
|
|
2
|
-
|
|
3
|
-
|
|
17
|
+
return extractField(mark.channelField(channel)?.field);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getFields(mark, channels) {
|
|
21
|
+
const fields = [];
|
|
22
|
+
const as = [];
|
|
23
|
+
channels.forEach(c => {
|
|
24
|
+
const q = c === 'color' ? ['color', 'fill', 'stroke']
|
|
25
|
+
: c === 'x' ? ['x', 'x1', 'x2']
|
|
26
|
+
: c === 'y' ? ['y', 'y1', 'y2']
|
|
27
|
+
: [c];
|
|
28
|
+
for (let i = 0; i < q.length; ++i) {
|
|
29
|
+
const f = mark.channelField(q[i], { exact: true });
|
|
30
|
+
if (f) {
|
|
31
|
+
fields.push(extractField(f.field));
|
|
32
|
+
as.push(f.as);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Missing channel: ${c}`);
|
|
37
|
+
});
|
|
38
|
+
return { fields, as };
|
|
4
39
|
}
|