@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,121 +0,0 @@
1
- import { select, zoom, ZoomTransform } from 'd3';
2
- import { Selection } from '@uwdata/mosaic-core';
3
- import { isBetween } from '@uwdata/mosaic-sql';
4
- import { getField } from './util/get-field.js';
5
-
6
- const asc = (a, b) => a - b;
7
-
8
- export class PanZoom {
9
- constructor(mark, {
10
- x = new Selection(),
11
- y = new Selection(),
12
- xfield,
13
- yfield,
14
- zoom = true,
15
- panx = true,
16
- pany = true
17
- }) {
18
- this.mark = mark;
19
- this.xsel = x;
20
- this.ysel = y;
21
- this.xfield = xfield || getField(mark, ['x', 'x1', 'x2']);
22
- this.yfield = yfield || getField(mark, ['y', 'y1', 'y2']);
23
- this.zoom = extent(zoom, [0, Infinity], [1, 1]);
24
- this.panx = this.xsel && panx;
25
- this.pany = this.ysel && pany;
26
-
27
- const { plot } = mark;
28
- if (panx) {
29
- this.xsel.addEventListener('value', value => {
30
- if (plot.setAttribute('xDomain', value)) plot.update();
31
- });
32
- }
33
- if (pany) {
34
- this.ysel.addEventListener('value', value => {
35
- if (plot.setAttribute('yDomain', value)) plot.update();
36
- });
37
- }
38
- }
39
-
40
- publish(transform) {
41
- if (this.panx) {
42
- const xdom = rescaleX(transform, this.xscale);
43
- this.xsel.update(this.clause(xdom, this.xfield, this.xscale));
44
- }
45
- if (this.pany) {
46
- const ydom = rescaleY(transform, this.yscale);
47
- this.ysel.update(this.clause(ydom, this.yfield, this.yscale));
48
- }
49
- }
50
-
51
- clause(value, field, scale) {
52
- return {
53
- source: this,
54
- schema: { type: 'interval', scales: [scale] },
55
- clients: this.mark.plot.markSet,
56
- value,
57
- predicate: value ? isBetween(field, value) : null
58
- };
59
- }
60
-
61
- init(svg) {
62
- this.svg = svg;
63
- if (this.initialized) return; else this.initialized = true;
64
-
65
- const { panx, pany, mark: { plot: { element } }, xsel, ysel } = this;
66
-
67
- this.xscale = svg.scale('x');
68
- this.yscale = svg.scale('y');
69
- const rx = this.xscale.range.slice().sort(asc);
70
- const ry = this.yscale.range.slice().sort(asc);
71
- const tx = extent(panx, [-Infinity, Infinity], rx);
72
- const ty = extent(pany, [-Infinity, Infinity], ry);
73
-
74
- const z = zoom()
75
- .extent([[rx[0], ry[0]], [rx[1], ry[1]]])
76
- .scaleExtent(this.zoom)
77
- .translateExtent([[tx[0], ty[0]], [tx[1], ty[1]]])
78
- .on('start', () => {
79
- this.xscale = this.svg.scale('x');
80
- this.yscale = this.svg.scale('y');
81
- })
82
- .on('end', () => element.__zoom = new ZoomTransform(1, 0, 0))
83
- .on('zoom', ({ transform }) => this.publish(transform));
84
-
85
- select(element).call(z);
86
-
87
- if (panx || pany) {
88
- let enter = false;
89
- element.addEventListener('mouseenter', () => {
90
- if (enter) return; else enter = true;
91
- if (panx) {
92
- const { xscale, xfield } = this;
93
- xsel.activate(this.clause(xscale.domain, xfield, xscale));
94
- }
95
- if (pany) {
96
- const { yscale, yfield } = this;
97
- ysel.activate(this.clause(yscale.domain, yfield, yscale));
98
- }
99
- });
100
- element.addEventListener('mouseleave', () => enter = false);
101
- }
102
- }
103
- }
104
-
105
- function extent(ext, defaultTrue, defaultFalse) {
106
- return ext
107
- ? (Array.isArray(ext) ? ext : defaultTrue)
108
- : defaultFalse;
109
- }
110
-
111
- function rescaleX(transform, scale) {
112
- return scale.range
113
- .map(transform.invertX, transform)
114
- .map(scale.invert, scale);
115
- }
116
-
117
- function rescaleY(transform, scale) {
118
- return scale.range
119
- .map(transform.invertY, transform)
120
- .map(scale.invert, scale);
121
- }
@@ -1,111 +0,0 @@
1
- import { and, or, isNotDistinct, literal } from '@uwdata/mosaic-sql';
2
-
3
- export class Toggle {
4
- constructor(mark, {
5
- selection,
6
- channels,
7
- peers = true
8
- }) {
9
- this.value = null;
10
- this.mark = mark;
11
- this.selection = selection;
12
- this.peers = peers;
13
- this.channels = channels.map(c => {
14
- const q = c === 'color' ? ['fill', 'stroke']
15
- : c === 'x' ? ['x', 'x1', 'x2']
16
- : c === 'y' ? ['y', 'y1', 'y2']
17
- : [c];
18
- for (let i = 0; i < q.length; ++i) {
19
- const f = mark.channelField(q[i]);
20
- if (f) return {
21
- field: f.field?.basis || f.field,
22
- as: f.as
23
- };
24
- }
25
- throw new Error(`Missing channel: ${c}`);
26
- });
27
- }
28
-
29
- clause(value) {
30
- const { channels, mark } = this;
31
- let predicate = null;
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 {
44
- source: this,
45
- schema: { type: 'point' },
46
- clients: this.peers ? mark.plot.markSet : new Set().add(mark),
47
- value,
48
- predicate
49
- };
50
- }
51
-
52
- init(svg, selector, accessor) {
53
- const { mark, channels, selection } = this;
54
- const { data } = mark;
55
- accessor = accessor || (target => {
56
- const datum = data[target.__data__];
57
- return channels.map(c => datum[c.as]);
58
- });
59
-
60
- selector = selector || `[data-index="${mark.index}"]`;
61
- const groups = new Set(svg.querySelectorAll(selector));
62
-
63
- svg.addEventListener('pointerdown', evt => {
64
- const state = selection.single ? selection.value : this.value;
65
- const target = evt.target;
66
- let value = null;
67
-
68
- if (isTargetElement(groups, target)) {
69
- const point = accessor(target);
70
- if (evt.shiftKey && state?.length) {
71
- value = state.filter(s => neq(s, point));
72
- if (value.length === state.length) value.push(point);
73
- } else if (state?.length === 1 && !neq(state[0], point)) {
74
- value = null;
75
- } else {
76
- value = [point];
77
- }
78
- }
79
-
80
- this.value = value;
81
- if (neqSome(state, value)) {
82
- selection.update(this.clause(value));
83
- }
84
- });
85
-
86
- svg.addEventListener('pointerenter', () => {
87
- this.selection.activate(this.clause([this.channels.map(() => 0)]));
88
- });
89
- }
90
- }
91
-
92
- function isTargetElement(groups, node) {
93
- return groups.has(node)
94
- || groups.has(node.parentNode)
95
- || groups.has(node.parentNode?.parentNode);
96
- }
97
-
98
- function neqSome(a, b) {
99
- return (a == null || b == null)
100
- ? (a != null || b != null)
101
- : (a.length !== b.length || a.some((x, i) => neq(x, b[i])));
102
- }
103
-
104
- function neq(a, b) {
105
- const n = a.length;
106
- if (b.length !== n) return true;
107
- for (let i = 0; i < n; ++i) {
108
- if (a[i] !== b[i]) return true;
109
- }
110
- return false;
111
- }
@@ -1,45 +0,0 @@
1
- import {
2
- brush as d3_brush, brushX as d3_brushX, brushY as d3_brushY
3
- } from 'd3';
4
-
5
- function wrap(brush) {
6
- const brushOn = brush.on;
7
- let enabled = true;
8
-
9
- function silence(callback) {
10
- enabled = false;
11
- callback();
12
- enabled = true;
13
- }
14
-
15
- brush.reset = (...args) => {
16
- silence(() => brush.clear(...args));
17
- };
18
-
19
- brush.moveSilent = (...args) => {
20
- silence(() => brush.move(...args));
21
- };
22
-
23
- brush.on = (...args) => {
24
- if (args.length > 1 && args[1]) {
25
- // wrap callback to respect enabled flag
26
- const callback = args[1];
27
- args[1] = (...event) => enabled && callback(...event);
28
- }
29
- return brushOn(...args);
30
- };
31
-
32
- return brush;
33
- }
34
-
35
- export function brush() {
36
- return wrap(d3_brush());
37
- }
38
-
39
- export function brushX() {
40
- return wrap(d3_brushX());
41
- }
42
-
43
- export function brushY() {
44
- return wrap(d3_brushY());
45
- }
@@ -1,9 +0,0 @@
1
- const EPS = 1e-12;
2
-
3
- export function closeTo(a, b) {
4
- return a === b || (
5
- a && b &&
6
- Math.abs(a[0] - b[0]) < EPS &&
7
- Math.abs(a[1] - b[1]) < EPS
8
- ) || false;
9
- }
@@ -1,4 +0,0 @@
1
- export function getField(mark, channels) {
2
- const field = mark.channelField(channels)?.field;
3
- return field?.basis || field;
4
- }
@@ -1,3 +0,0 @@
1
- export function invert(value, scale, pixelSize = 1) {
2
- return scale.invert(pixelSize * Math.floor(value / pixelSize));
3
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * Patch the getScreenCTM method to memoize the last non-null
3
- * result seen. This will let the method continue to function
4
- * even after the node is removed from the DOM.
5
- */
6
- export function patchScreenCTM() {
7
- const node = this;
8
- const getScreenCTM = node.getScreenCTM;
9
- let memo;
10
- node.getScreenCTM = () => {
11
- return node.isConnected ? (memo = getScreenCTM.call(node)) : memo;
12
- };
13
- }
@@ -1,9 +0,0 @@
1
- import { toKebabCase } from './to-kebab-case.js';
2
-
3
- export function sanitizeStyles(styles) {
4
- const s = {};
5
- for (const name in styles) {
6
- s[toKebabCase(name)] = styles[name];
7
- }
8
- return s;
9
- }
@@ -1,9 +0,0 @@
1
- export function toKebabCase(cc) {
2
- const lc = cc.toLowerCase();
3
- const n = cc.length;
4
- let kc = '';
5
- for (let i = 0; i < n; ++i) {
6
- kc += (cc[i] !== lc[i] ? '-' : '') + lc[i];
7
- }
8
- return kc;
9
- }
@@ -1,2 +0,0 @@
1
- export { hconcat, vconcat } from './concat.js';
2
- export { hspace, vspace } from './space.js';
package/src/legend.js DELETED
@@ -1,64 +0,0 @@
1
- import { Toggle } from './interactors/Toggle.js';
2
-
3
- export class Legend {
4
- constructor(channel, options) {
5
- const { as, ...rest } = options;
6
- this.channel = channel;
7
- this.options = { label: null, ...rest };
8
- this.selection = as;
9
-
10
- this.element = document.createElement('div');
11
- this.element.setAttribute('class', 'legend');
12
- this.element.value = this;
13
- }
14
-
15
- setPlot(plot) {
16
- const { channel, selection } = this;
17
- const mark = findMark(plot, channel);
18
- if (this.selection && mark) {
19
- this.handler = new Toggle(mark, { selection, channels: [channel] });
20
- this.selection.addEventListener('value', () => this.update());
21
- }
22
- }
23
-
24
- init(svg) {
25
- const { channel, options, handler } = this;
26
- const scale = svg.scale(channel);
27
- const opt = scale.type === 'ordinal'
28
- ? options
29
- : { marginTop: 1, tickSize: 2, height: 28, ...options };
30
- this.legend = svg.legend(channel, opt);
31
-
32
- if (handler) {
33
- handler.init(this.legend, ':scope > div', el => [el.__data__]);
34
- this.update();
35
- }
36
-
37
- this.element.replaceChildren(this.legend);
38
- return this.element;
39
- }
40
-
41
- update() {
42
- if (!this.legend) return;
43
- const { value } = this.selection;
44
- const curr = value && value.length ? new Set(value.map(v => v[0])) : null;
45
- const nodes = this.legend.querySelectorAll(':scope > div');
46
- for (const node of nodes) {
47
- const selected = curr ? curr.has(node.__data__) : true;
48
- node.style.opacity = selected ? 1 : 0.2;
49
- }
50
- }
51
- }
52
-
53
- function findMark({ marks }, channel) {
54
- const channels = channel === 'color' ? ['fill', 'stroke']
55
- : channel === 'opacity' ? ['opacity', 'fillOpacity', 'strokeOpacity']
56
- : null;
57
- if (channels == null) return null;
58
- for (let i = marks.length - 1; i > -1; --i) {
59
- if (marks[i].channelField(channels)) {
60
- return marks[i];
61
- }
62
- }
63
- return null;
64
- }
@@ -1,63 +0,0 @@
1
- import { Query, argmax, argmin, max, min, sql } from '@uwdata/mosaic-sql';
2
- import { binField } from './util/bin-field.js';
3
- import { filteredExtent } from './util/extent.js';
4
- import { Mark } from './Mark.js';
5
-
6
- export class ConnectedMark extends Mark {
7
- constructor(type, source, encodings) {
8
- const dim = type.endsWith('X') ? 'y' : 'x';
9
- const req = { [dim]: ['count', 'min', 'max'] };
10
-
11
- super(type, source, encodings, req);
12
- this.dim = dim;
13
- }
14
-
15
- query(filter = []) {
16
- const { plot, dim, source, stats } = this;
17
- const { optimize = true } = source.options || {};
18
- const { field, as } = this.channelField(dim);
19
- const q = super.query(filter);
20
-
21
- if (optimize) {
22
- // TODO: handle stacked data
23
- const { column } = field;
24
- const { count, max, min } = stats[column];
25
- const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
26
-
27
- const [lo, hi] = filteredExtent(filter, column) || [min, max];
28
- const scale = (hi - lo) / (max - min);
29
- if (count * scale > size * 4) {
30
- const dd = binField(this, dim, as);
31
- const val = this.channelField(dim === 'x' ? 'y' : 'x').as;
32
- const cols = q.select().map(c => c.as).filter(c => c !== as && c !== val);
33
- return m4(q, dd, as, val, lo, hi, size, cols);
34
- }
35
- }
36
-
37
- return q.orderby(as);
38
- }
39
- }
40
-
41
- /**
42
- * M4 is an optimization for value-preserving time-series aggregation
43
- * (http://www.vldb.org/pvldb/vol7/p797-jugel.pdf). This implementation uses
44
- * an efficient version with a single scan and the aggregate function
45
- * argmin and argmax, following https://arxiv.org/pdf/2306.03714.pdf.
46
- */
47
- function m4(input, bx, x, y, lo, hi, width, cols = []) {
48
- const bins = sql`FLOOR(${width / (hi - lo)}::DOUBLE * (${bx} - ${+lo}::DOUBLE))::INTEGER`;
49
-
50
- const q = (sel) => Query
51
- .from(input)
52
- .select(sel)
53
- .groupby(bins, cols);
54
-
55
- return Query
56
- .union(
57
- q([{ [x]: min(x), [y]: argmin(y, x) }, ...cols]),
58
- q([{ [x]: max(x), [y]: argmax(y, x) }, ...cols]),
59
- q([{ [x]: argmin(x, y), [y]: min(y) }, ...cols]),
60
- q([{ [x]: argmax(x, y), [y]: max(y) }, ...cols])
61
- )
62
- .orderby(cols, x);
63
- }
@@ -1,89 +0,0 @@
1
- import { contours, max } from 'd3';
2
- import { handleParam } from './util/handle-param.js';
3
- import { Grid2DMark } from './Grid2DMark.js';
4
- import { channelOption } from './Mark.js';
5
-
6
- export class ContourMark extends Grid2DMark {
7
- constructor(source, options) {
8
- const { thresholds = 10, ...channels } = options;
9
- super('geo', source, channels);
10
- handleParam(this, 'thresholds', thresholds, () => {
11
- return this.grids ? this.contours().update() : null
12
- });
13
- }
14
-
15
- convolve() {
16
- return super.convolve().contours();
17
- }
18
-
19
- contours() {
20
- const { bins, densityMap, kde, thresholds, groupby, plot } = this;
21
-
22
- let tz = thresholds;
23
- if (!Array.isArray(tz)) {
24
- const scale = max(kde.map(k => max(k)));
25
- tz = Array.from({length: tz - 1}, (_, i) => (scale * (i + 1)) / tz);
26
- }
27
-
28
- if (densityMap.fill || densityMap.stroke) {
29
- if (this.plot.getAttribute('colorScale') !== 'log') {
30
- this.plot.setAttribute('colorZero', true);
31
- }
32
- }
33
-
34
- // transform contours into data space coordinates
35
- // so we play nice with scale domains & axes
36
- const [nx, ny] = bins;
37
- const [x0, x1] = plot.getAttribute('xDomain');
38
- const [y0, y1] = plot.getAttribute('yDomain');
39
- const sx = (x1 - x0) / nx;
40
- const sy = (y1 - y0) / ny;
41
- const xo = +x0;
42
- const yo = +y0;
43
- const x = v => xo + v * sx;
44
- const y = v => yo + v * sy;
45
- const contour = contours().size(bins);
46
-
47
- // generate contours
48
- this.data = kde.flatMap(k => tz.map(t => {
49
- const c = transform(contour.contour(k, t), x, y);
50
- groupby.forEach((name, i) => c[name] = k.key[i]);
51
- c.density = t;
52
- return c;
53
- }));
54
-
55
- return this;
56
- }
57
-
58
- plotSpecs() {
59
- const { type, channels, densityMap, data } = this;
60
- const options = {};
61
- for (const c of channels) {
62
- const { channel } = c;
63
- if (channel !== 'x' && channel !== 'y') {
64
- options[channel] = channelOption(c);
65
- }
66
- }
67
- if (densityMap.fill) options.fill = 'density';
68
- if (densityMap.stroke) options.stroke = 'density';
69
- return [{ type, data, options }];
70
- }
71
- }
72
-
73
- function transform(geometry, x, y) {
74
- function transformPolygon(coordinates) {
75
- coordinates.forEach(transformRing);
76
- }
77
-
78
- function transformRing(coordinates) {
79
- coordinates.forEach(transformPoint);
80
- }
81
-
82
- function transformPoint(coordinates) {
83
- coordinates[0] = x(coordinates[0]);
84
- coordinates[1] = y(coordinates[1]);
85
- }
86
-
87
- geometry.coordinates.forEach(transformPolygon);
88
- return geometry;
89
- }