@uwdata/mosaic-plot 0.7.1 → 0.8.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.
@@ -26,46 +26,61 @@ export class Density2DMark extends Grid2DMark {
26
26
  const deltaY = (y1 - y0) / (ny - pad);
27
27
  const offset = pad ? 0 : 0.5;
28
28
  this.data = points(
29
- this.kde, bins, x0, y0, deltaX, deltaY,
29
+ this.grids, bins, x0, y0, deltaX, deltaY,
30
30
  scaleX.invert, scaleY.invert, offset
31
31
  );
32
32
  return this;
33
33
  }
34
34
 
35
35
  plotSpecs() {
36
- const { type, channels, densityMap, data } = this;
36
+ const { type, channels, densityMap, data: { numRows: length, columns } } = this;
37
37
  const options = {};
38
38
  for (const c of channels) {
39
39
  const { channel } = c;
40
40
  options[channel] = (channel === 'x' || channel === 'y')
41
- ? channel // use generated x/y data fields
42
- : channelOption(c);
41
+ ? columns[channel] // use generated x/y data fields
42
+ : channelOption(c, columns);
43
43
  }
44
44
  for (const channel in densityMap) {
45
45
  if (densityMap[channel]) {
46
- options[channel] = 'density';
46
+ options[channel] = columns.density;
47
47
  }
48
48
  }
49
- return [{ type, data, options }];
49
+ return [{ type, data: { length }, options }];
50
50
  }
51
51
  }
52
52
 
