@uwdata/mosaic-plot 0.5.0 → 0.6.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uwdata/mosaic-plot",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "A Mosaic-powered plotting framework based on Observable Plot.",
5
5
  "keywords": [
6
6
  "data",
@@ -29,10 +29,10 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@observablehq/plot": "^0.6.13",
32
- "@uwdata/mosaic-core": "^0.5.0",
33
- "@uwdata/mosaic-sql": "^0.5.0",
32
+ "@uwdata/mosaic-core": "^0.6.1",
33
+ "@uwdata/mosaic-sql": "^0.6.0",
34
34
  "d3": "^7.8.5",
35
35
  "isoformat": "^0.2.1"
36
36
  },
37
- "gitHead": "92886dddfb126c1439924c5a0189e4639c3519a7"
37
+ "gitHead": "9e788e6dc5241fa1c54967a25fd9599f97da1a41"
38
38
  }
package/src/index.js CHANGED
@@ -11,7 +11,7 @@ export { Density2DMark } from './marks/Density2DMark.js';
11
11
  export { GeoMark } from './marks/GeoMark.js';
12
12
  export { Grid2DMark } from './marks/Grid2DMark.js';
13
13
  export { HexbinMark } from './marks/HexbinMark.js';
14
- export { RasterMark } from './marks/RasterMark.js';
14
+ export { RasterMark, HeatmapMark } from './marks/RasterMark.js';
15
15
  export { RasterTileMark } from './marks/RasterTileMark.js';
16
16
  export { RegressionMark } from './marks/RegressionMark.js';
17
17
 
@@ -1,13 +1,12 @@
1
1
  import { Query, argmax, argmin, max, min, sql } from '@uwdata/mosaic-sql';
2
- import { binField } from './util/bin-field.js';
2
+ import { binExpr } from './util/bin-expr.js';
3
3
  import { filteredExtent } from './util/extent.js';
4
4
  import { Mark } from './Mark.js';
5
5
 
