@uwdata/mosaic-plot 0.12.0 → 0.12.2

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.12.0",
3
+ "version": "0.12.2",
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.16",
32
- "@uwdata/mosaic-core": "^0.12.0",
33
- "@uwdata/mosaic-sql": "^0.12.0",
32
+ "@uwdata/mosaic-core": "^0.12.2",
33
+ "@uwdata/mosaic-sql": "^0.12.2",
34
34
  "d3": "^7.9.0",
35
35
  "isoformat": "^0.2.1"
36
36
  },
37
- "gitHead": "523b1afe2a0880291c92f81e4a7b91829362d285"
37
+ "gitHead": "0ca741d840b98039255f26a5ceedf10be66f790e"
38
38
  }
@@ -1,5 +1,6 @@
1
1
  import { toDataColumns } from '@uwdata/mosaic-core';
2
2
  import { binLinear1d, isBetween } from '@uwdata/mosaic-sql';
3
+ import { max, sum } from 'd3';
3
4
  import { Transient } from '../symbols.js';
4
5
  import { binExpr } from './util/bin-expr.js';
5
6
  import { dericheConfig, dericheConv1d } from './util/density.js';
@@ -8,9 +9,17 @@ import { grid1d } from './util/grid.js';
8
9
  import { handleParam } from './util/handle-param.js';
9
10
  import { Mark, channelOption, markQuery } from './Mark.js';
10
11
 
