@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,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
- }
@@ -1,191 +0,0 @@
1
- import { Query, count, gt, isBetween, lt, lte, sql, sum } from '@uwdata/mosaic-sql';
2
- import { Transient } from '../symbols.js';
3
- import { binField } from './util/bin-field.js';
4
- import { dericheConfig, dericheConv2d } from './util/density.js';
5
- import { extentX, extentY, xyext } from './util/extent.js';
6
- import { grid2d } from './util/grid.js';
7
- import { handleParam } from './util/handle-param.js';
8
- import { Mark } from './Mark.js';
9
-
10
- export class Grid2DMark extends Mark {
11
- constructor(type, source, options) {
12
- const {
13
- bandwidth = 20,
14
- binType = 'linear',
15
- binWidth = 2,
16
- binPad = 1,
17
- ...channels
18
- } = options;
19
-
20
- const densityMap = createDensityMap(channels);
21
- super(type, source, channels, xyext);
22
- this.densityMap = densityMap;
23
-
24
- handleParam(this, 'bandwidth', bandwidth, () => {
25
- return this.grids ? this.convolve().update() : null;
26
- });
27
- handleParam(this, 'binWidth', binWidth);
28
- handleParam(this, 'binType', binType);
29
- handleParam(this, 'binPad', binPad);
30
- }
31
-
32
- setPlot(plot, index) {
33
- const update = () => { if (this.stats) this.requestUpdate(); };
34
- plot.addAttributeListener('domainX', update);
35
- plot.addAttributeListener('domainY', update);
36
- return super.setPlot(plot, index);
37
- }
38
-
39
- get filterIndexable() {
40
- const xdom = this.plot.getAttribute('xDomain');
41
- const ydom = this.plot.getAttribute('yDomain');
42
- return xdom && ydom && !xdom[Transient] && !ydom[Transient];
43
- }
44
-
45
- query(filter = []) {
46
- const { plot, binType, binPad, channels, densityMap, source } = this;
47
- const [x0, x1] = this.extentX = extentX(this, filter);
48
- const [y0, y1] = this.extentY = extentY(this, filter);
49
- const [nx, ny] = this.bins = this.binDimensions(this);
50
- const bx = binField(this, 'x');
51
- const by = binField(this, 'y');
52
- const rx = !!plot.getAttribute('xReverse');
53
- const ry = !!plot.getAttribute('yReverse');
54
- const x = bin1d(bx, x0, x1, nx, rx, this.binPad);
55
- const y = bin1d(by, y0, y1, ny, ry, this.binPad);
56
-
57
- // with padded bins, include the entire domain extent
58
- // if the bins are flush, exclude the extent max
59
- const bounds = binPad
60
- ? [isBetween(bx, [x0, x1]), isBetween(by, [y0, y1])]
61
- : [lte(x0, bx), lt(bx, x1), lte(y0, by), lt(by, y1)];
62
-
63
- const q = Query
64
- .from(source.table)
65
- .where(filter.concat(bounds));
66
-
67
- const groupby = this.groupby = [];
68
- let agg = count();
69
- for (const c of channels) {
70
- if (Object.hasOwn(c, 'field')) {
71
- const { as, channel, field } = c;
72
- if (field.aggregate) {
73
- agg = field;
74
- densityMap[channel] = true;
75
- } else if (channel === 'weight') {
76
- agg = sum(field);
77
- } else if (channel !== 'x' && channel !== 'y') {
78
- q.select({ [as]: field });
79
- groupby.push(as);
80
- }
81
- }
82
- }
83
-
84
- return binType === 'linear'
85
- ? binLinear2d(q, x, y, agg, nx, groupby)
86
- : bin2d(q, x, y, agg, nx, groupby);
87
- }
88
-
89
- binDimensions() {
90
- const { plot, binWidth } = this;
91
- return [
92
- Math.round(plot.innerWidth() / binWidth),
93
- Math.round(plot.innerHeight() / binWidth)
94
- ];
95
- }
96
-
97
- queryResult(data) {
98
- const [nx, ny] = this.bins;
99
- this.grids = grid2d(nx, ny, data, this.groupby);
100
- return this.convolve();
101
- }
102
-
103
- convolve() {
104
- const { bandwidth, bins, grids, plot } = this;
105
-
106
- if (bandwidth <= 0) {
107
- this.kde = this.grids.map(({ key, grid }) => {
108
- return (grid.key = key, grid);
109
- });
110
- } else {
111
- const w = plot.innerWidth();
112
- const h = plot.innerHeight();
113
- const [nx, ny] = bins;
114
- const neg = grids.some(({ grid }) => grid.some(v => v < 0));
115
- const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
116
- const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
117
- this.kde = this.grids.map(({ key, grid }) => {
118
- const k = dericheConv2d(configX, configY, grid, bins);
119
- return (k.key = key, k);
120
- });
121
- }
122
- return this;
123
- }
124
-
125
- plotSpecs() {
126
- throw new Error('Unimplemented. Use a Grid2D mark subclass.');
127
- }
128
- }
129
-
130
- function createDensityMap(channels) {
131
- const densityMap = {};
132
- for (const key in channels) {
133
- if (channels[key] === 'density') {
134
- delete channels[key];
135
- densityMap[key] = true;
136
- }
137
- }
138
- return densityMap;
139
- }
140
-
141
- function bin1d(x, x0, x1, n, reverse, pad) {
142
- const d = (n - pad) / (x1 - x0);
143
- const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
144
- return reverse
145
- ? sql`(${x1} - ${x}::DOUBLE)${f}`
146
- : sql`(${x}::DOUBLE - ${x0})${f}`;
147
- }
148
-
149
- function bin2d(q, xp, yp, value, xn, groupby) {
150
- return q
151
- .select({
152
- index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
153
- value
154
- })
155
- .groupby('index', groupby);
156
- }
157
-
158
- function binLinear2d(q, xp, yp, value, xn, groupby) {
159
- const w = value.column ? `* ${value.column}` : '';
160
- const subq = (i, w) => q.clone().select({ xp, yp, i, w });
161
-
162
- // grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
163
- const a = subq(
164
- sql`FLOOR(xp)::INTEGER + FLOOR(yp)::INTEGER * ${xn}`,
165
- sql`(FLOOR(xp)::INTEGER + 1 - xp) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
166
- );
167
-
168
- // grid[xu + yv * xn] += (xv - xp) * (yp - yu) * wi;
169
- const b = subq(
170
- sql`FLOOR(xp)::INTEGER + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
171
- sql`(FLOOR(xp)::INTEGER + 1 - xp) * (yp - FLOOR(yp)::INTEGER)${w}`
172
- );
173
-
174
- // grid[xv + yu * xn] += (xp - xu) * (yv - yp) * wi;
175
- const c = subq(
176
- sql`FLOOR(xp)::INTEGER + 1 + FLOOR(yp)::INTEGER * ${xn}`,
177
- sql`(xp - FLOOR(xp)::INTEGER) * (FLOOR(yp)::INTEGER + 1 - yp)${w}`
178
- );
179
-
180
- // grid[xv + yv * xn] += (xp - xu) * (yp - yu) * wi;
181
- const d = subq(
182
- sql`FLOOR(xp)::INTEGER + 1 + (FLOOR(yp)::INTEGER + 1) * ${xn}`,
183
- sql`(xp - FLOOR(xp)::INTEGER) * (yp - FLOOR(yp)::INTEGER)${w}`
184
- );
185
-
186
- return Query
187
- .from(Query.unionAll(a, b, c, d))
188
- .select({ index: 'i', value: sum('w') }, groupby)
189
- .groupby('index', groupby)
190
- .having(gt('value', 0));
191
- }
@@ -1,88 +0,0 @@
1
- import { Query, isNotNull, sql } from '@uwdata/mosaic-sql';
2
- import { Transient } from '../symbols.js';
3
- import { extentX, extentY, xyext } from './util/extent.js';
4
- import { Mark } from './Mark.js';
5
-
6
- export class HexbinMark extends Mark {
7
- constructor(source, options) {
8
- const { type = 'hexagon', binWidth = 20, ...channels } = options;
9
- super(type, source, { r: binWidth / 2, clip: true, ...channels }, xyext);
10
- this.binWidth = binWidth;
11
- }
12
-
13
- get filterIndexable() {
14
- const xdom = this.plot.getAttribute('xDomain');
15
- const ydom = this.plot.getAttribute('yDomain');
16
- return xdom && ydom && !xdom[Transient] && !ydom[Transient];
17
- }
18
-
19
- query(filter = []) {
20
- if (this.hasOwnData()) return null;
21
- const { plot, binWidth, channels, source } = this;
22
-
23
- // get x / y extents, may update plot domainX / domainY
24
- const [x1, x2] = extentX(this, filter);
25
- const [y1, y2] = extentY(this, filter);
26
-
27
- // Adjust screen-space coordinates by top/left
28
- // margins as this is what Observable Plot does.
29
- // TODO use zero margins when faceted?
30
- const ox = 0.5 - plot.getAttribute('marginLeft');
31
- const oy = 0 - plot.getAttribute('marginTop');
32
- const dx = `${binWidth}::DOUBLE`;
33
- const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
34
- const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
35
- const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
36
-
37
- // Extract channel information, update top-level query
38
- // and extract dependent columns for aggregates
39
- let x, y;
40
- const aggr = new Set;
41
- const cols = {};
42
- for (const c of channels) {
43
- if (c.channel === 'orderby') {
44
- q.orderby(c.value); // TODO revisit once groupby is added
45
- } else if (c.channel === 'x') {
46
- x = c;
47
- } else if (c.channel === 'y') {
48
- y = c;
49
- } else if (Object.hasOwn(c, 'field')) {
50
- cols[c.as] = c.field;
51
- if (c.field.aggregate) {
52
- c.field.columns.forEach(col => aggr.add(col));
53
- }
54
- }
55
- }
56
-
57
- // Top-level query; we add a hex binning subquery below
58
- // Maps binned screen space coordinates back to data
59
- // values to ensure we get correct data-driven scales
60
- const q = Query.select({
61
- [x.as]: sql`${x1}::DOUBLE + ((x + 0.5 * (y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
62
- [y.as]: sql`${y2}::DOUBLE - (y * ${dy} + ${oy})::DOUBLE / ${yr}`,
63
- ...cols
64
- }).groupby('x', 'y');
65
-
66
- // Map x/y channels to screen space
67
- const xx = `${xr} * (${x.field} - ${x1}::DOUBLE)`;
68
- const yy = `${yr} * (${y2}::DOUBLE - ${y.field})`;
69
-
70
- // Perform hex binning of x/y coordinates
71
- // TODO add groupby dims
72
- const hex = Query
73
- .select({
74
- py: sql`(${yy} - ${oy}) / ${dy}`,
75
- pj: sql`ROUND(py)::INTEGER`,
76
- px: sql`(${xx} - ${ox}) / ${dx} - 0.5 * (pj & 1)`,
77
- pi: sql`ROUND(px)::INTEGER`,
78
- tt: sql`ABS(py-pj) * 3 > 1 AND (px-pi)**2 + (py-pj)**2 > (px - pi - 0.5 * CASE WHEN px < pi THEN -1 ELSE 1 END)**2 + (py - pj - CASE WHEN py < pj THEN -1 ELSE 1 END)**2`,
79
- x: sql`CASE WHEN tt THEN (pi + (CASE WHEN px < pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE pi END`,
80
- y: sql`CASE WHEN tt THEN (pj + CASE WHEN py < pj THEN -1 ELSE 1 END)::INTEGER ELSE pj END`
81
- })
82
- .select(Array.from(aggr))
83
- .from(source.table)
84
- .where(isNotNull(x.field), isNotNull(y.field), filter)
85
-
86
- return q.from(hex);
87
- }
88
- }