@uwdata/vgplot 0.4.0 → 0.5.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/README.md +4 -2
- package/dist/vgplot.js +5643 -5842
- package/dist/vgplot.min.js +14 -35
- package/package.json +8 -10
- package/src/api.js +292 -0
- package/src/connect.js +14 -0
- package/src/context.js +20 -0
- package/src/index.js +14 -303
- package/src/inputs.js +24 -0
- package/src/{directives → plot}/attributes.js +14 -5
- package/src/{directives → plot}/interactors.js +8 -6
- package/src/{directives → plot}/legends.js +14 -6
- package/src/{directives → plot}/marks.js +16 -13
- package/src/plot/named-plots.js +49 -0
- package/src/plot/plot.js +9 -0
- package/src/directives/plot.js +0 -39
- package/src/interactors/Highlight.js +0 -101
- package/src/interactors/Interval1D.js +0 -90
- package/src/interactors/Interval2D.js +0 -102
- package/src/interactors/Nearest.js +0 -66
- package/src/interactors/PanZoom.js +0 -121
- package/src/interactors/Toggle.js +0 -111
- package/src/interactors/util/brush.js +0 -45
- package/src/interactors/util/close-to.js +0 -9
- package/src/interactors/util/get-field.js +0 -4
- package/src/interactors/util/invert.js +0 -3
- package/src/interactors/util/patchScreenCTM.js +0 -13
- package/src/interactors/util/sanitize-styles.js +0 -9
- package/src/interactors/util/to-kebab-case.js +0 -9
- package/src/layout/index.js +0 -2
- package/src/legend.js +0 -64
- package/src/marks/ConnectedMark.js +0 -63
- package/src/marks/ContourMark.js +0 -89
- package/src/marks/DenseLineMark.js +0 -146
- package/src/marks/Density1DMark.js +0 -104
- package/src/marks/Density2DMark.js +0 -69
- package/src/marks/Grid2DMark.js +0 -191
- package/src/marks/HexbinMark.js +0 -88
- package/src/marks/Mark.js +0 -195
- package/src/marks/RasterMark.js +0 -122
- package/src/marks/RasterTileMark.js +0 -332
- package/src/marks/RegressionMark.js +0 -117
- package/src/marks/util/bin-field.js +0 -17
- package/src/marks/util/density.js +0 -226
- package/src/marks/util/extent.js +0 -56
- package/src/marks/util/grid.js +0 -57
- package/src/marks/util/handle-param.js +0 -14
- package/src/marks/util/is-arrow-table.js +0 -3
- package/src/marks/util/is-color.js +0 -18
- package/src/marks/util/is-constant-option.js +0 -40
- package/src/marks/util/is-symbol.js +0 -20
- package/src/marks/util/raster.js +0 -44
- package/src/marks/util/stats.js +0 -133
- package/src/marks/util/to-data-array.js +0 -58
- package/src/plot-attributes.js +0 -211
- package/src/plot-renderer.js +0 -161
- package/src/plot.js +0 -136
- package/src/spec/parse-data.js +0 -69
- package/src/spec/parse-spec.js +0 -422
- package/src/spec/to-module.js +0 -465
- package/src/spec/util.js +0 -43
- package/src/symbols.js +0 -3
- package/src/transforms/bin.js +0 -81
- package/src/transforms/index.js +0 -3
- /package/src/{directives → plot}/data.js +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isParam } from '@uwdata/mosaic-core';
|
|
2
|
-
import {
|
|
2
|
+
import { setNamedPlot } from './named-plots.js';
|
|
3
3
|
|
|
4
4
|
export function name(name) {
|
|
5
|
-
return plot =>
|
|
5
|
+
return plot => setNamedPlot(this, name, plot);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
function setAttribute(plot, name, value) {
|
|
@@ -19,11 +19,11 @@ function setAttribute(plot, name, value) {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
function attribute(name, value) {
|
|
23
23
|
return plot => { setAttribute(plot, name, value); };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function attributes(values) {
|
|
27
27
|
return plot => {
|
|
28
28
|
for (const [name, value] of Object.entries(values)) {
|
|
29
29
|
setAttribute(plot, name, value)
|
|
@@ -41,6 +41,15 @@ export function margins(object) {
|
|
|
41
41
|
return attributes(attr);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
export function margin(value) {
|
|
45
|
+
return attributes({
|
|
46
|
+
marginTop: value,
|
|
47
|
+
marginBottom: value,
|
|
48
|
+
marginLeft: value,
|
|
49
|
+
marginRight: value
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export function xyDomain(value) {
|
|
45
54
|
return attributes({ xDomain: value, yDomain: value });
|
|
46
55
|
}
|
|
@@ -51,7 +60,6 @@ const attrf = name => value => attribute(name, value);
|
|
|
51
60
|
export const style = attrf('style');
|
|
52
61
|
export const width = attrf('width');
|
|
53
62
|
export const height = attrf('height');
|
|
54
|
-
export const margin = attrf('margin');
|
|
55
63
|
export const marginLeft = attrf('marginLeft');
|
|
56
64
|
export const marginRight = attrf('marginRight');
|
|
57
65
|
export const marginTop = attrf('marginTop');
|
|
@@ -201,6 +209,7 @@ export const colorScale = attrf('colorScale');
|
|
|
201
209
|
export const colorDomain = attrf('colorDomain');
|
|
202
210
|
export const colorRange = attrf('colorRange');
|
|
203
211
|
export const colorClamp = attrf('colorClamp');
|
|
212
|
+
export const colorN = attrf('colorN');
|
|
204
213
|
export const colorNice = attrf('colorNice');
|
|
205
214
|
export const colorScheme = attrf('colorScheme');
|
|
206
215
|
export const colorInterpolate = attrf('colorInterpolate');
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {
|
|
2
|
+
Highlight,
|
|
3
|
+
Toggle,
|
|
4
|
+
Interval1D,
|
|
5
|
+
Interval2D,
|
|
6
|
+
PanZoom,
|
|
7
|
+
Nearest
|
|
8
|
+
} from '@uwdata/mosaic-plot';
|
|
7
9
|
|
|
8
10
|
function interactor(InteractorClass, options) {
|
|
9
11
|
return plot => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Legend } from '
|
|
2
|
-
import {
|
|
1
|
+
import { Legend } from '@uwdata/mosaic-plot';
|
|
2
|
+
import { requestNamedPlot } from './named-plots.js';
|
|
3
3
|
|
|
4
4
|
function legend(channel, options = {}) {
|
|
5
5
|
if (options.for) {
|
|
@@ -8,7 +8,7 @@ function legend(channel, options = {}) {
|
|
|
8
8
|
const type = typeof maybePlot;
|
|
9
9
|
const add = plot => plot.addLegend(legend, false);
|
|
10
10
|
if (type === 'string') {
|
|
11
|
-
|
|
11
|
+
requestNamedPlot(this, maybePlot, add);
|
|
12
12
|
} else if (maybePlot.value) {
|
|
13
13
|
add(maybePlot.value);
|
|
14
14
|
}
|
|
@@ -18,6 +18,14 @@ function legend(channel, options = {}) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export function colorLegend(options) {
|
|
22
|
+
return legend.call(this, 'color', options);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function opacityLegend(options) {
|
|
26
|
+
return legend.call(this, 'opacity', options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function symbolLegend(options) {
|
|
30
|
+
return legend.call(this, 'symbol', options);
|
|
31
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import {
|
|
2
|
+
Mark,
|
|
3
|
+
ConnectedMark,
|
|
4
|
+
ContourMark,
|
|
5
|
+
Density1DMark,
|
|
6
|
+
Density2DMark,
|
|
7
|
+
DenseLineMark,
|
|
8
|
+
GeoMark,
|
|
9
|
+
HexbinMark,
|
|
10
|
+
RasterMark,
|
|
11
|
+
RasterTileMark,
|
|
12
|
+
RegressionMark
|
|
13
|
+
} from '@uwdata/mosaic-plot';
|
|
11
14
|
|
|
12
15
|
const decorators = new Set([
|
|
13
16
|
'frame',
|
|
@@ -17,8 +20,8 @@ const decorators = new Set([
|
|
|
17
20
|
'graticule', 'sphere'
|
|
18
21
|
]);
|
|
19
22
|
|
|
20
|
-
function mark(type, data, channels) {
|
|
21
|
-
if (arguments.length === 2) {
|
|
23
|
+
function mark(type, data, channels = {}) {
|
|
24
|
+
if (arguments.length === 2 && !Array.isArray(data)) {
|
|
22
25
|
channels = data;
|
|
23
26
|
data = decorators.has(type) ? null : [{}];
|
|
24
27
|
}
|
|
@@ -118,6 +121,6 @@ export const gridY = (...args) => mark('gridY', ...args);
|
|
|
118
121
|
export const gridFx = (...args) => mark('gridFx', ...args);
|
|
119
122
|
export const gridFy = (...args) => mark('gridFy', ...args);
|
|
120
123
|
|
|
121
|
-
export const geo = (...args) =>
|
|
124
|
+
export const geo = (...args) => implicitType(GeoMark, ...args);
|
|
122
125
|
export const sphere = (...args) => mark('sphere', ...args);
|
|
123
126
|
export const graticule = (...args) => mark('graticule', ...args);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class NamedPlots extends Map {
|
|
2
|
+
request(name, callback) {
|
|
3
|
+
if (this.has(name)) {
|
|
4
|
+
callback(this.get(name));
|
|
5
|
+
} else {
|
|
6
|
+
const waiting = this.waiting || (this.waiting = new Map);
|
|
7
|
+
const list = waiting.get(name) || [];
|
|
8
|
+
waiting.set(name, list.concat(callback));
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
set(name, plot) {
|
|
12
|
+
if (this.has(name)) {
|
|
13
|
+
console.warn(`Overwriting named plot "${name}".`);
|
|
14
|
+
}
|
|
15
|
+
const { waiting } = this;
|
|
16
|
+
if (waiting?.has(name)) {
|
|
17
|
+
waiting.get(name).forEach(fn => fn(plot));
|
|
18
|
+
waiting.delete(name);
|
|
19
|
+
}
|
|
20
|
+
return super.set(name, plot);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default instance of named plots map.
|
|
26
|
+
*/
|
|
27
|
+
export const namedPlots = new NamedPlots();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context-sensitive lookup of named plots.
|
|
31
|
+
* This method proxies access to the NamedPlots.request().
|
|
32
|
+
* If the provided context object has a local namedPlots, that is used.
|
|
33
|
+
* Otherwise the default instance is used.
|
|
34
|
+
*/
|
|
35
|
+
export function requestNamedPlot(ctx, name, callback) {
|
|
36
|
+
const map = ctx?.context?.namedPlots ?? namedPlots;
|
|
37
|
+
map.request(name, callback);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Context-sensitive addition of named plots.
|
|
42
|
+
* This method proxies access to the NamedPlots.set().
|
|
43
|
+
* If the provided context object has a local namedPlots, that is used.
|
|
44
|
+
* Otherwise the default instance is used.
|
|
45
|
+
*/
|
|
46
|
+
export function setNamedPlot(ctx, name, plot) {
|
|
47
|
+
const map = ctx?.context?.namedPlots ?? namedPlots;
|
|
48
|
+
map.set(name, plot);
|
|
49
|
+
}
|
package/src/plot/plot.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Plot } from '@uwdata/mosaic-plot';
|
|
2
|
+
import { connect } from '../connect.js';
|
|
3
|
+
|
|
4
|
+
export function plot(...directives) {
|
|
5
|
+
const p = new Plot();
|
|
6
|
+
directives.flat().forEach(dir => dir(p));
|
|
7
|
+
connect(this, ...p.marks); // this -> optional API context
|
|
8
|
+
return p.element;
|
|
9
|
+
}
|
package/src/directives/plot.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { coordinator } from '@uwdata/mosaic-core';
|
|
2
|
-
import { Plot } from '../plot.js';
|
|
3
|
-
|
|
4
|
-
export function plot(...directives) {
|
|
5
|
-
const p = new Plot();
|
|
6
|
-
directives.flat().forEach(dir => dir(p));
|
|
7
|
-
p.marks.forEach(mark => coordinator().connect(mark));
|
|
8
|
-
return p.element;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class NamedPlots extends Map {
|
|
12
|
-
request(name, callback) {
|
|
13
|
-
if (this.has(name)) {
|
|
14
|
-
callback(this.get(name));
|
|
15
|
-
} else {
|
|
16
|
-
const waiting = this.waiting || (this.waiting = new Map);
|
|
17
|
-
const list = waiting.get(name) || [];
|
|
18
|
-
waiting.set(name, list.concat(callback));
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
set(name, plot) {
|
|
22
|
-
if (this.has(name)) {
|
|
23
|
-
console.warn(`Overwriting named plot "${name}".`);
|
|
24
|
-
}
|
|
25
|
-
const { waiting } = this;
|
|
26
|
-
if (waiting?.has(name)) {
|
|
27
|
-
waiting.get(name).forEach(fn => fn(plot));
|
|
28
|
-
waiting.delete(name);
|
|
29
|
-
}
|
|
30
|
-
return super.set(name, plot);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const namedPlots = new NamedPlots();
|
|
35
|
-
|
|
36
|
-
export function reset() {
|
|
37
|
-
namedPlots.clear();
|
|
38
|
-
coordinator().clear();
|
|
39
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { coordinator, throttle } from '@uwdata/mosaic-core';
|
|
2
|
-
import { and } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
4
|
-
|
|
5
|
-
function configureMark(mark) {
|
|
6
|
-
const { channels } = mark;
|
|
7
|
-
const dims = new Set;
|
|
8
|
-
let ordered = false;
|
|
9
|
-
let aggregate = false;
|
|
10
|
-
|
|
11
|
-
for (const c of channels) {
|
|
12
|
-
const { channel, field, as } = c;
|
|
13
|
-
if (channel === 'orderby') {
|
|
14
|
-
ordered = true;
|
|
15
|
-
} else if (field) {
|
|
16
|
-
if (field.aggregate) {
|
|
17
|
-
aggregate = true;
|
|
18
|
-
} else {
|
|
19
|
-
if (dims.has(as)) continue;
|
|
20
|
-
dims.add(as);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// if orderby is defined, we're ok: nothing to do
|
|
26
|
-
// or, if there is no groupby aggregation, we're ok: nothing to do
|
|
27
|
-
// grouping may result in optimizations that change result order
|
|
28
|
-
// so we orderby the grouping dimensions to ensure stable indices
|
|
29
|
-
if (!ordered && aggregate && dims.size) {
|
|
30
|
-
mark.channels.push(({ channel: 'orderby', value: Array.from(dims) }));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return mark;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class Highlight {
|
|
37
|
-
constructor(mark, {
|
|
38
|
-
selection,
|
|
39
|
-
channels = {}
|
|
40
|
-
}) {
|
|
41
|
-
this.mark = configureMark(mark);
|
|
42
|
-
this.selection = selection;
|
|
43
|
-
const c = Object.entries(sanitizeStyles(channels));
|
|
44
|
-
this.channels = c.length ? c : [['opacity', 0.2]];
|
|
45
|
-
this.selection.addEventListener('value', throttle(() => this.update()));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
init(svg) {
|
|
49
|
-
this.svg = svg;
|
|
50
|
-
const values = this.values = [];
|
|
51
|
-
const index = this.mark.index;
|
|
52
|
-
const nodes = this.nodes = svg.querySelectorAll(`[data-index="${index}"] > *`);
|
|
53
|
-
|
|
54
|
-
const { channels } = this;
|
|
55
|
-
for (let i = 0; i < nodes.length; ++i) {
|
|
56
|
-
const node = nodes[i];
|
|
57
|
-
values.push(channels.map(c => node.getAttribute(c[0])));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return this.update();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async update() {
|
|
64
|
-
const { svg, nodes, channels, values, mark, selection } = this;
|
|
65
|
-
if (!svg) return;
|
|
66
|
-
|
|
67
|
-
const test = await predicateFunction(mark, selection);
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < nodes.length; ++i) {
|
|
70
|
-
const node = nodes[i];
|
|
71
|
-
const base = values[i];
|
|
72
|
-
const t = test(node.__data__);
|
|
73
|
-
// TODO? handle inherited values / remove attributes
|
|
74
|
-
for (let j = 0; j < channels.length; ++j) {
|
|
75
|
-
const [attr, value] = channels[j];
|
|
76
|
-
node.setAttribute(attr, t ? base[j] : value);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function predicateFunction(mark, selection) {
|
|
83
|
-
const pred = selection?.predicate(mark);
|
|
84
|
-
|
|
85
|
-
if (!pred || pred.length === 0) {
|
|
86
|
-
return () => true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// set flag so we do not skip cross-filtered sources
|
|
90
|
-
const filter = mark.filterBy?.predicate(mark, true);
|
|
91
|
-
|
|
92
|
-
const s = { __: and(pred) };
|
|
93
|
-
const q = mark.query(filter);
|
|
94
|
-
const p = q.groupby().length ? q.select(s) : q.$select(s);
|
|
95
|
-
|
|
96
|
-
const data = await coordinator().query(p);
|
|
97
|
-
const v = data.getChild?.('__');
|
|
98
|
-
return !(data.numRows || data.length) ? (() => false)
|
|
99
|
-
: v ? (i => v.get(i))
|
|
100
|
-
: (i => data[i].__);
|
|
101
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { select, min, max } from 'd3';
|
|
2
|
-
import { isBetween } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { brushX, brushY } from './util/brush.js';
|
|
4
|
-
import { closeTo } from './util/close-to.js';
|
|
5
|
-
import { getField } from './util/get-field.js';
|
|
6
|
-
import { invert } from './util/invert.js';
|
|
7
|
-
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
8
|
-
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
9
|
-
|
|
10
|
-
export class Interval1D {
|
|
11
|
-
constructor(mark, {
|
|
12
|
-
channel,
|
|
13
|
-
selection,
|
|
14
|
-
field,
|
|
15
|
-
pixelSize = 1,
|
|
16
|
-
peers = true,
|
|
17
|
-
brush: style
|
|
18
|
-
}) {
|
|
19
|
-
this.mark = mark;
|
|
20
|
-
this.channel = channel;
|
|
21
|
-
this.pixelSize = pixelSize || 1;
|
|
22
|
-
this.selection = selection;
|
|
23
|
-
this.peers = peers;
|
|
24
|
-
this.field = field || getField(mark, [channel, channel+'1', channel+'2']);
|
|
25
|
-
this.style = style && sanitizeStyles(style);
|
|
26
|
-
this.brush = channel === 'y' ? brushY() : brushX();
|
|
27
|
-
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
reset() {
|
|
31
|
-
this.value = undefined;
|
|
32
|
-
if (this.g) this.brush.reset(this.g);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
activate() {
|
|
36
|
-
this.selection.activate(this.clause(this.value || [0, 1]));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
publish(extent) {
|
|
40
|
-
let range = undefined;
|
|
41
|
-
if (extent) {
|
|
42
|
-
range = extent
|
|
43
|
-
.map(v => invert(v, this.scale, this.pixelSize))
|
|
44
|
-
.sort((a, b) => a - b);
|
|
45
|
-
}
|
|
46
|
-
if (!closeTo(range, this.value)) {
|
|
47
|
-
this.value = range;
|
|
48
|
-
this.g.call(this.brush.moveSilent, extent);
|
|
49
|
-
this.selection.update(this.clause(range));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
clause(value) {
|
|
54
|
-
const { mark, pixelSize, field, scale } = this;
|
|
55
|
-
return {
|
|
56
|
-
source: this,
|
|
57
|
-
schema: { type: 'interval', pixelSize, scales: [scale] },
|
|
58
|
-
clients: this.peers ? mark.plot.markSet : new Set().add(mark),
|
|
59
|
-
value,
|
|
60
|
-
predicate: value ? isBetween(field, value) : null
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
init(svg) {
|
|
65
|
-
const { brush, channel, style } = this;
|
|
66
|
-
this.scale = svg.scale(channel);
|
|
67
|
-
|
|
68
|
-
const rx = svg.scale('x').range;
|
|
69
|
-
const ry = svg.scale('y').range;
|
|
70
|
-
brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
|
|
71
|
-
|
|
72
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
73
|
-
const root = facets.size() ? facets : select(svg);
|
|
74
|
-
this.g = root
|
|
75
|
-
.append('g')
|
|
76
|
-
.attr('class', `interval-${channel}`)
|
|
77
|
-
.each(patchScreenCTM)
|
|
78
|
-
.call(brush)
|
|
79
|
-
.call(brush.moveSilent, this.value?.map(this.scale.apply));
|
|
80
|
-
|
|
81
|
-
if (style) {
|
|
82
|
-
const brushes = this.g.selectAll('rect.selection');
|
|
83
|
-
for (const name in style) {
|
|
84
|
-
brushes.attr(name, style[name]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
svg.addEventListener('pointerenter', () => this.activate());
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { select, min, max } from 'd3';
|
|
2
|
-
import { and, isBetween } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { brush } from './util/brush.js';
|
|
4
|
-
import { closeTo } from './util/close-to.js';
|
|
5
|
-
import { getField } from './util/get-field.js';
|
|
6
|
-
import { invert } from './util/invert.js';
|
|
7
|
-
import { patchScreenCTM } from './util/patchScreenCTM.js';
|
|
8
|
-
import { sanitizeStyles } from './util/sanitize-styles.js';
|
|
9
|
-
|
|
10
|
-
const asc = (a, b) => a - b;
|
|
11
|
-
|
|
12
|
-
export class Interval2D {
|
|
13
|
-
constructor(mark, {
|
|
14
|
-
selection,
|
|
15
|
-
xfield,
|
|
16
|
-
yfield,
|
|
17
|
-
pixelSize = 1,
|
|
18
|
-
peers = true,
|
|
19
|
-
brush: style
|
|
20
|
-
}) {
|
|
21
|
-
this.mark = mark;
|
|
22
|
-
this.pixelSize = pixelSize || 1;
|
|
23
|
-
this.selection = selection;
|
|
24
|
-
this.peers = peers;
|
|
25
|
-
this.xfield = xfield || getField(mark, ['x', 'x1', 'x2']);
|
|
26
|
-
this.yfield = yfield || getField(mark, ['y', 'y1', 'y2']);
|
|
27
|
-
this.style = style && sanitizeStyles(style);
|
|
28
|
-
this.brush = brush();
|
|
29
|
-
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
reset() {
|
|
33
|
-
this.value = undefined;
|
|
34
|
-
if (this.g) this.brush.reset(this.g);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
activate() {
|
|
38
|
-
this.selection.activate(this.clause(this.value || [[0, 1], [0, 1]]));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
publish(extent) {
|
|
42
|
-
const { value, pixelSize, xscale, yscale } = this;
|
|
43
|
-
let xr = undefined;
|
|
44
|
-
let yr = undefined;
|
|
45
|
-
if (extent) {
|
|
46
|
-
const [a, b] = extent;
|
|
47
|
-
xr = [a[0], b[0]].map(v => invert(v, xscale, pixelSize)).sort(asc);
|
|
48
|
-
yr = [a[1], b[1]].map(v => invert(v, yscale, pixelSize)).sort(asc);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!closeTo(xr, value?.[0]) || !closeTo(yr, value?.[1])) {
|
|
52
|
-
this.value = extent ? [xr, yr] : undefined;
|
|
53
|
-
this.g.call(this.brush.moveSilent, extent);
|
|
54
|
-
this.selection.update(this.clause(this.value));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
clause(value) {
|
|
59
|
-
const { mark, pixelSize, xfield, yfield, xscale, yscale } = this;
|
|
60
|
-
return {
|
|
61
|
-
source: this,
|
|
62
|
-
schema: { type: 'interval', pixelSize, scales: [xscale, yscale] },
|
|
63
|
-
clients: this.peers ? mark.plot.markSet : new Set().add(mark),
|
|
64
|
-
value,
|
|
65
|
-
predicate: value
|
|
66
|
-
? and(isBetween(xfield, value[0]), isBetween(yfield, value[1]))
|
|
67
|
-
: null
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
init(svg) {
|
|
72
|
-
const { brush, style } = this;
|
|
73
|
-
const xscale = this.xscale = svg.scale('x');
|
|
74
|
-
const yscale = this.yscale = svg.scale('y');
|
|
75
|
-
const rx = xscale.range;
|
|
76
|
-
const ry = yscale.range;
|
|
77
|
-
brush.extent([[min(rx), min(ry)], [max(rx), max(ry)]]);
|
|
78
|
-
|
|
79
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
80
|
-
const root = facets.size() ? facets : select(svg);
|
|
81
|
-
this.g = root
|
|
82
|
-
.append('g')
|
|
83
|
-
.attr('class', `interval-xy`)
|
|
84
|
-
.each(patchScreenCTM)
|
|
85
|
-
.call(brush);
|
|
86
|
-
|
|
87
|
-
if (style) {
|
|
88
|
-
const brushes = this.g.selectAll('rect.selection');
|
|
89
|
-
for (const name in style) {
|
|
90
|
-
brushes.attr(name, style[name]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this.value) {
|
|
95
|
-
const [x1, x2] = this.value[0].map(xscale.apply).sort(asc);
|
|
96
|
-
const [y1, y2] = this.value[1].map(yscale.apply).sort(asc);
|
|
97
|
-
this.g.call(brush.moveSilent, [[x1, y1], [x2, y2]]);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
svg.addEventListener('pointerenter', () => this.activate());
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { isSelection } from '@uwdata/mosaic-core';
|
|
2
|
-
import { eq, literal } from '@uwdata/mosaic-sql';
|
|
3
|
-
import { select, pointer } from 'd3';
|
|
4
|
-
import { getField } from './util/get-field.js';
|
|
5
|
-
|
|
6
|
-
export class Nearest {
|
|
7
|
-
constructor(mark, {
|
|
8
|
-
selection,
|
|
9
|
-
channel,
|
|
10
|
-
field
|
|
11
|
-
}) {
|
|
12
|
-
this.mark = mark;
|
|
13
|
-
this.selection = selection;
|
|
14
|
-
this.clients = new Set().add(mark);
|
|
15
|
-
this.channel = channel;
|
|
16
|
-
this.field = field || getField(mark, [channel]);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
clause(value) {
|
|
20
|
-
const { clients, field } = this;
|
|
21
|
-
const predicate = value ? eq(field, literal(value)) : null;
|
|
22
|
-
return {
|
|
23
|
-
source: this,
|
|
24
|
-
schema: { type: 'point' },
|
|
25
|
-
clients,
|
|
26
|
-
value,
|
|
27
|
-
predicate
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
init(svg) {
|
|
32
|
-
const that = this;
|
|
33
|
-
const { mark, channel, selection } = this;
|
|
34
|
-
const { data } = mark;
|
|
35
|
-
const key = mark.channelField(channel).as;
|
|
36
|
-
|
|
37
|
-
const facets = select(svg).selectAll('g[aria-label="facet"]');
|
|
38
|
-
const root = facets.size() ? facets : select(svg);
|
|
39
|
-
const scale = svg.scale(channel);
|
|
40
|
-
const param = !isSelection(selection);
|
|
41
|
-
|
|
42
|
-
root.on('pointerdown pointermove', function(evt) {
|
|
43
|
-
const [x, y] = pointer(evt, this);
|
|
44
|
-
const z = findNearest(data, key, scale.invert(channel === 'x' ? x : y));
|
|
45
|
-
selection.update(param ? z : that.clause(z));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (param) return;
|
|
49
|
-
svg.addEventListener('pointerenter', () => {
|
|
50
|
-
this.selection.activate(this.clause(0));
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findNearest(data, key, value) {
|
|
56
|
-
let dist = Infinity;
|
|
57
|
-
let v;
|
|
58
|
-
data.forEach(d => {
|
|
59
|
-
const delta = Math.abs(d[key] - value);
|
|
60
|
-
if (delta < dist) {
|
|
61
|
-
dist = delta;
|
|
62
|
-
v = d[key];
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
return v;
|
|
66
|
-
}
|