12
+ const GROUPBY = { fill: 1, stroke: 1, z: 1 };
13
+
11
14
  export class Density1DMark extends Mark {
12
15
  constructor(type, source, options) {
13
- const { bins = 1024, bandwidth = 20, ...channels } = options;
16
+ const {
17
+ bins = 1024,
18
+ bandwidth = 20,
19
+ normalize = false,
20
+ stack = false,
21
+ ...channels
22
+ } = options;
14
23
  const dim = type.endsWith('X') ? 'y' : 'x';
15
24
 
16
25
  super(type, source, channels, dim === 'x' ? xext : yext);
@@ -24,7 +33,17 @@ export class Density1DMark extends Mark {
24
33
  /** @type {number} */
25
34
  this.bandwidth = handleParam(bandwidth, value => {
26
35
  this.bandwidth = value;
27
- return this.grid ? this.convolve().update() : null;
36
+ return this.grids ? this.convolve().update() : null;
37
+ });
38
+
39
+ /** @type {string | boolean} */
40
+ this.normalize = handleParam(normalize, value => {
41
+ return (this.normalize = value, this.convolve().update());
42
+ });
43
+
44
+ /** @type {boolean} */
45
+ this.stack = handleParam(stack, value => {
46
+ return (this.stack = value, this.update());
28
47
  });
29
48
  }
30
49
 
@@ -42,48 +61,76 @@ export class Density1DMark extends Mark {
42
61
  const q = markQuery(channels, this.sourceTable(), [dim])
43
62
  .where(filter.concat(isBetween(bx, extent)));
44
63
  const v = this.channelField('weight') ? 'weight' : null;
45
- return binLinear1d(q, x, v);
64
+ const g = this.groupby = channels.flatMap(c => {
65
+ return (GROUPBY[c.channel] && c.field) ? c.as : [];
66
+ });
67
+ return binLinear1d(q, x, v, g);
46
68
  }
47
69
 
48
70
  queryResult(data) {
49
- const { columns: { index, density } } = toDataColumns(data);
50
- this.grid = grid1d(this.bins, index, density);
71
+ const c = toDataColumns(data).columns;
72
+ this.grids = grid1d(this.bins, c.index, c.density, c, this.groupby);
51
73
  return this.convolve();
52
74
  }
53
75
 
54
76
  convolve() {
55
- const { bins, bandwidth, dim, grid, plot, extent: [lo, hi] } = this;
77
+ const {
78
+ bins, bandwidth, normalize, dim, grids, groupby, plot, extent: [lo, hi]
79
+ } = this;
80
+
81
+ const cols = grids.columns;
82
+ const numGrids = grids.numRows;
56
83
 
57
- // perform smoothing
58
- const neg = grid.some(v => v < 0);
84
+ const b = this.channelField(dim).as;
85
+ const v = dim === 'x' ? 'y' : 'x';
59
86
  const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
87
+ const neg = cols._grid.some(grid => grid.some(v => v < 0));
60
88
  const config = dericheConfig(bandwidth * (bins - 1) / size, neg);
61
- const result = dericheConv1d(config, grid, bins);
62
89
 
63
- // map smoothed grid values to sample data points
64
- const v = dim === 'x' ? 'y' : 'x';
65
- const b = this.channelField(dim).as;
66
90
  const b0 = +lo;
67
91
  const delta = (hi - b0) / (bins - 1);
68
- const scale = 1 / delta;
69
92
 
70
- const _b = new Float64Array(bins);
71
- const _v = new Float64Array(bins);
72
- for (let i = 0; i < bins; ++i) {
73
- _b[i] = b0 + i * delta;
74
- _v[i] = result[i] * scale;
93
+ const numRows = bins * numGrids;
94
+ const _b = new Float64Array(numRows);
95
+ const _v = new Float64Array(numRows);
96
+ const _g = groupby.reduce((m, name) => (m[name] = Array(numRows), m), {});
97
+
98
+ for (let k = 0, g = 0; g < numGrids; ++g) {
99
+ // fill in groupby values
100
+ groupby.forEach(name => _g[name].fill(cols[name][g], k, k + bins));
101
+
102
+ // perform smoothing, map smoothed grid values to sample data points
103
+ const grid = cols._grid[g];
104
+ const result = dericheConv1d(config, grid, bins);
105
+ const scale = 1 / norm(grid, result, delta, normalize);
106
+ for (let i = 0; i < bins; ++i, ++k) {
107
+ _b[k] = b0 + i * delta;
108
+ _v[k] = result[i] * scale;
109
+ }
75
110
  }
76
- this.data = { numRows: bins, columns: { [b]: _b, [v]: _v } };
77
111
 
112
+ this.data = { numRows, columns: { [b]: _b, [v]: _v, ..._g } };
78
113
  return this;
79
114
  }
80
115
 
81
116
  plotSpecs() {
82
- const { type, data: { numRows: length, columns }, channels, dim } = this;
83
- const options = dim === 'x' ? { y: columns.y } : { x: columns.x };
117
+ const { type, data: { numRows: length, columns }, channels, dim, stack } = this;
118
+
119
+ // control if Plot's implicit stack transform is applied
120
+ // no stacking is done if x2/y2 are used instead of x/y
121
+ const _ = type.startsWith('area') && !stack ? '2' : '';
122
+ const options = dim === 'x' ? { [`y${_}`]: columns.y } : { [`x${_}`]: columns.x };
123
+
84
124
  for (const c of channels) {
85
125
  options[c.channel] = channelOption(c, columns);
86
126
  }
87
127
  return [{ type, data: { length }, options }];
88
128
  }
89
129
  }
130
+
131
+ function norm(grid, smoothed, delta, type) {
132
+ const value = type === true || type === 'sum' ? sum(grid)
133
+ : type === 'max' ? max(smoothed)
134
+ : delta;
135
+ return value || 1;
136
+ }
@@ -75,7 +75,10 @@ export class HexbinMark extends Mark {
75
75
  float64(x1),
76
76
  div(add(mul(add(x, mul(0.5, bitAnd(y, 1))), dx), ox), xr)
77
77
  ),
78
- [yc.as]: sub(float64(y2), div(add(mul(y, dy), oy), yr)),
78
+ [yc.as]: sub(
79
+ float64(y2),
80
+ div(add(mul(y, dy), oy), yr)
81
+ ),
79
82
  ...cols
80
83
  })
81
84
  .groupby(x, y, ...dims)
