@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.
Files changed (65) hide show
  1. package/README.md +4 -2
  2. package/dist/vgplot.js +5643 -5842
  3. package/dist/vgplot.min.js +14 -35
  4. package/package.json +8 -10
  5. package/src/api.js +292 -0
  6. package/src/connect.js +14 -0
  7. package/src/context.js +20 -0
  8. package/src/index.js +14 -303
  9. package/src/inputs.js +24 -0
  10. package/src/{directives → plot}/attributes.js +14 -5
  11. package/src/{directives → plot}/interactors.js +8 -6
  12. package/src/{directives → plot}/legends.js +14 -6
  13. package/src/{directives → plot}/marks.js +16 -13
  14. package/src/plot/named-plots.js +49 -0
  15. package/src/plot/plot.js +9 -0
  16. package/src/directives/plot.js +0 -39
  17. package/src/interactors/Highlight.js +0 -101
  18. package/src/interactors/Interval1D.js +0 -90
  19. package/src/interactors/Interval2D.js +0 -102
  20. package/src/interactors/Nearest.js +0 -66
  21. package/src/interactors/PanZoom.js +0 -121
  22. package/src/interactors/Toggle.js +0 -111
  23. package/src/interactors/util/brush.js +0 -45
  24. package/src/interactors/util/close-to.js +0 -9
  25. package/src/interactors/util/get-field.js +0 -4
  26. package/src/interactors/util/invert.js +0 -3
  27. package/src/interactors/util/patchScreenCTM.js +0 -13
  28. package/src/interactors/util/sanitize-styles.js +0 -9
  29. package/src/interactors/util/to-kebab-case.js +0 -9
  30. package/src/layout/index.js +0 -2
  31. package/src/legend.js +0 -64
  32. package/src/marks/ConnectedMark.js +0 -63
  33. package/src/marks/ContourMark.js +0 -89
  34. package/src/marks/DenseLineMark.js +0 -146
  35. package/src/marks/Density1DMark.js +0 -104
  36. package/src/marks/Density2DMark.js +0 -69
  37. package/src/marks/Grid2DMark.js +0 -191
  38. package/src/marks/HexbinMark.js +0 -88
  39. package/src/marks/Mark.js +0 -195
  40. package/src/marks/RasterMark.js +0 -122
  41. package/src/marks/RasterTileMark.js +0 -332
  42. package/src/marks/RegressionMark.js +0 -117
  43. package/src/marks/util/bin-field.js +0 -17
  44. package/src/marks/util/density.js +0 -226
  45. package/src/marks/util/extent.js +0 -56
  46. package/src/marks/util/grid.js +0 -57
  47. package/src/marks/util/handle-param.js +0 -14
  48. package/src/marks/util/is-arrow-table.js +0 -3
  49. package/src/marks/util/is-color.js +0 -18
  50. package/src/marks/util/is-constant-option.js +0 -40
  51. package/src/marks/util/is-symbol.js +0 -20
  52. package/src/marks/util/raster.js +0 -44
  53. package/src/marks/util/stats.js +0 -133
  54. package/src/marks/util/to-data-array.js +0 -58
  55. package/src/plot-attributes.js +0 -211
  56. package/src/plot-renderer.js +0 -161
  57. package/src/plot.js +0 -136
  58. package/src/spec/parse-data.js +0 -69
  59. package/src/spec/parse-spec.js +0 -422
  60. package/src/spec/to-module.js +0 -465
  61. package/src/spec/util.js +0 -43
  62. package/src/symbols.js +0 -3
  63. package/src/transforms/bin.js +0 -81
  64. package/src/transforms/index.js +0 -3
  65. /package/src/{directives → plot}/data.js +0 -0
@@ -1,8 +1,8 @@
1
1
  import { isParam } from '@uwdata/mosaic-core';
2
- import { namedPlots } from './plot.js';
2
+ import { setNamedPlot } from './named-plots.js';
3
3
 
4
4
  export function name(name) {
5
- return plot => namedPlots.set(name, 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
- export function attribute(name, value) {
22
+ function attribute(name, value) {
23
23
  return plot => { setAttribute(plot, name, value); };
24
24
  }
25
25
 
26
- export function attributes(values) {
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 { Highlight } from '../interactors/Highlight.js';
2
- import { Toggle } from '../interactors/Toggle.js';
3
- import { Interval1D } from '../interactors/Interval1D.js';
4
- import { Interval2D } from '../interactors/Interval2D.js';
5
- import { PanZoom } from '../interactors/PanZoom.js';
6
- import { Nearest } from '../interactors/Nearest.js';
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 '../legend.js';
2
- import { namedPlots } from './plot.js';
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
- namedPlots.request(maybePlot, add);
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 const colorLegend = options => legend('color', options);
22
- export const opacityLegend = options => legend('opacity', options);
23
- export const symbolLegend = options => legend('symbol', options);
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 { Mark } from '../marks/Mark.js';
2
- import { ConnectedMark } from '../marks/ConnectedMark.js';
3
- import { Density1DMark } from '../marks/Density1DMark.js';
4
- import { Density2DMark } from '../marks/Density2DMark.js';
5
- import { DenseLineMark } from '../marks/DenseLineMark.js';
6
- import { ContourMark } from '../marks/ContourMark.js';
7
- import { HexbinMark } from '../marks/HexbinMark.js';
8
- import { RasterMark } from '../marks/RasterMark.js';
9
- import { RasterTileMark } from '../marks/RasterTileMark.js';
10
- import { RegressionMark } from '../marks/RegressionMark.js';
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) => mark('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
+ }
@@ -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
+ }
@@ -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
- }