6
6
  export class ConnectedMark extends Mark {
7
7
  constructor(type, source, encodings) {
8
8
  const dim = type.endsWith('X') ? 'y' : type.endsWith('Y') ? 'x' : null;
9
- const req = { [dim]: ['count', 'min', 'max'] };
10
-
9
+ const req = { [dim]: ['min', 'max'] };
11
10
  super(type, source, encodings, req);
12
11
  this.dim = dim;
13
12
  }
@@ -16,28 +15,28 @@ export class ConnectedMark extends Mark {
16
15
  const { plot, dim, source, stats } = this;
17
16
  const { optimize = true } = source.options || {};
18
17
  const q = super.query(filter);
18
+ if (!dim) return q;
19
19
 
20
- if (optimize && dim) {
21
- const { field, as } = this.channelField(dim);
20
+ const ortho = dim === 'x' ? 'y' : 'x';
21
+ const value = this.channelField(ortho)?.as;
22
+ const { field, as } = this.channelField(dim);
23
+ const { type } = stats[field.column];
24
+ const isContinuous = type === 'date' || type === 'number';
22
25
 
23
- // TODO: handle stacked data
26
+ if (optimize && isContinuous && value) {
27
+ // TODO: handle stacked data!
24
28
  const { column } = field;
25
- const { count, max, min } = stats[column];
29
+ const { max, min } = stats[column];
26
30
  const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
27
-
28
31
  const [lo, hi] = filteredExtent(filter, column) || [min, max];
29
- const scale = (hi - lo) / (max - min);
30
- if (count * scale > size * 4) {
31
- const dd = binField(this, dim, as);
32
- const val = this.channelField(dim === 'x' ? 'y' : 'x').as;
33
- const cols = q.select().map(c => c.as).filter(c => c !== as && c !== val);
34
- return m4(q, dd, as, val, lo, hi, size, cols);
35
- }
36
-
37
- q.orderby(as);
32
+ const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
33
+ const cols = q.select()
34
+ .map(c => c.as)
35
+ .filter(c => c !== as && c !== value);
36
+ return m4(q, expr, as, value, cols);
37
+ } else {
38
+ return q.orderby(field);
38
39
  }
39
-
40
- return q;
41
40
  }
42
41
  }
43
42
 
@@ -47,13 +46,13 @@ export class ConnectedMark extends Mark {
47
46
  * an efficient version with a single scan and the aggregate function
48
47
  * argmin and argmax, following https://arxiv.org/pdf/2306.03714.pdf.
49
48
  */
50
- function m4(input, bx, x, y, lo, hi, width, cols = []) {
51
- const bins = sql`FLOOR(${width / (hi - lo)}::DOUBLE * (${bx} - ${+lo}::DOUBLE))::INTEGER`;
49
+ function m4(input, bin, x, y, cols = []) {
50
+ const pixel = sql`FLOOR(${bin})::INTEGER`;
52
51
 
53
52
  const q = (sel) => Query
54
53
  .from(input)
55
54
  .select(sel)
56
- .groupby(bins, cols);
55
+ .groupby(pixel, cols);
57
56
 
58
57
  return Query
59
58
  .union(
@@ -1,4 +1,5 @@
1
- import { contours, max } from 'd3';
1
+ import { contours } from 'd3';
2
+ import { gridDomainContinuous } from './util/grid.js';
2
3
  import { handleParam } from './util/handle-param.js';
3
4
  import { Grid2DMark } from './Grid2DMark.js';
4
5
  import { channelOption } from './Mark.js';
@@ -6,7 +7,12 @@ import { channelOption } from './Mark.js';
6
7
  export class ContourMark extends Grid2DMark {
7
8
  constructor(source, options) {
8
9
  const { thresholds = 10, ...channels } = options;
9
- super('geo', source, channels);
10
+ super('geo', source, {
11
+ bandwidth: 20,
12
+ interpolate: 'linear',
13
+ pixelSize: 2,
14
+ ...channels
15
+ });
10
16
  handleParam(this, 'thresholds', thresholds, () => {
11
17
  return this.grids ? this.contours().update() : null
12
18
  });
@@ -17,12 +23,12 @@ export class ContourMark extends Grid2DMark {
17
23
  }
18
24
 
19
25
  contours() {
20
- const { bins, densityMap, kde, thresholds, groupby, plot } = this;
26
+ const { bins, densityMap, kde, thresholds, plot } = this;
21
27
 
22
28
  let tz = thresholds;
23
29
  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);
30
+ const [, hi] = gridDomainContinuous(kde, 'density');
31
+ tz = Array.from({length: tz - 1}, (_, i) => (hi * (i + 1)) / tz);
26
32
  }
27
33
 
28
34
  if (densityMap.fill || densityMap.stroke) {
@@ -45,11 +51,11 @@ export class ContourMark extends Grid2DMark {
45
51
  const contour = contours().size(bins);
46
52
 
47
53
  // 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;
54
+ this.data = kde.flatMap(cell => tz.map(t => {
55
+ return Object.assign(
56
+ transform(contour.contour(cell.density, t), x, y),
57
+ { ...cell, density: t }
58
+ );
53
59
  }));
54
60
 
55
61
  return this;
@@ -1,5 +1,5 @@
1
1
  import { Query, and, count, isNull, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
- import { binField, bin1d } from './util/bin-field.js';
2
+ import { binExpr } from './util/bin-expr.js';
3
3
  import { extentX, extentY } from './util/extent.js';
4
4
  import { handleParam } from './util/handle-param.js';
5
5
  import { RasterMark } from './RasterMark.js';
@@ -7,26 +7,21 @@ import { RasterMark } from './RasterMark.js';
7
7
  export class DenseLineMark extends RasterMark {
8
8
  constructor(source, options) {
9
9
  const { normalize = true, ...rest } = options;
10
- super(source, { bandwidth: 0, ...rest });
10
+ super(source, rest);
11
11
  handleParam(this, 'normalize', normalize);
12
12
  }
13
13
 
14
14
  query(filter = []) {
15
- const { plot, channels, normalize, source } = this;
16
- const [x0, x1] = extentX(this, filter);
17
- const [y0, y1] = extentY(this, filter);
15
+ const { channels, normalize, source, binPad } = this;
18
16
  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);
17
+ const [x] = binExpr(this, 'x', nx, extentX(this, filter), binPad);
18
+ const [y] = binExpr(this, 'y', ny, extentY(this, filter), binPad);
25
19
 
26
20
  const q = Query
27
21
  .from(source.table)
28
22
  .where(stripXY(this, filter));
29
23
 
24
+ this.aggr = ['density'];
30
25
  const groupby = this.groupby = [];
31
26
  const z = [];
32
27
  for (const c of channels) {
@@ -132,7 +127,7 @@ function lineDensity(
132
127
  ? { w: sql`1.0 / COUNT(*) OVER (PARTITION BY ${pointPart})` }
133
128
  : null
134
129
  )
135
- .where(and(isBetween('x', [0, xn]), isBetween('y', [0, yn])));
130
+ .where(and(isBetween('x', [0, xn], true), isBetween('y', [0, yn], true)));
136
131
 
137
132
  // sum normalized, rasterized series into output grids
138
133
  return Query
@@ -140,7 +135,7 @@ function lineDensity(
140
135
  .from('points')
141
136
  .select(groupby, {
142
137
  index: sql`x + y * ${xn}::INTEGER`,
143
- value: normalize ? sum('w') : count()
138
+ density: normalize ? sum('w') : count()
144
139
  })
145
140
  .groupby('index', groupby);
146
141
  }
@@ -1,6 +1,6 @@
1
1
  import { Query, gt, isBetween, sql, sum } from '@uwdata/mosaic-sql';
2
2
  import { Transient } from '../symbols.js';
3
- import { binField, bin1d } from './util/bin-field.js';
3
+ import { binExpr } from './util/bin-expr.js';
4
4
  import { dericheConfig, dericheConv1d } from './util/density.js';
5
5
  import { extentX, extentY, xext, yext } from './util/extent.js';
6
6
  import { grid1d } from './util/grid.js';
@@ -30,14 +30,12 @@ export class Density1DMark extends Mark {
30
30
  query(filter = []) {
31
31
  if (this.hasOwnData()) throw new Error('Density1DMark requires a data source');
32
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
- );
33
+ const extent = this.extent = (dim === 'x' ? extentX : extentY)(this, filter);
34
+ const [x, bx] = binExpr(this, dim, bins, extent);
35
+ const q = markQuery(channels, table, [dim])
36
+ .where(filter.concat(isBetween(bx, extent)));
37
+ const v = this.channelField('weight') ? 'weight' : null;
38
+ return binLinear1d(q, x, v);
41
39
  }
42
40
 
43
41
  queryResult(data) {
@@ -81,8 +79,8 @@ export class Density1DMark extends Mark {
81
79
  }
82
80
  }
83
81
 
84
- function binLinear1d(q, p, value) {
85
- const w = value ? `* ${value}` : '';
82
+ function binLinear1d(q, p, density) {
83
+ const w = density ? `* ${density}` : '';
86
84
 
87
85
  const u = q.clone().select({
88
86
  p,
@@ -98,7 +96,7 @@ function binLinear1d(q, p, value) {
98
96
 
99
97
  return Query
100
98
  .from(Query.unionAll(u, v))
101
- .select({ index: 'i', value: sum('w') })
99
+ .select({ index: 'i', density: sum('w') })
102
100
  .groupby('index')
103
- .having(gt('value', 0));
101
+ .having(gt('density', 0));
104
102
  }
@@ -1,37 +1,37 @@
1
- import { handleParam } from './util/handle-param.js';
1
+ import { channelScale } from './util/channel-scale.js';
2
2
  import { Grid2DMark } from './Grid2DMark.js';
3
3
  import { channelOption } from './Mark.js';
4
4
 
5
5
  export class Density2DMark extends Grid2DMark {
6
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);
7
+ const { type = 'dot', ...channels } = options;
8
+ super(type, source, {
9
+ bandwidth: 20,
10
+ interpolate: 'linear',
11
+ pad: 0,
12
+ pixelSize: 2,
13
+ ...channels
14
+ });
12
15
  }
13
16
 
14
17
  convolve() {
15
18
  super.convolve();
16
- const { bins, binPad, extentX, extentY } = this;
19
+ const { bins, pad, extentX, extentY } = this;
17
20
  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);
21
+ const scaleX = channelScale(this, 'x');
22
+ const scaleY = channelScale(this, 'y');
23
+ const [x0, x1] = extentX.map(v => scaleX.apply(v));
24
+ const [y0, y1] = extentY.map(v => scaleY.apply(v));
25
+ const deltaX = (x1 - x0) / (nx - pad);
26
+ const deltaY = (y1 - y0) / (ny - pad);
27
+ const offset = pad ? 0 : 0.5;
28
+ this.data = points(
29
+ this.kde, bins, x0, y0, deltaX, deltaY,
30
+ scaleX.invert, scaleY.invert, offset
31
+ );
24
32
  return this;
25
33
  }
26
34
 
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
35
  plotSpecs() {
36
36
  const { type, channels, densityMap, data } = this;
37
37
  const options = {};
@@ -50,16 +50,18 @@ export class Density2DMark extends Grid2DMark {
50
50
  }
51
51
  }
52
52
 
53
- function points(kde, bins, x0, y0, deltaX, deltaY, offset) {
53
+ function points(kde, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
54
54
  const scale = 1 / (deltaX * deltaY);
55
55
  const [nx, ny] = bins;
56
56
  const data = [];
57
- for (const grid of kde) {
57
+ for (const cell of kde) {
58
+ const grid = cell.density;
58
59
  for (let k = 0, j = 0; j < ny; ++j) {
59
60
  for (let i = 0; i < nx; ++i, ++k) {
60
61
  data.push({
61
- x: x0 + (i + offset) * deltaX,
62
- y: y0 + (j + offset) * deltaY,
62
+ ...cell,
63
+ x: invertX(x0 + (i + offset) * deltaX),
64
+ y: invertY(y0 + (j + offset) * deltaY),
63
65
  density: grid[k] * scale
64
66
  });
65
67
  }
@@ -1,19 +1,26 @@
1
- import { Query, count, gt, isBetween, lt, lte, sql, sum } from '@uwdata/mosaic-sql';
1
+ import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
2
2
  import { Transient } from '../symbols.js';
3
- import { binField } from './util/bin-field.js';
3
+ import { binExpr } from './util/bin-expr.js';
4
4
  import { dericheConfig, dericheConv2d } from './util/density.js';
5
5
  import { extentX, extentY, xyext } from './util/extent.js';
6
6
  import { grid2d } from './util/grid.js';
7
7
  import { handleParam } from './util/handle-param.js';
8
+ import {
9
+ interpolateNearest, interpolatorBarycentric, interpolatorRandomWalk
10
+ } from './util/interpolate.js';
8
11
  import { Mark } from './Mark.js';
9
12
 
13
+ export const DENSITY = 'density';
14
+
10
15
  export class Grid2DMark extends Mark {
11
16
  constructor(type, source, options) {
12
17
  const {
13
- bandwidth = 20,
14
- binType = 'linear',
15
- binWidth = 2,
16
- binPad = 1,
18
+ bandwidth = 0,
19
+ interpolate = 'none',
20
+ pixelSize = 1,
21
+ pad = 1,
22
+ width,
23
+ height,
17
24
  ...channels
18
25
  } = options;
19
26
 
@@ -24,9 +31,11 @@ export class Grid2DMark extends Mark {
24
31
  handleParam(this, 'bandwidth', bandwidth, () => {
25
32
  return this.grids ? this.convolve().update() : null;
26
33
  });
27
- handleParam(this, 'binWidth', binWidth);
28
- handleParam(this, 'binType', binType);
29
- handleParam(this, 'binPad', binPad);
34
+ handleParam(this, 'pixelSize', pixelSize);
35
+ handleParam(this, 'interpolate', interpolate);
36
+ handleParam(this, 'pad', pad);
37
+ handleParam(this, 'width', width);
38
+ handleParam(this, 'height', height);
30
39
  }
31
40
 
32
41
  setPlot(plot, index) {
@@ -43,82 +52,115 @@ export class Grid2DMark extends Mark {
43
52
  }
44
53
 
45
54
  query(filter = []) {
46
- const { plot, binType, binPad, channels, densityMap, source } = this;
55
+ const { interpolate, pad, channels, densityMap, source } = this;
47
56
  const [x0, x1] = this.extentX = extentX(this, filter);
48
57
  const [y0, y1] = this.extentY = extentY(this, filter);
49
58
  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);
59
+ const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
60
+ const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
56
61
 
57
62
  // with padded bins, include the entire domain extent
58
63
  // 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)];
64
+ const bounds = pad
65
+ ? [isBetween(bx, [+x0, +x1]), isBetween(by, [+y0, +y1])]
66
+ : [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
62
67
 
63
68
  const q = Query
64
69
  .from(source.table)
65
70
  .where(filter.concat(bounds));
66
71
 
67
72
  const groupby = this.groupby = [];
68
- let agg = count();
73
+ const aggrMap = {};
69
74
  for (const c of channels) {
70
75
  if (Object.hasOwn(c, 'field')) {
71
76
  const { as, channel, field } = c;
72
77
  if (field.aggregate) {
73
- agg = field;
78
+ // include custom aggregate
79
+ aggrMap[channel] = field;
74
80
  densityMap[channel] = true;
75
81
  } else if (channel === 'weight') {
76
- agg = sum(field);
82
+ // compute weighted density
83
+ aggrMap[DENSITY] = sum(field);
77
84
  } else if (channel !== 'x' && channel !== 'y') {
85
+ // add groupby field
78
86
  q.select({ [as]: field });
79
87
  groupby.push(as);
80
88
  }
81
89
  }
82
90
  }
91
+ const aggr = this.aggr = Object.keys(aggrMap);
92
+
93
+ // check for incompatible encodings
94
+ if (aggrMap.density && aggr.length > 1) {
95
+ throw new Error('Weight option can not be used with custom aggregates.');
96
+ }
97
+
98
+ // if no aggregates, default to count density
99
+ if (!aggr.length) {
100
+ aggr.push(DENSITY);
101
+ aggrMap.density = count();
102
+ }
83
103
 
84
- return binType === 'linear'
85
- ? binLinear2d(q, x, y, agg, nx, groupby)
86
- : bin2d(q, x, y, agg, nx, groupby);
104
+ // generate grid binning query
105
+ if (interpolate === 'linear') {
106
+ if (aggr.length > 1) {
107
+ throw new Error('Linear binning not applicable to multiple aggregates.');
108
+ }
109
+ if (!aggrMap.density) {
110
+ throw new Error('Linear binning not applicable to custom aggregates.');
111
+ }
112
+ return binLinear2d(q, x, y, aggrMap[DENSITY], nx, groupby);
113
+ } else {
114
+ return bin2d(q, x, y, aggrMap, nx, groupby);
115
+ }
87
116
  }
88
117
 
89
118
  binDimensions() {
90
- const { plot, binWidth } = this;
119
+ const { plot, pixelSize, width, height } = this;
91
120
  return [
92
- Math.round(plot.innerWidth() / binWidth),
93
- Math.round(plot.innerHeight() / binWidth)
121
+ width ?? Math.round(plot.innerWidth() / pixelSize),
122
+ height ?? Math.round(plot.innerHeight() / pixelSize)
94
123
  ];
95
124
  }
96
125
 
97
126
  queryResult(data) {
98
- const [nx, ny] = this.bins;
99
- this.grids = grid2d(nx, ny, data, this.groupby);
127
+ const [w, h] = this.bins;
128
+ const interp = maybeInterpolate(this.interpolate);
129
+ this.grids = grid2d(w, h, data, this.aggr, this.groupby, interp);
100
130
  return this.convolve();
101
131
  }
102
132
 
103
133
  convolve() {
104
- const { bandwidth, bins, grids, plot } = this;
134
+ const { aggr, bandwidth, bins, grids, plot } = this;
105
135
 
106
- if (bandwidth <= 0) {
107
- this.kde = this.grids.map(({ key, grid }) => {
108
- return (grid.key = key, grid);
109
- });
110
- } else {
136
+ // no smoothing as default fallback
137
+ this.kde = this.grids;
138
+
139
+ if (bandwidth > 0) {
140
+ // determine which grid to smooth
141
+ const gridProp = aggr.length === 1 ? aggr[0]
142
+ : aggr.includes(DENSITY) ? DENSITY
143
+ : null;
144
+
145
+ // bail if no compatible grid found
146
+ if (!gridProp) {
147
+ console.warn('No compatible grid found for smoothing.');
148
+ return this;
149
+ }
150
+
151
+ // apply smoothing, bandwidth uses units of screen pixels
111
152
  const w = plot.innerWidth();
112
153
  const h = plot.innerHeight();
113
154
  const [nx, ny] = bins;
114
- const neg = grids.some(({ grid }) => grid.some(v => v < 0));
155
+ const neg = grids.some(cell => cell[gridProp].some(v => v < 0));
115
156
  const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
116
157
  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);
158
+ this.kde = this.grids.map(grid => {
159
+ const density = dericheConv2d(configX, configY, grid[gridProp], bins);
160
+ return { ...grid, [gridProp]: density };
120
161
  });
121
162
  }
163
+
122
164
  return this;
123
165
  }
124
166
 
@@ -127,6 +169,9 @@ export class Grid2DMark extends Mark {
127
169
  }
128
170
  }
129
171
 
172
+ /**
173
+ * Extract channels that explicitly encode computed densities.
174
+ */
130
175
  function createDensityMap(channels) {
131
176
  const densityMap = {};
132
177
  for (const key in channels) {
@@ -138,25 +183,33 @@ function createDensityMap(channels) {
138
183
  return densityMap;
139
184
  }
140
185
 
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}`;
186
+ function maybeInterpolate(interpolate = 'none') {
187
+ if (typeof interpolate === 'function') return interpolate;
188
+ switch (`${interpolate}`.toLowerCase()) {
189
+ case 'none':
190
+ case 'linear':
191
+ return undefined; // no special interpolation need
192
+ case 'nearest':
193
+ return interpolateNearest;
194
+ case 'barycentric':
195
+ return interpolatorBarycentric();
196
+ case 'random-walk':
197
+ return interpolatorRandomWalk();
198
+ }
199
+ throw new Error(`invalid interpolate: ${interpolate}`);
147
200
  }
148
201
 
149
- function bin2d(q, xp, yp, value, xn, groupby) {
202
+ function bin2d(q, xp, yp, aggs, xn, groupby) {
150
203
  return q
151
204
  .select({
152
205
  index: sql`FLOOR(${xp})::INTEGER + FLOOR(${yp})::INTEGER * ${xn}`,
153
- value
206
+ ...aggs
154
207
  })
155
208
  .groupby('index', groupby);
156
209
  }
157
210
 
158
- function binLinear2d(q, xp, yp, value, xn, groupby) {
159
- const w = value.column ? `* ${value.column}` : '';
211
+ function binLinear2d(q, xp, yp, density, xn, groupby) {
212
+ const w = density?.column ? `* ${density.column}` : '';
160
213
  const subq = (i, w) => q.clone().select({ xp, yp, i, w });
161
214
 
162
215
  // grid[xu + yu * xn] += (xv - xp) * (yv - yp) * wi;
@@ -185,7 +238,7 @@ function binLinear2d(q, xp, yp, value, xn, groupby) {
185
238
 
186
239
  return Query
187
240
  .from(Query.unionAll(a, b, c, d))
188
- .select({ index: 'i', value: sum('w') }, groupby)
241
+ .select({ index: 'i', density: sum('w') }, groupby)
189
242
  .groupby('index', groupby)
190
- .having(gt('value', 0));
243
+ .having(neq('density', 0));
191
244
  }