@@ -83,7 +86,7 @@ export class HexbinMark extends Mark {
83
86
  // Subquery performs hex binning in screen space and also passes
84
87
  // original columns through (the DB should optimize this).
85
88
  Query.select({
86
- [py]: div(mul(yr, sub(sub(y2, yc.field), oy)), dy),
89
+ [py]: div(sub(mul(yr, sub(y2, yc.field)), oy), dy),
87
90
  [pj]: int32(round(py)),
88
91
  [px]: sub(
89
92
  div(sub(mul(xr, sub(xc.field, x1)), ox), dx),
package/src/marks/Mark.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isParam, MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
2
- import { Query, SelectQuery, collectParams, column, isAggregateExpression, isColumnRef, isNode, isParamLike } from '@uwdata/mosaic-sql';
2
+ import { Query, SelectQuery, collectParams, column, isAggregateExpression, isColumnParam, isColumnRef, isNode, 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';
@@ -15,7 +15,7 @@ const isFieldObject = (channel, field) => {
15
15
  const fieldEntry = (channel, field) => ({
16
16
  channel,
17
17
  field,
18
- as: isColumnRef(field) ? field.column : channel
18
+ as: isColumnRef(field) && !isColumnParam(field) ? field.column : channel
19
19
  });
20
20
  const valueEntry = (channel, value) => ({ channel, value });
21
21
 
@@ -136,7 +136,11 @@ export class Mark extends MosaicClient {
136
136
  }
137
137
 
138
138
  const table = this.sourceTable();
139
- return Array.from(fields, ([c, s]) => ({ table, column: c, stats: s }));
139
+ return Array.from(fields, ([c, s]) => ({
140
+ table,
141
+ column: c,
142
+ stats: Array.from(s)
143
+ }));
140
144
  }
141
145
 
142
146
  fieldInfo(info) {
@@ -24,15 +24,44 @@ export function array(size, proto = []) {
24
24
  * @param {number} size The grid size.
25
25
  * @param {Arrayish} index The grid indices for sample points.
26
26
  * @param {Arrayish} value The sample point values.
27
- * @returns {Arrayish} The generated value grid.
27
+ * @param {Record<string,Arrayish>} columns Named column arrays with groupby values.
28
+ * @param {string[]} groupby The names of columns to group by.
29
+ * @returns {{
30
+ * numRows: number;
31
+ * columns: { [key:string]: Arrayish }
32
+ * }} Named column arrays of generated grid values.
28
33
  */
29
- export function grid1d(size, index, value) {
30
- const G = array(size, value);
31
- const n = value.length;
32
- for (let i = 0; i < n; ++i) {
33
- G[index[i]] = value[i];
34
+ export function grid1d(size, index, value, columns, groupby) {
35
+ const numRows = index.length;
36
+ const result = {};
37
+ const cells = [];
38
+
39
+ // if grouped, generate per-row group indices
40
+ if (groupby?.length) {
41
+ const group = new Int32Array(numRows);
42
+ const gvalues = groupby.map(name => columns[name]);
43
+ const cellMap = {};
44
+ for (let row = 0; row < numRows; ++row) {
45
+ const key = gvalues.map(group => group[row]);
46
+ group[row] = cellMap[key] ??= cells.push(key) - 1;
47
+ }
48
+ for (let i = 0; i < groupby.length; ++i) {
49
+ result[groupby[i]] = cells.map(cell => cell[i]);
50
+ }
51
+ const G = result._grid = cells.map(() => array(size, value));
52
+ for (let row = 0; row < numRows; ++row) {
53
+ G[group[row]][index[row]] = value[row];
54
+ }
55
+ } else {
56
+ cells.push([]); // single group
57
+ const [G] = result._grid = [array(size, value)]
58
+ for (let row = 0; row < numRows; ++row) {
59
+ G[index[row]] = value[row];
60
+ }
34
61
  }
35
- return G;
62
+
63
+ // @ts-ignore
64
+ return { numRows: cells.length, columns: result };
36
65
  }
37
66
 
38
67
  /**
@@ -1,5 +1,7 @@
1
1
  const constantOptions = new Set([
2
+ 'offset',
2
3
  'order',
4
+ 'reverse',
3
5
  'sort',
4
6
  'label',
5
7
  'anchor',