53
- function points(kde, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
53
+ function points(data, bins, x0, y0, deltaX, deltaY, invertX, invertY, offset) {
54
54
  const scale = 1 / (deltaX * deltaY);
55
55
  const [nx, ny] = bins;
56
- const data = [];
57
- for (const cell of kde) {
58
- const grid = cell.density;
56
+ const batch = nx * ny;
57
+ const numRows = batch * data.numRows;
58
+
59
+ const x = new Float64Array(numRows);
60
+ const y = new Float64Array(numRows);
61
+ const density = new Float64Array(numRows);
62
+ const columns = { x, y, density };
63
+ const { density: grids, ...rest } = data.columns;
64
+ for (const name in rest) {
65
+ columns[name] = new rest[name].constructor(numRows);
66
+ }
67
+
68
+ let r = 0;
69
+ for (let row = 0; row < data.numRows; ++row) {
70
+ // copy repeated values in batch
71
+ for (const name in rest) {
72
+ columns[name].fill(rest[name][row], r, r + batch);
73
+ }
74
+ // copy individual grid values
75
+ const grid = grids[row];
59
76
  for (let k = 0, j = 0; j < ny; ++j) {
60
- for (let i = 0; i < nx; ++i, ++k) {
61
- data.push({
62
- ...cell,
63
- x: invertX(x0 + (i + offset) * deltaX),
64
- y: invertY(y0 + (j + offset) * deltaY),
65
- density: grid[k] * scale
66
- });
77
+ for (let i = 0; i < nx; ++i, ++r, ++k) {
78
+ x[r] = invertX(x0 + (i + offset) * deltaX);
79
+ y[r] = invertY(y0 + (j + offset) * deltaY);
80
+ density[r] = grid[k] * scale;
67
81
  }
68
82
  }
69
83
  }
70
- return data;
84
+
85
+ return { numRows, columns };
71
86
  }
@@ -18,16 +18,15 @@ export class GeoMark extends Mark {
18
18
  }
19
19
 
20
20
  queryResult(data) {
21
- super.queryResult(data);
21
+ super.queryResult(data); // map to columns, set this.data
22
22
 
23
- // parse GeoJSON strings to JSON objects
23
+ // look for an explicit geometry field
24
24
  const geom = this.channelField('geometry')?.as;
25
- if (geom && this.data) {
26
- this.data.forEach(data => {
27
- if (typeof data[geom] === 'string') {
28
- data[geom] = JSON.parse(data[geom]);
29
- }
30
- });
25
+ if (geom) {
26
+ const { columns } = this.data;
27
+ if (typeof columns[geom][0] === 'string') {
28
+ columns[geom] = columns[geom].map(s => JSON.parse(s));
29
+ }
31
30
  }
32
31
 
33
32
  return this;
@@ -1,3 +1,4 @@
1
+ import { interpolatorBarycentric, interpolateNearest, interpolatorRandomWalk } from '@observablehq/plot';
1
2
  import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
2
3
  import { Transient } from '../symbols.js';
3
4
  import { binExpr } from './util/bin-expr.js';
@@ -5,9 +6,7 @@ import { dericheConfig, dericheConv2d } from './util/density.js';
5
6
  import { extentX, extentY, xyext } from './util/extent.js';
6
7
  import { grid2d } from './util/grid.js';
7
8
  import { handleParam } from './util/handle-param.js';
8
- import {
9
- interpolateNearest, interpolatorBarycentric, interpolatorRandomWalk
10
- } from './util/interpolate.js';
9
+ import { toDataColumns } from './util/to-data-columns.js';
11
10
  import { Mark } from './Mark.js';
12
11
 
13
12
  export const DENSITY = 'density';
@@ -28,21 +27,47 @@ export class Grid2DMark extends Mark {
28
27
  super(type, source, channels, xyext);
29
28
  this.densityMap = densityMap;
30
29
 
31
- handleParam(this, 'bandwidth', bandwidth, () => {
30
+ /** @type {number} */
31
+ this.bandwidth = handleParam(bandwidth, value => {
32
+ this.bandwidth = value;
32
33
  return this.grids ? this.convolve().update() : null;
33
34
  });
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);
35
+
36
+ /** @type {string} */
37
+ this.interpolate = handleParam(interpolate, value => {
38
+ return (this.interpolate = value, this.requestUpdate());
39
+ });
40
+
41
+ /** @type {number} */
42
+ this.pixelSize = handleParam(pixelSize, value => {
43
+ return (this.pixelSize = value, this.requestUpdate());
44
+ });
45
+
46
+ /** @type {number} */
47
+ this.pad = handleParam(pad, value => {
48
+ return (this.pad = value, this.requestUpdate());
49
+ });
50
+
51
+ /** @type {number|undefined} */
52
+ this.width = handleParam(width, value => {
53
+ return (this.width = value, this.requestUpdate());
54
+ });
55
+
56
+ /** @type {number|undefined} */
57
+ this.height = handleParam(height, value => {
58
+ return (this.height = value, this.requestUpdate());
59
+ });
39
60
  }
40
61
 
62
+ /**
63
+ * @param {import('../plot.js').Plot} plot The plot.
64
+ * @param {number} index
65
+ */
41
66
  setPlot(plot, index) {
42
67
  const update = () => { if (this.hasFieldInfo()) this.requestUpdate(); };
43
- plot.addAttributeListener('domainX', update);
44
- plot.addAttributeListener('domainY', update);
45
- return super.setPlot(plot, index);
68
+ plot.addAttributeListener('xDomain', update);
69
+ plot.addAttributeListener('yDomain', update);
70
+ super.setPlot(plot, index);
46
71
  }
47
72
 
48
73
  get filterIndexable() {
@@ -55,7 +80,7 @@ export class Grid2DMark extends Mark {
55
80
  const { interpolate, pad, channels, densityMap, source } = this;
56
81
  const [x0, x1] = this.extentX = extentX(this, filter);
57
82
  const [y0, y1] = this.extentY = extentY(this, filter);
58
- const [nx, ny] = this.bins = this.binDimensions(this);
83
+ const [nx, ny] = this.bins = this.binDimensions();
59
84
  const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
60
85
  const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
61
86
 
@@ -115,6 +140,9 @@ export class Grid2DMark extends Mark {
115
140
  }
116
141
  }
117
142
 
143
+ /**
144
+ * @returns {[number, number]} The bin dimensions.
145
+ */
118
146
  binDimensions() {
119
147
  const { plot, pixelSize, width, height } = this;
120
148
  return [
@@ -126,47 +154,49 @@ export class Grid2DMark extends Mark {
126
154
  queryResult(data) {
127
155
  const [w, h] = this.bins;
128
156
  const interp = maybeInterpolate(this.interpolate);
129
- this.grids = grid2d(w, h, data, this.aggr, this.groupby, interp);
157
+ const { columns } = toDataColumns(data);
158
+ this.grids0 = grid2d(w, h, columns.index, columns, this.aggr, this.groupby, interp);
130
159
  return this.convolve();
131
160
  }
132
161
 
133
162
  convolve() {
134
- const { aggr, bandwidth, bins, grids, plot } = this;
163
+ const { aggr, bandwidth, bins, grids0, plot } = this;
135
164
 
136
165
  // no smoothing as default fallback
137
- this.kde = this.grids;
166
+ this.grids = grids0;
138
167
 
139
168
  if (bandwidth > 0) {
140
169
  // determine which grid to smooth
141
- const gridProp = aggr.length === 1 ? aggr[0]
170
+ const prop = aggr.length === 1 ? aggr[0]
142
171
  : aggr.includes(DENSITY) ? DENSITY
143
172
  : null;
144
173
 
145
174
  // bail if no compatible grid found
146
- if (!gridProp) {
175
+ if (!prop) {
147
176
  console.warn('No compatible grid found for smoothing.');
148
177
  return this;
149
178
  }
179
+ const g = grids0.columns[prop];
150
180
 
151
181
  // apply smoothing, bandwidth uses units of screen pixels
152
182
  const w = plot.innerWidth();
153
183
  const h = plot.innerHeight();
154
184
  const [nx, ny] = bins;
155
- const neg = grids.some(cell => cell[gridProp].some(v => v < 0));
185
+ const neg = g.some(grid => grid.some(v => v < 0));
156
186
  const configX = dericheConfig(bandwidth * (nx - 1) / w, neg);
157
187
  const configY = dericheConfig(bandwidth * (ny - 1) / h, neg);
158
- this.kde = this.grids.map(grid => {
159
- const density = dericheConv2d(configX, configY, grid[gridProp], bins);
160
- return { ...grid, [gridProp]: density };
161
- });
188
+ this.grids = {
189
+ numRows: grids0.numRows,
190
+ columns: {
191
+ ...grids0.columns,
192
+ // @ts-ignore
193
+ [prop]: g.map(grid => dericheConv2d(configX, configY, grid, bins))
194
+ }
195
+ };
162
196
  }
163
197
 
164
198
  return this;
165
199
  }
166
-
167
- plotSpecs() {
168
- throw new Error('Unimplemented. Use a Grid2D mark subclass.');
169
- }
170
200
  }
171
201
 
172
202
  /**
@@ -185,7 +215,7 @@ function createDensityMap(channels) {
185
215
 
186
216
  function maybeInterpolate(interpolate = 'none') {
187
217
  if (typeof interpolate === 'function') return interpolate;
188
- switch (`${interpolate}`.toLowerCase()) {
218
+ switch (interpolate.toLowerCase()) {
189
219
  case 'none':
190
220
  case 'linear':
191
221
  return undefined; // no special interpolation need
@@ -2,12 +2,17 @@ import { Query, isNotNull, sql } from '@uwdata/mosaic-sql';
2
2
  import { Transient } from '../symbols.js';
3
3
  import { extentX, extentY, xyext } from './util/extent.js';
4
4
  import { Mark } from './Mark.js';
5
+ import { handleParam } from './util/handle-param.js';
5
6
 
6
7
  export class HexbinMark extends Mark {
7
8
  constructor(source, options) {
8
9
  const { type = 'hexagon', binWidth = 20, ...channels } = options;
9
10
  super(type, source, { r: binWidth / 2, clip: true, ...channels }, xyext);
10
- this.binWidth = binWidth;
11
+
12
+ /** @type {number} */
13
+ this.binWidth = handleParam(binWidth, value => {
14
+ return (this.binWidth = value, this.requestUpdate());
15
+ });
11
16
  }
12
17
 
13
18
  get filterIndexable() {
@@ -39,9 +44,10 @@ export class HexbinMark extends Mark {
39
44
  let x, y;
40
45
  const aggr = new Set;
41
46
  const cols = {};
47
+ let orderby;
42
48
  for (const c of channels) {
43
49
  if (c.channel === 'orderby') {
44
- q.orderby(c.value); // TODO revisit once groupby is added
50
+ orderby = c.value; // TODO revisit once groupby is added
45
51
  } else if (c.channel === 'x') {
46
52
  x = c;
47
53
  } else if (c.channel === 'y') {
@@ -63,6 +69,8 @@ export class HexbinMark extends Mark {
63
69
  ...cols
64
70
  }).groupby('x', 'y');
65
71
 
72
+ if (orderby) q.orderby(orderby);
73
+
66
74
  // Map x/y channels to screen space
67
75
  const xx = `${xr} * (${x.field} - ${x1}::DOUBLE)`;
68
76
  const yy = `${yr} * (${y2}::DOUBLE - ${y.field})`;
package/src/marks/Mark.js CHANGED
@@ -3,10 +3,11 @@ import { Query, Ref, column, isParamLike } from '@uwdata/mosaic-sql';
3
3
  import { isColor } from './util/is-color.js';
4
4
  import { isConstantOption } from './util/is-constant-option.js';
5
5
  import { isSymbol } from './util/is-symbol.js';
6
- import { toDataArray } from './util/to-data-array.js';
6
+ import { toDataColumns } from './util/to-data-columns.js';
7
7
  import { Transform } from '../symbols.js';
8
8
 
9
9
  const isColorChannel = channel => channel === 'stroke' || channel === 'fill';
10
+ const isOpacityChannel = channel => /opacity$/i.test(channel);
10
11
  const isSymbolChannel = channel => channel === 'symbol';
11
12
  const isFieldObject = (channel, field) => {
12
13
  return channel !== 'sort' && channel !== 'tip'
@@ -31,7 +32,7 @@ export class Mark extends MosaicClient {
31
32
 
32
33
  this.source = source;
33
34
  if (isDataArray(this.source)) {
34
- this.data = this.source;
35
+ this.data = toDataColumns(this.source);
35
36
  }
36
37
 
37
38
  const channels = this.channels = [];
@@ -63,12 +64,15 @@ export class Mark extends MosaicClient {
63
64
  }
64
65
  } else if (isParamLike(entry)) {
65
66
  if (Array.isArray(entry.columns)) {
67
+ // we currently duck-type to having a columns array
68
+ // as a check that this is SQLExpression-compatible
66
69
  channels.push(fieldEntry(channel, entry));
67
70
  params.add(entry);
68
71
  } else {
69
72
  const c = valueEntry(channel, entry.value);
70
73
  channels.push(c);
71
74
  entry.addEventListener('value', value => {
75
+ // update immediately, the value is simply passed to Plot
72
76
  c.value = value;
73
77
  return this.update();
74
78
  });
@@ -85,6 +89,10 @@ export class Mark extends MosaicClient {
85
89
  }
86
90
  }
87
91
 
92
+ /**
93
+ * @param {import('../plot.js').Plot} plot The plot.
94
+ * @param {number} index
95
+ */
88
96
  setPlot(plot, index) {
89
97
  this.plot = plot;
90
98
  this.index = index;
@@ -104,7 +112,7 @@ export class Mark extends MosaicClient {
104
112
  return this.channels.find(c => c.channel === channel);
105
113
  }
106
114
 
107
- channelField(channel, { exact } = {}) {
115
+ channelField(channel, { exact = false } = {}) {
108
116
  const c = exact
109
117
  ? this.channel(channel)
110
118
  : this.channels.find(c => c.channel.startsWith(channel));
@@ -140,6 +148,11 @@ export class Mark extends MosaicClient {
140
148
  return this;
141
149
  }
142
150
 
151
+ /**
152
+ * Return a query specifying the data needed by this Mark client.
153
+ * @param {*} [filter] The filtering criteria to apply in the query.
154
+ * @returns {*} The client query
155
+ */
143
156
  query(filter = []) {
144
157
  if (this.hasOwnData()) return null;
145
158
  const { channels, source: { table } } = this;
@@ -151,8 +164,11 @@ export class Mark extends MosaicClient {
151
164
  return this;
152
165
  }
153
166
 
167
+ /**
168
+ * Provide query result data to the mark.
169
+ */
154
170
  queryResult(data) {
155
- this.data = toDataArray(data);
171
+ this.data = toDataColumns(data);
156
172
  return this;
157
173
  }
158
174
 
@@ -160,16 +176,29 @@ export class Mark extends MosaicClient {
160
176
  return this.plot.update(this);
161
177
  }
162
178
 
179
+ /**
180
+ * Generate an array of Plot mark specifications.
181
+ * @returns {object[]}
182
+ */
163
183
  plotSpecs() {
164
- const { type, data, detail, channels } = this;
184
+ const { type, detail, channels } = this;
185
+ // @ts-ignore
186
+ const { numRows: length, values, columns } = this.data || {};
187
+
188
+ // populate plot specification options
165
189
  const options = {};
166
190
  const side = {};
167
191
  for (const c of channels) {
168
192
  const obj = detail.has(c.channel) ? side : options;
169
- obj[c.channel] = channelOption(c)
193
+ obj[c.channel] = channelOption(c, columns);
170
194
  }
171
195
  if (detail.size) options.channels = side;
172
- return [{ type, data, options }];
196
+
197
+ // if provided raw source values (not objects) pass as-is
198
+ // otherwise we pass columnar data directy in the options
199
+ const data = values ?? (this.data ? { length } : null);
200
+ const spec = [{ type, data, options }];
201
+ return spec;
173
202
  }
174
203
  }
175
204
 
@@ -178,14 +207,17 @@ export class Mark extends MosaicClient {
178
207
  * Checks if a constant value or a data field is needed.
179
208
  * Also avoids misinterpretation of data values as color names.
180
209
  * @param {*} c a visual encoding channel spec
210
+ * @param {object} columns named data column arrays
181
211
  * @returns the Plot channel option
182
212
  */
183
- export function channelOption(c) {
213
+ export function channelOption(c, columns) {
184
214
  // use a scale override for color channels to sidestep
185
215
  // https://github.com/observablehq/plot/issues/1593
216
+ const value = columns?.[c.as] ?? c.as;
186
217
  return Object.hasOwn(c, 'value') ? c.value
187
- : isColorChannel(c.channel) ? { value: c.as, scale: 'color' }
188
- : c.as;
218
+ : isColorChannel(c.channel) ? { value, scale: 'color' }
219
+ : isOpacityChannel(c.channel) ? { value, scale: 'opacity' }
220
+ : value;
189
221
  }
190
222
 
191
223
  /**
@@ -196,7 +228,7 @@ export function channelOption(c) {
196
228
  * @param {*} table the table to query.
197
229
  * @param {*} skip an optional array of channels to skip.
198
230
  * Mark subclasses can skip channels that require special handling.
199
- * @returns a Query instance
231
+ * @returns {Query} a Query instance
200
232
  */
201
233
  export function markQuery(channels, table, skip = []) {
202
234
  const q = Query.from({ source: table });
@@ -20,6 +20,7 @@ import { Fixed, Transient } from '../symbols.js';
20
20
  export class RasterMark extends Grid2DMark {
21
21
  constructor(source, options) {
22
22
  super('image', source, options);
23
+ this.image = null;
23
24
  }
24
25
 
25
26
  setPlot(plot, index) {
@@ -33,37 +34,45 @@ export class RasterMark extends Grid2DMark {
33
34
  }
34
35
 
35
36
  rasterize() {
36
- const { bins, kde } = this;
37
+ const { bins, grids } = this;
37
38
  const [ w, h ] = bins;
39
+ const { numRows, columns } = grids;
38
40
 
39
41
  // raster data
40
42
  const { canvas, ctx, img } = imageData(this, w, h);
41
43
 
42
44
  // color + opacity encodings
43
45
  const { alpha, alphaProp, color, colorProp } = rasterEncoding(this);
46
+ const alphaData = columns[alphaProp] ?? [];
47
+ const colorData = columns[colorProp] ?? [];
44
48
 
45
49
  // generate rasters
46
- this.data = kde.map(cell => {
47
- color?.(img.data, w, h, cell[colorProp]);
48
- alpha?.(img.data, w, h, cell[alphaProp]);
49
- ctx.putImageData(img, 0, 0);
50
- return { src: canvas.toDataURL() };
51
- });
50
+ this.data = {
51
+ numRows,
52
+ columns: {
53
+ src: Array.from({ length: numRows }, (_, i) => {
54
+ color?.(img.data, w, h, colorData[i]);
55
+ alpha?.(img.data, w, h, alphaData[i]);
56
+ ctx.putImageData(img, 0, 0);
57
+ return canvas.toDataURL();
58
+ })
59
+ }
60
+ };
52
61
 
53
62
  return this;
54
63
  }
55
64
 
56
65
  plotSpecs() {
57
- const { type, plot, data } = this;
66
+ const { type, plot, data: { numRows: length, columns } } = this;
58
67
  const options = {
59
- src: 'src',
68
+ src: columns.src,
60
69
  width: plot.innerWidth(),
61
70
  height: plot.innerHeight(),
62
71
  preserveAspectRatio: 'none',
63
72
  imageRendering: this.channel('imageRendering')?.value,
64
73
  frameAnchor: 'middle'
65
74
  };
66
- return [{ type, data, options }];
75
+ return [{ type, data: { length }, options }];
67
76
  }
68
77
  }
69
78
 
@@ -86,6 +95,7 @@ export class HeatmapMark extends RasterMark {
86
95
  /**
87
96
  * Utility method to generate color and alpha encoding helpers.
88
97
  * The returned methods can write directly to a pixel raster.
98
+ * @param {RasterMark} mark
89
99
  */
90
100
  export function rasterEncoding(mark) {
91
101
  const { aggr, densityMap, groupby, plot } = mark;
@@ -132,18 +142,24 @@ export function rasterEncoding(mark) {
132
142
  return { alphaProp, colorProp, alpha, color };
133
143
  }
134
144
 
145
+ /**
146
+ * Generate an opacity rasterizer for a bitmap alpha channel.
147
+ * @param {RasterMark} mark The mark instance
148
+ * @param {string} prop The data property name
149
+ * @returns A bitmap rasterizer function.
150
+ */
135
151
  function alphaScale(mark, prop) {
136
- const { plot, kde: grids } = mark;
152
+ const { plot, grids } = mark;
137
153
 
138
154
  // determine scale domain
139
155
  const domainAttr = plot.getAttribute('opacityDomain');
140
156
  const domainFixed = domainAttr === Fixed;
141
157
  const domainTransient = domainAttr?.[Transient];
142
158
  const domain = (!domainFixed && !domainTransient && domainAttr)
143
- || gridDomainContinuous(grids, prop);
144
- if (domainFixed || domainTransient) {
145
- if (domainTransient) domain[Transient] = true;
146
- plot.setAttribute('colorDomain', domain);
159
+ || gridDomainContinuous(grids.columns[prop]);
160
+ if (domainFixed || domainTransient || !domainAttr) {
161
+ if (!domainFixed) domain[Transient] = true;
162
+ plot.setAttribute('opacityDomain', domain);
147
163
  }
148
164
 
149
165
  // generate opacity scale
@@ -163,22 +179,29 @@ function alphaScale(mark, prop) {
163
179
  return alphaScheme(s);
164
180
  }
165
181
 
182
+ /**
183
+ * Generate an color rasterizer for bitmap r, g, b channels.
184
+ * @param {RasterMark} mark The mark instance
185
+ * @param {string} prop
186
+ * @returns A bitmap rasterizer function.
187
+ */
166
188
  function colorScale(mark, prop) {
167
- const { plot, kde: grids } = mark;
168
- const flat = !grids[0][prop]?.map; // not array-like
169
- const discrete = flat || Array.isArray(grids[0][prop]);
189
+ const { plot, grids } = mark;
190
+ const data = grids.columns[prop];
191
+ const flat = !data[0]?.map; // not array-like
192
+ const discrete = flat || Array.isArray(data[0]);
170
193
 
171
194
  // determine scale domain
172
195
  const domainAttr = plot.getAttribute('colorDomain');
173
196
  const domainFixed = domainAttr === Fixed;
174
197
  const domainTransient = domainAttr?.[Transient];
175
198
  const domain = (!domainFixed && !domainTransient && domainAttr) || (
176
- flat ? grids.map(cell => cell[prop]).sort(ascending)
177
- : discrete ? gridDomainDiscrete(grids, prop)
178
- : gridDomainContinuous(grids, prop)
199
+ flat ? data.sort(ascending)
200
+ : discrete ? gridDomainDiscrete(data)
201
+ : gridDomainContinuous(data)
179
202
  );
180
- if (domainFixed || domainTransient) {
181
- if (domainTransient) domain[Transient] = true;
203
+ if (domainFixed || domainTransient || !domainAttr) {
204
+ if (!domainFixed) domain[Transient] = true;
182
205
  plot.setAttribute('colorDomain', domain);
183
206
  }
184
207
 
@@ -234,6 +257,15 @@ function inferScaleType(type) {
234
257
  return type;
235
258
  }
236
259
 
260
+ /**
261
+ * Retrieve canvas image data for a 2D raster bitmap.
262
+ * The resulting data is cached in the mark.image property.
263
+ * If the canvas dimensions change, a new canvas is created.
264
+ * @param {RasterMark} mark The mark instance
265
+ * @param {number} w The canvas width.
266
+ * @param {number} h The canvas height.
267
+ * @returns An object with a canvas, context, image data, and dimensions.
268
+ */
237
269
  export function imageData(mark, w, h) {
238
270
  if (!mark.image || mark.image.w !== w || mark.image.h !== h) {
239
271
  const canvas = createCanvas(w, h);