@uwdata/vgplot 0.4.0 → 0.6.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 +6677 -6503
  3. package/dist/vgplot.min.js +12 -33
  4. package/package.json +8 -10
  5. package/src/api.js +347 -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 +32 -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 +18 -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
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
- }
@@ -1,146 +0,0 @@
1
- import { Query, and, count, isNull, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
- import { binField, bin1d } from './util/bin-field.js';
3
- import { extentX, extentY } from './util/extent.js';
4
- import { handleParam } from './util/handle-param.js';
5
- import { RasterMark } from './RasterMark.js';
6
-
7
- export class DenseLineMark extends RasterMark {
8
- constructor(source, options) {
9
- const { normalize = true, ...rest } = options;
10
- super(source, { bandwidth: 0, ...rest });
11
- handleParam(this, 'normalize', normalize);
12
- }
13
-
14
- query(filter = []) {
15
- const { plot, channels, normalize, source } = this;
16
- const [x0, x1] = extentX(this, filter);
17
- const [y0, y1] = extentY(this, filter);
18
- const [nx, ny] = this.bins = this.binDimensions(this);
19
- const bx = binField(this, 'x');
20
- const by = binField(this, 'y');
21
- const rx = !!plot.getAttribute('xReverse');
22
- const ry = !!plot.getAttribute('yReverse');
23
- const x = bin1d(bx, x0, x1, nx, rx, this.binPad);
24
- const y = bin1d(by, y0, y1, ny, ry, this.binPad);
25
-
26
- const q = Query
27
- .from(source.table)
28
- .where(stripXY(this, filter));
29
-
30
- const groupby = this.groupby = [];
31
- const z = [];
32
- for (const c of channels) {
33
- if (Object.hasOwn(c, 'field')) {
34
- const { channel, field } = c;
35
- if (channel === 'z') {
36
- q.select({ [channel]: field });
37
- z.push('z');
38
- } else if (channel !== 'x' && channel !== 'y') {
39
- q.select({ [channel]: field });
40
- groupby.push(channel);
41
- }
42
- }
43
- }
44
-
45
- return lineDensity(q, x, y, z, nx, ny, groupby, normalize);
46
- }
47
- }
48
-
49
- // strip x, y fields from filter predicate
50
- // to prevent improper clipping of line segments
51
- // TODO: improve, perhaps with supporting query utilities
52
- function stripXY(mark, filter) {
53
- if (Array.isArray(filter) && !filter.length) return filter;
54
-
55
- const xc = mark.channelField('x').field.column;
56
- const yc = mark.channelField('y').field.column;
57
- const test = p => p.op !== 'BETWEEN'
58
- || p.field.column !== xc && p.field.column !== yc;
59
- const filterAnd = p => p.op === 'AND'
60
- ? and(p.children.filter(c => test(c)))
61
- : p;
62
-
63
- return Array.isArray(filter)
64
- ? filter.filter(p => test(p)).map(p => filterAnd(p))
65
- : filterAnd(filter);
66
- }
67
-
68
- function lineDensity(
69
- q, x, y, z, xn, yn,
70
- groupby = [], normalize = true
71
- ) {
72
- // select x, y points binned to the grid
73
- q.select({
74
- x: sql`FLOOR(${x})::INTEGER`,
75
- y: sql`FLOOR(${y})::INTEGER`
76
- });
77
-
78
- // select line segment end point pairs
79
- const groups = groupby.concat(z);
80
- const pairPart = groups.length ? `PARTITION BY ${groups.join(', ')} ` : '';
81
- const pairs = Query
82
- .from(q)
83
- .select(groups, {
84
- x0: 'x',
85
- y0: 'y',
86
- dx: sql`(lead(x) OVER sw - x)`,
87
- dy: sql`(lead(y) OVER sw - y)`
88
- })
89
- .window({ sw: sql`${pairPart}ORDER BY x ASC` })
90
- .qualify(and(
91
- sql`(x0 < ${xn} OR x0 + dx < ${xn})`,
92
- sql`(y0 < ${yn} OR y0 + dy < ${yn})`,
93
- sql`(x0 > 0 OR x0 + dx > 0)`,
94
- sql`(y0 > 0 OR y0 + dy > 0)`
95
- ));
96
-
97
- // indices to join against for rasterization
98
- // generate the maximum number of indices needed
99
- const num = Query
100
- .select({ x: sql`GREATEST(MAX(ABS(dx)), MAX(ABS(dy)))` })
101
- .from('pairs');
102
- const indices = Query.select({ i: sql`UNNEST(range((${num})))::INTEGER` });
103
-
104
- // rasterize line segments
105
- const raster = Query.unionAll(
106
- Query
107
- .select(groups, {
108
- x: sql`x0 + i`,
109
- y: sql`y0 + ROUND(i * dy / dx::FLOAT)::INTEGER`
110
- })
111
- .from('pairs', 'indices')
112
- .where(sql`ABS(dy) <= ABS(dx) AND i < ABS(dx)`),
113
- Query
114
- .select(groups, {
115
- x: sql`x0 + ROUND(SIGN(dy) * i * dx / dy::FLOAT)::INTEGER`,
116
- y: sql`y0 + SIGN(dy) * i`
117
- })
118
- .from('pairs', 'indices')
119
- .where(sql`ABS(dy) > ABS(dx) AND i < ABS(dy)`),
120
- Query
121
- .select(groups, { x: 'x0', y: 'y0' })
122
- .from('pairs')
123
- .where(isNull('dx'))
124
- );
125
-
126
- // filter raster, normalize columns for each series
127
- const pointPart = ['x'].concat(groups).join(', ');
128
- const points = Query
129
- .from('raster')
130
- .select(groups, 'x', 'y',
131
- normalize
132
- ? { w: sql`1.0 / COUNT(*) OVER (PARTITION BY ${pointPart})` }
133
- : null
134
- )
135
- .where(and(isBetween('x', [0, xn]), isBetween('y', [0, yn])));
136
-
137
- // sum normalized, rasterized series into output grids
138
- return Query
139
- .with({ pairs, indices, raster, points })
140
- .from('points')
141
- .select(groupby, {
142
- index: sql`x + y * ${xn}::INTEGER`,
143
- value: normalize ? sum('w') : count()
144
- })
145
- .groupby('index', groupby);
146
- }
@@ -1,104 +0,0 @@
1
- import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
- import { Transient } from '../symbols.js';
3
- import { binField, bin1d } from './util/bin-field.js';
4
- import { dericheConfig, dericheConv1d } from './util/density.js';
5
- import { extentX, extentY, xext, yext } from './util/extent.js';
6
- import { grid1d } from './util/grid.js';
7
- import { handleParam } from './util/handle-param.js';
8
- import { Mark, channelOption, markQuery } from './Mark.js';
9
-
10
- export class Density1DMark extends Mark {
11
- constructor(type, source, options) {
12
- const { bins = 1024, bandwidth = 20, ...channels } = options;
13
- const dim = type.endsWith('X') ? 'y' : 'x';
14
-
15
- super(type, source, channels, dim === 'x' ? xext : yext);
16
- this.dim = dim;
17
-
18
- handleParam(this, 'bins', bins);
19
- handleParam(this, 'bandwidth', bandwidth, () => {
20
- return this.grid ? this.convolve().update() : null
21
- });
22
- }
23
-
24
- get filterIndexable() {
25
- const name = this.dim === 'x' ? 'xDomain' : 'yDomain';
26
- const dom = this.plot.getAttribute(name);
27
- return dom && !dom[Transient];
28
- }
29
-
30
- query(filter = []) {
31
- if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
32
- const { bins, channels, dim, source: { table } } = this;
33
- const [lo, hi] = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
34
- const bx = binField(this, dim);
35
- return binLinear1d(
36
- markQuery(channels, table, [dim])
37
- .where(filter.concat(isBetween(bx, [lo, hi]))),
38
- bin1d(bx, lo, hi, bins),
39
- this.channelField('weight') ? 'weight' : null
40
- );
41
- }
42
-
43
- queryResult(data) {
44
- this.grid = grid1d(this.bins, data);
45
- return this.convolve();
46
- }
47
-
48
- convolve() {
49
- const { bins, bandwidth, dim, grid, plot, extent: [lo, hi] } = this;
50
-
51
- // perform smoothing
52
- const neg = grid.some(v => v < 0);
53
- const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
54
- const config = dericheConfig(bandwidth * (bins - 1) / size, neg);
55
- const result = dericheConv1d(config, grid, bins);
56
-
57
- // map smoothed grid values to sample data points
58
- const points = this.data = [];
59
- const v = dim === 'x' ? 'y' : 'x';
60
- const b = this.channelField(dim).as;
61
- const b0 = +lo;
62
- const delta = (hi - b0) / (bins - 1);
63
- const scale = 1 / delta;
64
- for (let i = 0; i < bins; ++i) {
65
- points.push({
66
- [b]: b0 + i * delta,
67
- [v]: result[i] * scale
68
- });
69
- }
70
-
71
- return this;
72
- }
73
-
74
- plotSpecs() {
75
- const { type, data, channels, dim } = this;
76
- const options = dim === 'x' ? { y: 'y' } : { x: 'x' };
77
- for (const c of channels) {
78
- options[c.channel] = channelOption(c);
79
- }
80
- return [{ type, data, options }];
81
- }
82
- }
83
-
84
- function binLinear1d(q, p, value) {
85
- const w = value ? `* ${value}` : '';
86
-
87
- const u = q.clone().select({
88
- p,
89
- i: sql`FLOOR(p)::INTEGER`,
90
- w: sql`(FLOOR(p) + 1 - p)${w}`
91
- });
92
-
93
- const v = q.clone().select({
94
- p,
95
- i: sql`FLOOR(p)::INTEGER + 1`,
96
- w: sql`(p - FLOOR(p))${w}`
97
- });
98
-
99
- return Query
100
- .from(Query.unionAll(u, v))
101
- .select({ index: 'i', value: sum('w') })
102
- .groupby('index')
103
- .having(gt('value', 0));
104
- }
@@ -1,69 +0,0 @@
1
- import { handleParam } from './util/handle-param.js';
2
- import { Grid2DMark } from './Grid2DMark.js';
3
- import { channelOption } from './Mark.js';
4
-
5
- export class Density2DMark extends Grid2DMark {
6
- constructor(source, options) {
7
- const { type = 'dot', binsX, binsY, ...channels } = options;
8
- channels.binPad = channels.binPad ?? 0;
9
- super(type, source, channels);
10
- handleParam(this, 'binsX', binsX);
11
- handleParam(this, 'binsY', binsY);
12
- }
13
-
14
- convolve() {
15
- super.convolve();
16
- const { bins, binPad, extentX, extentY } = this;
17
- const [nx, ny] = bins;
18
- const [x0, x1] = extentX;
19
- const [y0, y1] = extentY;
20
- const deltaX = (x1 - x0) / (nx - binPad);
21
- const deltaY = (y1 - y0) / (ny - binPad);
22
- const offset = binPad ? 0 : 0.5;
23
- this.data = points(this.kde, bins, x0, y0, deltaX, deltaY, offset);
24
- return this;
25
- }
26
-
27
- binDimensions() {
28
- const { plot, binWidth, binsX, binsY } = this;
29
- return [
30
- binsX ?? Math.round(plot.innerWidth() / binWidth),
31
- binsY ?? Math.round(plot.innerHeight() / binWidth)
32
- ];
33
- }
34
-
35
- plotSpecs() {
36
- const { type, channels, densityMap, data } = this;
37
- const options = {};
38
- for (const c of channels) {
39
- const { channel } = c;
40
- options[channel] = (channel === 'x' || channel === 'y')
41
- ? channel // use generated x/y data fields
42
- : channelOption(c);
43
- }
44
- for (const channel in densityMap) {
45
- if (densityMap[channel]) {
46
- options[channel] = 'density';
47
- }
48
- }
49
- return [{ type, data, options }];
50
- }
51
- }
52
-
53
- function points(kde, bins, x0, y0, deltaX, deltaY, offset) {
54
- const scale = 1 / (deltaX * deltaY);
55
- const [nx, ny] = bins;
56
- const data = [];
57
- for (const grid of kde) {
58
- for (let k = 0, j = 0; j < ny; ++j) {
59
- for (let i = 0; i < nx; ++i, ++k) {
60
- data.push({
61
- x: x0 + (i + offset) * deltaX,
62
- y: y0 + (j + offset) * deltaY,
63
- density: grid[k] * scale
64
- });
65
- }
66
- }
67
- }
68
- return data;
69
- }