@uwdata/mosaic-plot 0.5.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.
@@ -1,57 +1,128 @@
1
- import { isArrowTable } from './is-arrow-table.js';
1
+ import { InternSet, ascending } from 'd3';
2
+ import { DECIMAL, FLOAT, INTEGER, isArrowTable } from './arrow.js';
3
+
4
+ function arrayType(values, name = 'density') {
5
+ if (isArrowTable(values)) {
6
+ const type = values.getChild(name).type;
7
+ switch (type.typeId) {
8
+ case INTEGER:
9
+ case FLOAT:
10
+ case DECIMAL:
11
+ return Float64Array;
12
+ default:
13
+ return Array;
14
+ }
15
+ } else {
16
+ return typeof values[0][name] === 'number' ? Float64Array : Array;
17
+ }
18
+ }
2
19
 
3
20
  export function grid1d(n, values) {
4
- return valuesToGrid(new Float64Array(n), values);
21
+ const Type = arrayType(values);
22
+ return valuesToGrid(new Type(n), values);
5
23
  }
6
24
 
7
- export function grid2d(m, n, values, groupby = []) {
8
- return groupby.length
9
- ? Object.values(groupedValuesToGrids(m * n, values, groupby))
10
- : [{ grid: valuesToGrid(new Float64Array(m * n), values) }];
25
+ export function grid2d(m, n, values, aggr, groupby = []) {
26
+ if (groupby.length) {
27
+ // generate grids per group
28
+ return groupedValuesToGrids(m * n, values, aggr, groupby);
29
+ } else {
30
+ const cell = {};
31
+ aggr.forEach(name => {
32
+ const Type = arrayType(values, name);
33
+ cell[name] = valuesToGrid(new Type(m * n), values, name);
34
+ });
35
+ return [cell];
36
+ }
11
37
  }
12
38
 
13
- function valuesToGrid(grid, values) {
39
+ function valuesToGrid(grid, values, name = 'density') {
14
40
  if (isArrowTable(values)) {
15
41
  // optimize access for Arrow tables
16
42
  const numRows = values.numRows;
17
43
  if (numRows === 0) return grid;
18
44
  const index = values.getChild('index').toArray();
19
- const value = values.getChild('value').toArray();
45
+ const value = values.getChild(name).toArray();
20
46
  for (let row = 0; row < numRows; ++row) {
21
47
  grid[index[row]] = value[row];
22
48
  }
23
49
  } else {
24
50
  // fallback to iterable data
25
51
  for (const row of values) {
26
- grid[row.index] = row.value;
52
+ grid[row.index] = row[name];
27
53
  }
28
54
  }
29
55
  return grid;
30
56
  }
31
57
 
32
- function groupedValuesToGrids(size, values, groupby) {
33
- const grids = {};
34
- const getGrid = key => {
35
- const cell = grids[key] || (grids[key] = { key, grid: new Float64Array(size) });
36
- return cell.grid;
58
+ function groupedValuesToGrids(size, values, aggr, groupby) {
59
+ const Types = aggr.map(name => arrayType(values, name));
60
+ const numAggr = aggr.length;
61
+
62
+ const cellMap = {};
63
+ const getCell = key => {
64
+ let cell = cellMap[key];
65
+ if (!cell) {
66
+ cell = cellMap[key] = {};
67
+ groupby.forEach((name, i) => cell[name] = key[i]);
68
+ aggr.forEach((name, i) => cell[name] = new Types[i](size));
69
+ }
70
+ return cell;
37
71
  };
72
+
38
73
  if (isArrowTable(values)) {
39
74
  // optimize access for Arrow tables
40
75
  const numRows = values.numRows;
41
- if (numRows === 0) return grids;
76
+ if (numRows === 0) return [];
77
+
42
78
  const index = values.getChild('index').toArray();
43
- const value = values.getChild('value').toArray();
79
+ const value = aggr.map(name => values.getChild(name).toArray());
44
80
  const groups = groupby.map(name => values.getChild(name));
81
+
45
82
  for (let row = 0; row < numRows; ++row) {
46
83
  const key = groups.map(vec => vec.get(row));
47
- getGrid(key)[index[row]] = value[row];
84
+ const cell = getCell(key);
85
+ for (let i = 0; i < numAggr; ++i) {
86
+ cell[aggr[i]][index[row]] = value[i][row];
87
+ }
48
88
  }
49
89
  } else {
50
90
  // fallback to iterable data
51
91
  for (const row of values) {
52
92
  const key = groupby.map(col => row[col]);
53
- getGrid(key)[row.index] = row.value;
93
+ const cell = getCell(key);
94
+ for (let i = 0; i < numAggr; ++i) {
95
+ cell[aggr[i]][row.index] = row[aggr[i]];
96
+ }
54
97
  }
55
98
  }
56
- return grids;
99
+
100
+ return Object.values(cellMap);
101
+ }
102
+
103
+ export function gridDomainContinuous(grids, prop) {
104
+ let lo = 0, hi = 0;
105
+ grids.forEach(cell => {
106
+ const grid = cell[prop];
107
+ const n = grid.length;
108
+ for (let i = 0; i < n; ++i) {
109
+ const v = grid[i];
110
+ if (v < lo) lo = v;
111
+ if (v > hi) hi = v;
112
+ }
113
+ });
114
+ return (lo === 0 && hi === 0) ? [0, 1] : [lo, hi];
115
+ }
116
+
117
+ export function gridDomainDiscrete(grids, prop) {
118
+ // TODO: sort options?
119
+ const values = new InternSet();
120
+ grids.forEach(cell => {
121
+ const grid = cell[prop];
122
+ const n = grid.length;
123
+ for (let i = 0; i < n; ++i) {
124
+ values.add(grid[i]);
125
+ }
126
+ });
127
+ return Array.from(values).sort(ascending);
57
128
  }
@@ -1,44 +1,131 @@
1
1
  import { rgb } from 'd3';
2
2
 
3
- export function raster(grid, data, w, h, scale, scheme) {
4
- const n = (scheme.length >> 2) - 1;
5
- for (let j = 0, k = 0; j < h; ++j) {
6
- for (let i = 0, row = (h - j - 1) * w; i < w; ++i, k += 4) {
7
- const c = (n * scale(grid[i + row])) << 2;
8
- data[k + 0] = scheme[c + 0];
9
- data[k + 1] = scheme[c + 1];
10
- data[k + 2] = scheme[c + 2];
11
- data[k + 3] = scheme[c + 3];
12
- }
3
+ export function createCanvas(w, h) {
4
+ if (typeof document !== 'undefined') {
5
+ const c = document.createElement('canvas');
6
+ c.setAttribute('width', w);
7
+ c.setAttribute('height', h);
8
+ return c;
13
9
  }
10
+ throw new Error('Can not create a canvas instance.');
14
11
  }
15
12
 
16
- export function palette(size, interp) {
17
- const p = new Uint8ClampedArray(4 * size);
18
- const n = size - 1;
13
+ export function alphaConstant(v = 1) {
14
+ const a = (255 * v) | 0;
15
+
16
+ // rasterize
17
+ return (data, w, h) => {
18
+ for (let j = 0, k = 0; j < h; ++j) {
19
+ for (let i = 0; i < w; ++i, k += 4) {
20
+ data[k + 3] = a;
21
+ }
22
+ }
23
+ };
24
+ }
25
+
26
+ export function alphaScheme(scale) {
27
+ const { apply } = scale;
28
+
29
+ // rasterize
30
+ return (data, w, h, grid) => {
31
+ for (let j = 0, k = 0; j < h; ++j) {
32
+ for (let i = 0, row = (h - j - 1) * w; i < w; ++i, k += 4) {
33
+ data[k + 3] = (255 * apply(grid[i + row])) | 0;
34
+ }
35
+ }
36
+ };
37
+ }
38
+
39
+ export function colorConstant(v = {}) {
40
+ const { r = 0, g = 0, b = 0, opacity = 1 } = typeof v === 'string' ? rgb(v) : v;
41
+ const c = new Uint8ClampedArray([r, g, b, (255 * opacity) | 0]);
42
+
43
+ // rasterize
44
+ return (data, w, h) => {
45
+ for (let j = 0, k = 0; j < h; ++j) {
46
+ for (let i = 0; i < w; ++i, k += 4) {
47
+ data[k + 0] = c[0];
48
+ data[k + 1] = c[1];
49
+ data[k + 2] = c[2];
50
+ data[k + 3] = c[3];
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ export function colorCategory(scale) {
57
+ const { domain, range } = scale;
58
+ const idx = Object.create(null);
59
+ const p = new Uint8ClampedArray(4 * domain.length);
60
+ const n = domain.length - 1;
61
+ const m = range.length;
62
+
63
+ // build palette and index lookup map
19
64
  for (let i = 0; i <= n; ++i) {
20
- const v = interp(i / n);
65
+ const v = range[i % m];
21
66
  const { r, g, b, opacity = 1 } = typeof v === 'string' ? rgb(v) : v;
22
67
  const k = i << 2;
23
68
  p[k + 0] = r;
24
69
  p[k + 1] = g;
25
70
  p[k + 2] = b;
26
71
  p[k + 3] = (255 * opacity) | 0;
72
+ idx[domain[i]] = k;
27
73
  }
28
- return p;
74
+
75
+ // rasterize
76
+ return (data, w, h, grid) => {
77
+ if (grid.map) {
78
+ // categorical values in grid
79
+ for (let j = 0, k = 0; j < h; ++j) {
80
+ for (let i = 0, row = (h - j - 1) * w; i < w; ++i, k += 4) {
81
+ const c = idx[grid[i + row]];
82
+ data[k + 0] = p[c + 0];
83
+ data[k + 1] = p[c + 1];
84
+ data[k + 2] = p[c + 2];
85
+ data[k + 3] = p[c + 3];
86
+ }
87
+ }
88
+ } else {
89
+ // categorical value for group
90
+ const c = idx[grid];
91
+ for (let j = 0, k = 0; j < h; ++j) {
92
+ for (let i = 0; i < w; ++i, k += 4) {
93
+ data[k + 0] = p[c + 0];
94
+ data[k + 1] = p[c + 1];
95
+ data[k + 2] = p[c + 2];
96
+ data[k + 3] = p[c + 3];
97
+ }
98
+ }
99
+ }
100
+ };
29
101
  }
30
102
 
31
- export function createCanvas(w, h) {
32
- if (typeof document !== 'undefined') {
33
- const c = document.createElement('canvas');
34
- c.setAttribute('width', w);
35
- c.setAttribute('height', h);
36
- return c;
103
+ export function colorScheme(size, scale, frac) {
104
+ const { interpolate } = scale;
105
+ const p = new Uint8ClampedArray(4 * size);
106
+ const n = size - 1;
107
+
108
+ // build palette
109
+ for (let i = 0; i <= n; ++i) {
110
+ const v = interpolate(i / n);
111
+ const { r, g, b, opacity = 1 } = typeof v === 'string' ? rgb(v) : v;
112
+ const k = i << 2;
113
+ p[k + 0] = r;
114
+ p[k + 1] = g;
115
+ p[k + 2] = b;
116
+ p[k + 3] = (255 * opacity) | 0;
37
117
  }
38
- throw new Error('Can not create a canvas instance.');
39
- }
40
118
 
41
- export function opacityMap(color = 'black') {
42
- const { r, g, b } = rgb(color);
43
- return opacity => ({ r, g, b, opacity });
119
+ // rasterize
120
+ return (data, w, h, grid) => {
121
+ for (let j = 0, k = 0; j < h; ++j) {
122
+ for (let i = 0, row = (h - j - 1) * w; i < w; ++i, k += 4) {
123
+ const c = (n * frac(grid[i + row])) << 2;
124
+ data[k + 0] = p[c + 0];
125
+ data[k + 1] = p[c + 1];
126
+ data[k + 2] = p[c + 2];
127
+ data[k + 3] = p[c + 3];
128
+ }
129
+ }
130
+ };
44
131
  }
@@ -1,7 +1,4 @@
1
- import { isArrowTable } from './is-arrow-table.js';
2
-
3
- const INTEGER = 2;
4
- const TIMESTAMP = 10;
1
+ import { convertArrow, isArrowTable } from './arrow.js';
5
2
 
6
3
  export function toDataArray(data) {
7
4
  return isArrowTable(data) ? arrowToObjects(data) : data;
@@ -37,7 +34,7 @@ export function arrowToObjects(data) {
37
34
  for (let j = 0; j < numCols; ++j) {
38
35
  const child = batch.getChildAt(j);
39
36
  const { name, type } = schema.fields[j];
40
- const valueOf = convert(type);
37
+ const valueOf = convertArrow(type);
41
38
 
42
39
  // for each row in the current batch...
43
40
  for (let o = k, i = 0; i < numRows; ++i, ++o) {
@@ -51,20 +48,3 @@ export function arrowToObjects(data) {
51
48
 
52
49
  return objects;
53
50
  }
54
-
55
- function convert(type) {
56
- const { typeId } = type;
57
-
58
- // map timestamp numbers to date objects
59
- if (typeId === TIMESTAMP) {
60
- return v => v == null ? v : new Date(v);
61
- }
62
-
63
- // map bignum to number
64
- if (typeId === INTEGER && type.bitWidth >= 64) {
65
- return v => v == null ? v : Number(v);
66
- }
67
-
68
- // otherwise use Arrow JS defaults
69
- return v => v;
70
- }
@@ -45,6 +45,9 @@ export const attributeMap = new Map([
45
45
  ['xAriaDescription', 'x.ariaDescription'],
46
46
  ['xReverse', 'x.reverse'],
47
47
  ['xZero', 'x.zero'],
48
+ ['xBase', 'x.base'],
49
+ ['xExponent', 'x.exponent'],
50
+ ['xConstant', 'x.constant'],
48
51
  ['yScale', 'y.type'],
49
52
  ['yDomain', 'y.domain'],
50
53
  ['yRange', 'y.range'],
@@ -75,6 +78,9 @@ export const attributeMap = new Map([
75
78
  ['yAriaDescription', 'y.ariaDescription'],
76
79
  ['yReverse', 'y.reverse'],
77
80
  ['yZero', 'y.zero'],
81
+ ['yBase', 'y.base'],
82
+ ['yExponent', 'y.exponent'],
83
+ ['yConstant', 'y.constant'],
78
84
  ['facetMargin', 'facet.margin'],
79
85
  ['facetMarginTop', 'facet.marginTop'],
80
86
  ['facetMarginBottom', 'facet.marginBottom'],
@@ -150,6 +156,9 @@ export const attributeMap = new Map([
150
156
  ['colorReverse', 'color.reverse'],
151
157
  ['colorZero', 'color.zero'],
152
158
  ['colorTickFormat', 'color.tickFormat'],
159
+ ['colorBase', 'color.base'],
160
+ ['colorExponent', 'color.exponent'],
161
+ ['colorConstant', 'color.constant'],
153
162
  ['opacityScale', 'opacity.type'],
154
163
  ['opacityDomain', 'opacity.domain'],
155
164
  ['opacityRange', 'opacity.range'],
@@ -159,18 +168,27 @@ export const attributeMap = new Map([
159
168
  ['opacityReverse', 'opacity.reverse'],
160
169
  ['opacityZero', 'opacity.zero'],
161
170
  ['opacityTickFormat', 'opacity.tickFormat'],
171
+ ['opacityBase', 'opacity.base'],
172
+ ['opacityExponent', 'opacity.exponent'],
173
+ ['opacityConstant', 'opacity.constant'],
162
174
  ['rScale', 'r.type'],
163
175
  ['rDomain', 'r.domain'],
164
176
  ['rRange', 'r.range'],
165
177
  ['rClamp', 'r.clamp'],
166
178
  ['rNice', 'r.nice'],
167
179
  ['rZero', 'r.zero'],
180
+ ['rBase', 'r.base'],
181
+ ['rExponent', 'r.exponent'],
182
+ ['rConstant', 'r.constant'],
168
183
  ['lengthScale', 'length.type'],
169
184
  ['lengthDomain', 'length.domain'],
170
185
  ['lengthRange', 'length.range'],
171
186
  ['lengthClamp', 'length.clamp'],
172
187
  ['lengthNice', 'length.nice'],
173
188
  ['lengthZero', 'length.zero'],
189
+ ['lengthBase', 'length.base'],
190
+ ['lengthExponent', 'length.exponent'],
191
+ ['lengthConstant', 'length.constant'],
174
192
  ['projectionType', 'projection.type'],
175
193
  ['projectionParallels', 'projection.parallels'],
176
194
  ['projectionPrecision', 'projection.precision'],
@@ -1,30 +1,26 @@
1
- import { asColumn } from '@uwdata/mosaic-sql';
2
1
  import { Transform } from '../symbols.js';
2
+ import { channelScale } from '../marks/util/channel-scale.js';
3
3
 
4
- const EXTENT = [
5
- 'rectY-x', 'rectX-y', 'rect-x', 'rect-y'
6
- ];
7
-
8
- function hasExtent(channel, type) {
9
- return EXTENT.includes(`${type}-${channel}`);
10
- }
4
+ const EXTENT = new Set(['rectY-x', 'rectX-y', 'rect-x', 'rect-y']);
11
5
 
12
6
  export function bin(field, options = { steps: 25 }) {
13
7
  const fn = (mark, channel) => {
14
- return hasExtent(channel, mark.type)
15
- ? {
16
- [`${channel}1`]: binField(mark, field, options),
17
- [`${channel}2`]: binField(mark, field, { ...options, offset: 1 })
18
- }
19
- : {
20
- [channel]: binField(mark, field, options)
21
- };
8
+ if (EXTENT.has(`${mark.type}-${channel}`)) {
9
+ return {
10
+ [`${channel}1`]: binField(mark, channel, field, options),
11
+ [`${channel}2`]: binField(mark, channel, field, { ...options, offset: 1 })
12
+ };
13
+ } else {
14
+ return {
15
+ [channel]: binField(mark, channel, field, options)
16
+ };
17
+ }
22
18
  };
23
19
  fn[Transform] = true;
24
20
  return fn;
25
21
  }
26
22
 
27
- function binField(mark, column, options) {
23
+ function binField(mark, channel, column, options) {
28
24
  return {
29
25
  column,
30
26
  label: column,
@@ -32,13 +28,15 @@ function binField(mark, column, options) {
32
28
  get columns() { return [column]; },
33
29
  get basis() { return column; },
34
30
  toString() {
31
+ const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
35
32
  const { min, max } = mark.stats[column];
36
- const b = bins(min, max, options);
37
- const col = asColumn(column);
33
+ const b = bins(apply(min), apply(max), options);
34
+ const col = sqlApply(column);
38
35
  const base = b.min === 0 ? col : `(${col} - ${b.min})`;
39
36
  const alpha = `${(b.max - b.min) / b.steps}::DOUBLE`;
40
37
  const off = options.offset ? `${options.offset} + ` : '';
41
- return `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha})::INTEGER)`;
38
+ const expr = `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha}))`;
39
+ return `${sqlInvert(expr)}`;
42
40
  }
43
41
  };
44
42
  }
@@ -1,17 +0,0 @@
1
- import { epoch_ms, sql } from '@uwdata/mosaic-sql';
2
-
3
- export function binField(mark, channel, expr) {
4
- if (!mark.stats) return field;
5
- const { field } = mark.channelField(channel);
6
- const { type } = mark.stats[field.column];
7
- expr = expr ?? field;
8
- return type === 'date' ? epoch_ms(expr) : expr;
9
- }
10
-
11
- export function bin1d(x, x0, x1, n, reverse = false, pad = 1) {
12
- const d = (n - pad) / (x1 - x0);
13
- const f = d !== 1 ? ` * ${d}::DOUBLE` : '';
14
- return reverse
15
- ? sql`(${+x1} - ${x}::DOUBLE)${f}`
16
- : sql`(${x}::DOUBLE - ${+x0})${f}`;
17
- }
@@ -1,3 +0,0 @@
1
- export function isArrowTable(values) {
2
- return typeof values?.getChild === 'function';
3
- }