@uwdata/mosaic-plot 0.7.0 → 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.
@@ -10,6 +10,7 @@ export class RasterTileMark extends Grid2DMark {
10
10
  constructor(source, options) {
11
11
  const { origin = [0, 0], dim = 'xy', ...markOptions } = options;
12
12
  super('image', source, markOptions);
13
+ this.image = null;
13
14
 
14
15
  // TODO: make part of data source instead of options?
15
16
  this.origin = origin;
@@ -34,15 +35,15 @@ export class RasterTileMark extends Grid2DMark {
34
35
  }
35
36
 
36
37
  tileQuery(extent) {
37
- const { binType, binPad, channels, densityMap, source } = this;
38
+ const { interpolate, pad, channels, densityMap, source } = this;
38
39
  const [[x0, x1], [y0, y1]] = extent;
39
40
  const [nx, ny] = this.bins;
40
- const [x, bx] = binExpr(this, 'x', nx, [x0, x1], binPad);
41
- const [y, by] = binExpr(this, 'y', ny, [y0, y1], binPad);
41
+ const [x, bx] = binExpr(this, 'x', nx, [x0, x1], pad);
42
+ const [y, by] = binExpr(this, 'y', ny, [y0, y1], pad);
42
43
 
43
44
  // with padded bins, include the entire domain extent
44
45
  // if the bins are flush, exclude the extent max
45
- const bounds = binPad
46
+ const bounds = pad
46
47
  ? [isBetween(bx, [+x0, +x1]), isBetween(by, [+y0, +y1])]
47
48
  : [lte(+x0, bx), lt(bx, +x1), lte(+y0, by), lt(by, +y1)];
48
49
 
@@ -83,7 +84,7 @@ export class RasterTileMark extends Grid2DMark {
83
84
  }
84
85
 
85
86
  // generate grid binning query
86
- if (binType === 'linear') {
87
+ if (interpolate === 'linear') {
87
88
  if (aggr.length > 1) {
88
89
  throw new Error('Linear binning not applicable to multiple aggregates.');
89
90
  }
@@ -102,14 +103,14 @@ export class RasterTileMark extends Grid2DMark {
102
103
  if (this.prefetch) mc.cancel(this.prefetch);
103
104
 
104
105
  // get view extent info
105
- const { binPad, tileX, tileY, origin: [tx, ty] } = this;
106
- const [m, n] = this.bins = this.binDimensions(this);
106
+ const { pad, tileX, tileY, origin: [tx, ty] } = this;
107
+ const [m, n] = this.bins = this.binDimensions();
107
108
  const [x0, x1] = extentX(this, this._filter);
108
109
  const [y0, y1] = extentY(this, this._filter);
109
110
  const xspan = x1 - x0;
110
111
  const yspan = y1 - y0;
111
- const xx = Math.floor((x0 - tx) * (m - binPad) / xspan);
112
- const yy = Math.floor((y0 - ty) * (n - binPad) / yspan);
112
+ const xx = Math.floor((x0 - tx) * (m - pad) / xspan);
113
+ const yy = Math.floor((y0 - ty) * (n - pad) / yspan);
113
114
 
114
115
  const tileExtent = (i, j) => [
115
116
  [tx + i * xspan, tx + (i + 1) * xspan],
@@ -155,7 +156,11 @@ export class RasterTileMark extends Grid2DMark {
155
156
 
156
157
  // wait for tile queries to complete, then update
157
158
  const tiles = await Promise.all(queries);
158
- this.grids = [{ density: processTiles(m, n, xx, yy, coords, tiles) }];
159
+ const density = processTiles(m, n, xx, yy, coords, tiles);
160
+ this.grids0 = {
161
+ numRows: density.length,
162
+ columns: { density: [density] }
163
+ };
159
164
  this.convolve().update();
160
165
  }
161
166
 
@@ -164,37 +169,45 @@ export class RasterTileMark extends Grid2DMark {
164
169
  }
165
170
 
166
171
  rasterize() {
167
- const { bins, kde } = this;
172
+ const { bins, grids } = this;
168
173
  const [ w, h ] = bins;
174
+ const { numRows, columns } = grids;
169
175
 
170
176
  // raster data
171
177
  const { canvas, ctx, img } = imageData(this, w, h);
172
178
 
173
179
  // color + opacity encodings
174
180
  const { alpha, alphaProp, color, colorProp } = rasterEncoding(this);
181
+ const alphaData = columns[alphaProp] ?? [];
182
+ const colorData = columns[colorProp] ?? [];
175
183
 
176
184
  // generate rasters
177
- this.data = kde.map(cell => {
178
- color?.(img.data, w, h, cell[colorProp]);
179
- alpha?.(img.data, w, h, cell[alphaProp]);
180
- ctx.putImageData(img, 0, 0);
181
- return { src: canvas.toDataURL() };
182
- });
185
+ this.data = {
186
+ numRows,
187
+ columns: {
188
+ src: Array.from({ length: numRows }, (_, i) => {
189
+ color?.(img.data, w, h, colorData[i]);
190
+ alpha?.(img.data, w, h, alphaData[i]);
191
+ ctx.putImageData(img, 0, 0);
192
+ return canvas.toDataURL();
193
+ })
194
+ }
195
+ };
183
196
 
184
197
  return this;
185
198
  }
186
199
 
187
200
  plotSpecs() {
188
- const { type, data, plot } = this;
201
+ const { type, plot, data: { numRows: length, columns } } = this;
189
202
  const options = {
190
- src: 'src',
203
+ src: columns.src,
191
204
  width: plot.innerWidth(),
192
205
  height: plot.innerHeight(),
193
206
  preserveAspectRatio: 'none',
194
207
  imageRendering: this.channel('imageRendering')?.value,
195
208
  frameAnchor: 'middle'
196
209
  };
197
- return [{ type, data, options }];
210
+ return [{ type, data: { length }, options }];
198
211
  }
199
212
  }
200
213
 
@@ -7,17 +7,24 @@ import {
7
7
  import { qt } from './util/stats.js';
8
8
  import { Mark, channelOption } from './Mark.js';
9
9
  import { handleParam } from './util/handle-param.js';
10
- import { toDataArray } from './util/to-data-array.js';
10
+ import { toDataColumns } from './util/to-data-columns.js';
11
11
 
12
12
  export class RegressionMark extends Mark {
13
13
  constructor(source, options) {
14
14
  const { ci = 0.95, precision = 4, ...channels } = options;
15
15
  super('line', source, channels);
16
- const update = () => {
17
- return this.modelFit ? this.confidenceBand().update() : null
18
- };
19
- handleParam(this, 'ci', ci, update);
20
- handleParam(this, 'precision', precision, update);
16
+
17
+ const update = () => this.modelFit ? this.confidenceBand().update() : null;
18
+
19
+ /** @type {number} */
20
+ this.ci = handleParam(ci, value => {
21
+ return (this.ci = value, update());
22
+ });
23
+
24
+ /** @type {number} */
25
+ this.precision = handleParam(precision, value => {
26
+ return (this.precision = value, update());
27
+ });
21
28
  }
22
29
 
23
30
  query(filter = []) {
@@ -44,10 +51,10 @@ export class RegressionMark extends Mark {
44
51
  }
45
52
 
46
53
  queryResult(data) {
47
- this.modelFit = toDataArray(data);
54
+ this.modelFit = toDataColumns(data);
48
55
 
49
56
  // regression line
50
- this.lineData = this.modelFit.flatMap(m => linePoints(m));
57
+ this.lineData = linePoints(this.modelFit);
51
58
 
52
59
  // prepare confidence band
53
60
  return this.confidenceBand();
@@ -56,15 +63,17 @@ export class RegressionMark extends Mark {
56
63
  confidenceBand() {
57
64
  // regression ci area
58
65
  const { ci, modelFit, precision, plot } = this;
59
- const w = plot.innerWidth();
60
- this.areaData = ci ? modelFit.flatMap(m => areaPoints(ci, precision, m, w)) : null;
66
+ const width = plot.innerWidth();
67
+ this.areaData = ci ? areaPoints(modelFit, ci, precision, width) : null;
61
68
  return this;
62
69
  }
63
70
 
64
71
  plotSpecs() {
65
72
  const { lineData, areaData, channels, ci } = this;
66
- const lopt = { x: 'x', y: 'y' };
67
- const aopt = { x: 'x', y1: 'y1', y2: 'y2', fillOpacity: 0.1 };
73
+ const lcols = lineData.columns;
74
+ const acols = ci ? areaData.columns : {};
75
+ const lopt = { x: lcols.x, y: lcols.y };
76
+ const aopt = { x: acols.x, y1: acols.y1, y2: acols.y2, fillOpacity: 0.1 };
68
77
 
69
78
  for (const c of channels) {
70
79
  switch (c.channel) {
@@ -72,46 +81,72 @@ export class RegressionMark extends Mark {
72
81
  case 'y':
73
82
  case 'fill':
74
83
  break;
84
+ case 'tip':
85
+ aopt.tip = channelOption(c, acols);
86
+ break;
75
87
  case 'stroke':
76
- lopt.stroke = aopt.fill = channelOption(c);
88
+ lopt.stroke = channelOption(c, lcols);
89
+ aopt.fill = channelOption(c, acols);
77
90
  break;
78
91
  case 'strokeOpacity':
79
- lopt.strokeOpacity = channelOption(c);
92
+ lopt.strokeOpacity = channelOption(c, lcols);
80
93
  break;
81
94
  case 'fillOpacity':
82
- aopt.fillOpacity = channelOption(c);
95
+ aopt.fillOpacity = channelOption(c, acols);
83
96
  break;
84
97
  default:
85
- lopt[c.channel] = aopt[c.channel] = channelOption(c);
98
+ lopt[c.channel] = channelOption(c, lcols);
99
+ aopt[c.channel] = channelOption(c, acols);
86
100
  break;
87
101
  }
88
102
  }
89
103
 
90
104
  return [
91
- ...(ci ? [{ type: 'areaY', data: areaData, options: aopt }] : []),
92
- { type: 'line', data: lineData, options: lopt }
105
+ ...(ci ? [{ type: 'areaY', data: { length: areaData.numRows }, options: aopt }] : []),
106
+ { type: 'line', data: { length: lineData.numRows }, options: lopt }
93
107
  ];
94
108
  }
95
109
  }
96
110
 
97
- function linePoints(model) {
111
+ function concat(a, b) {
112
+ if (a.concat) return a.concat(b);
113
+ const array = new (a.constructor)(a.length + b.length);
114
+ array.set(a, 0);
115
+ array.set(b, a.length);
116
+ return array;
117
+ }
118
+
119
+ function linePoints(fit) {
98
120
  // eslint-disable-next-line no-unused-vars
99
- const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = model;
100
- return [
101
- { x: x0, y: intercept + x0 * slope, ...rest },
102
- { x: x1, y: intercept + x1 * slope, ...rest }
103
- ];
121
+ const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
122
+ const predict = (x, i) => intercept[i] + x * slope[i];
123
+ const x = concat(x0, x1);
124
+ const y = concat(x0.map(predict), x1.map(predict));
125
+ for (const name in rest) {
126
+ rest[name] = concat(rest[name], rest[name]);
127
+ }
128
+ return { numRows: x.length, columns: { x, y, ...rest } };
104
129
  }
105
130
 
106
- function areaPoints(ci, precision, model, width) {
107
- const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = model;
108
- const pp = precision * (x1 - x0) / width;
109
- const t_sy = qt((1 - ci) / 2, n - 2) * Math.sqrt(ssy / (n - 2));
110
- return range(x0, x1 - pp / 2, pp)
111
- .concat(x1)
112
- .map(x => {
113
- const y = intercept + x * slope;
114
- const ye = t_sy * Math.sqrt(1 / n + (x - xm) ** 2 / ssx);
115
- return { x, y1: y - ye, y2: y + ye, ...rest };
131
+ function areaPoints(fit, ci, precision, width) {
132
+ const len = fit.numRows;
133
+ const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
134
+ const other = Object.keys(rest);
135
+ const columns = { x: [], y1: [], y2: [] };
136
+ other.forEach(name => columns[name] = []);
137
+
138
+ for (let i = 0; i < len; ++i) {
139
+ const pp = precision * (x1[i] - x0[i]) / width;
140
+ const t_sy = qt((1 - ci) / 2, n[i] - 2) * Math.sqrt(ssy[i] / (n[i] - 2));
141
+ range(x0[i], x1[i] - pp / 2, pp).concat(x1[i]).forEach(x => {
142
+ const y = intercept[i] + x * slope[i];
143
+ const ye = t_sy * Math.sqrt(1 / n[i] + (x - xm[i]) ** 2 / ssx[i]);
144
+ columns.x.push(x);
145
+ columns.y1.push(y - ye);
146
+ columns.y2.push(y + ye);
147
+ other.forEach(name => columns[name].push(rest[name][i]));
116
148
  });
149
+ }
150
+
151
+ return { numRows: columns.x.length, columns };
117
152
  }
@@ -1,119 +1,127 @@
1
1
  import { InternSet, ascending } from 'd3';
2
- import {
3
- convertArrowArrayType,
4
- convertArrowColumn,
5
- isArrowTable
6
- } from '@uwdata/mosaic-core';
7
2
 
8
- function arrayType(values, name = 'density') {
9
- return isArrowTable(values)
10
- ? convertArrowArrayType(values.getChild(name).type)
11
- : typeof values[0]?.[name] === 'number' ? Float64Array : Array;
3
+ /**
4
+ * @typedef {Array | Int8Array | Uint8Array | Uint8ClampedArray
5
+ * | Int16Array | Uint16Array | Int32Array | Uint32Array
6
+ * | Float32Array | Float64Array
7
+ * } Arrayish - an Array or TypedArray
8
+ */
9
+
10
+ /**
11
+ * Generate a new array with designated size and type.
12
+ * @param {number} size The size of the array
13
+ * @param {Arrayish} [proto] A prototype object of the desired array type.
14
+ * This may be a typed array or standard array (the default).
15
+ * @returns {Arrayish} The generated array.
16
+ */
17
+ export function array(size, proto = []) {
18
+ // @ts-ignore
19
+ return new proto.constructor(size);
12
20
  }
13
21
 
14
- export function grid1d(n, values, name = 'density') {
15
- const grid = new (arrayType(values))(n);
16
- if (isArrowTable(values)) {
17
- // optimize access for Arrow tables
18
- const numRows = values.numRows;
19
- if (numRows === 0) return grid;
20
- const index = convertArrowColumn(values.getChild('index'));
21
- const value = convertArrowColumn(values.getChild(name));
22
- for (let row = 0; row < numRows; ++row) {
23
- grid[index[row]] = value[row];
24
- }
25
- } else {
26
- // fallback to iterable data
27
- for (const row of values) {
28
- grid[row.index] = row[name];
29
- }
22
+ /**
23
+ * Create a 1D grid for the given sample values
24
+ * @param {number} size The grid size.
25
+ * @param {Arrayish} index The grid indices for sample points.
26
+ * @param {Arrayish} value The sample point values.
27
+ * @returns {Arrayish} The generated value grid.
28
+ */
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];
30
34
  }
31
- return grid;
35
+ return G;
32
36
  }
33
37
 
34
- export function grid2d(w, h, values, aggr, groupby = [], interpolate) {
38
+ /**
39
+ * Create a 2D grid for the given sample values.
40
+ * Can handle multiple grids and groupby values per output row.
41
+ * @param {number} w The grid width.
42
+ * @param {number} h The grid height.
43
+ * @param {Arrayish} index The grid indices for sample points.
44
+ * An index value is an integer of the form (y * w + x).
45
+ * @param {Record<string,Arrayish>} columns Named column arrays with sample point values.
46
+ * @param {string[]} aggregates The names of aggregate columns to grid.
47
+ * @param {string[]} groupby The names of additional columns to group by.
48
+ * @param {function} [interpolate] A grid interpolation function.
49
+ * By default sample values are directly copied to output grid arrays.
50
+ * @returns {{
51
+ * numRows: number;
52
+ * columns: { [key:string]: Arrayish }
53
+ * }} Named column arrays of generated grid values.
54
+ */
55
+ export function grid2d(w, h, index, columns, aggregates, groupby, interpolate) {
56
+ const numRows = index.length;
35
57
  const size = w * h;
36
- const Types = aggr.map(name => arrayType(values, name));
37
- const numAggr = aggr.length;
58
+ const values = aggregates.map(name => columns[name]);
59
+ const result = {};
60
+ const cells = [];
61
+ const group = new Int32Array(numRows);
38
62
 
39
- // grid data tuples
40
- const createCell = (key) => {
41
- const cell = {};
42
- groupby.forEach((name, i) => cell[name] = key[i]);
43
- aggr.forEach((name, i) => cell[name] = new Types[i](size));
44
- return cell;
45
- };
46
- const cellMap = {};
47
- const baseCell = groupby.length ? null : (cellMap[[]] = createCell([]));
48
- const getCell = groupby.length
49
- ? key => cellMap[key] ?? (cellMap[key] = createCell(key))
50
- : () => baseCell;
51
-
52
- // early exit if empty query result
53
- const numRows = values.numRows;
54
- if (numRows === 0) return Object.values(cellMap);
55
-
56
- // extract arrays from arrow table
57
- const index = convertArrowColumn(values.getChild('index'));
58
- const value = aggr.map(name => convertArrowColumn(values.getChild(name)));
59
- const groups = groupby.map(name => values.getChild(name));
60
-
61
- if (!interpolate) {
62
- // if no interpolation, copy values over
63
+ // if grouped, generate per-row group indices
64
+ if (groupby?.length) {
65
+ const gvalues = groupby.map(name => columns[name]);
66
+ const cellMap = {};
63
67
  for (let row = 0; row < numRows; ++row) {
64
- const key = groups.map(vec => vec.get(row));
65
- const cell = getCell(key);
66
- for (let i = 0; i < numAggr; ++i) {
67
- cell[aggr[i]][index[row]] = value[i][row];
68
- }
68
+ const key = gvalues.map(group => group[row]);
69
+ group[row] = cellMap[key] ??= cells.push(key) - 1;
70
+ }
71
+ for (let i = 0; i < groupby.length; ++i) {
72
+ result[groupby[i]] = cells.map(cell => cell[i]);
69
73
  }
70
74
  } else {
75
+ cells.push([]); // single group
76
+ }
77
+
78
+ if (interpolate) {
71
79
  // prepare index arrays, then interpolate grid values
72
80
  const X = index.map(k => k % w);
73
81
  const Y = index.map(k => Math.floor(k / w));
74
- if (groupby.length) {
75
- for (let row = 0; row < numRows; ++row) {
76
- const key = groups.map(vec => vec.get(row));
77
- const cell = getCell(key);
78
- if (!cell.index) { cell.index = []; }
79
- cell.index.push(row);
80
- }
81
- } else {
82
- baseCell.index = index.map((_, i) => i);
82
+ const I = cells.map(() => []);
83
+ for (let row = 0; row < numRows; ++row) {
84
+ I[group[row]].push(row);
83
85
  }
84
- Object.values(cellMap).forEach(cell => {
85
- for (let i = 0; i < numAggr; ++i) {
86
- interpolate(cell.index, w, h, X, Y, value[i], cell[aggr[i]]);
86
+ aggregates.forEach((name, i) => {
87
+ const V = values[i];
88
+ result[name] = cells.map((_, j) => interpolate(I[j], w, h, X, Y, V));
89
+ });
90
+ } else {
91
+ // no interpolation, copy values directly to grids
92
+ aggregates.forEach((name, i) => {
93
+ const V = values[i];
94
+ const G = result[name] = cells.map(() => array(size, V));
95
+ for (let row = 0; row < numRows; ++row) {
96
+ G[group[row]][index[row]] = V[row];
87
97
  }
88
- delete cell.index;
89
- })
98
+ });
90
99
  }
91
100
 
92
- return Object.values(cellMap);
101
+ // @ts-ignore
102
+ return { numRows: cells.length, columns: result };
93
103
  }
94
104
 
95
- export function gridDomainContinuous(grids, prop) {
96
- let lo = 0, hi = 0;
97
- grids.forEach(cell => {
98
- const grid = cell[prop];
99
- const n = grid.length;
105
+ export function gridDomainContinuous(grids) {
106
+ let lo = Infinity, hi = -Infinity;
107
+ grids.forEach(G => {
108
+ const n = G.length;
100
109
  for (let i = 0; i < n; ++i) {
101
- const v = grid[i];
110
+ const v = G[i];
102
111
  if (v < lo) lo = v;
103
112
  if (v > hi) hi = v;
104
113
  }
105
114
  });
106
- return (lo === 0 && hi === 0) ? [0, 1] : [lo, hi];
115
+ return (Number.isFinite(lo) && Number.isFinite(hi)) ? [lo, hi] : [0, 1];
107
116
  }
108
117
 
109
- export function gridDomainDiscrete(grids, prop) {
110
- // TODO: sort options?
118
+ export function gridDomainDiscrete(grids) {
119
+ // TODO: sort options?
111
120
  const values = new InternSet();
112
- grids.forEach(cell => {
113
- const grid = cell[prop];
114
- const n = grid.length;
121
+ grids.forEach(G => {
122
+ const n = G.length;
115
123
  for (let i = 0; i < n; ++i) {
116
- values.add(grid[i]);
124
+ values.add(G[i]);
117
125
  }
118
126
  });
119
127
  return Array.from(values).sort(ascending);
@@ -1,14 +1,13 @@
1
1
  import { isParam } from '@uwdata/mosaic-core';
2
2
 
3
- export function handleParam(client, key, param, update) {
4
- if (isParam(param)) {
5
- update = update || (() => client.requestUpdate());
6
- param.addEventListener('value', value => {
7
- client[key] = value;
8
- return update();
9
- });
10
- client[key] = param.value;
11
- } else {
12
- client[key] = param;
13
- }
3
+ /**
4
+ * Utility to check if a value is a Param, and if so, bind a listener.
5
+ * @param {*} value A potentially Param-typed value.
6
+ * @param {(value: *) => Promise|void} update Update callback
7
+ * @returns the input value or (if a Param) the current Param value.
8
+ */
9
+ export function handleParam(value, update) {
10
+ return isParam(value)
11
+ ? (value.addEventListener('value', update), value.value)
12
+ : value;
14
13
  }
@@ -20,6 +20,13 @@
20
20
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  // SOFTWARE.
22
22
 
23
+ /**
24
+ * ibetainv function
25
+ * @param {number} p
26
+ * @param {number} a
27
+ * @param {number} b
28
+ * @returns {number}
29
+ */
23
30
  export function ibetainv(p, a, b) {
24
31
  var EPS = 1e-8;
25
32
  var a1 = a - 1;
@@ -60,11 +67,18 @@ export function ibetainv(p, a, b) {
60
67
  return x;
61
68
  }
62
69
 
70
+ /**
71
+ * ibeta function
72
+ * @param {number} x
73
+ * @param {number} a
74
+ * @param {number} b
75
+ * @returns {number}
76
+ */
63
77
  export function ibeta(x, a, b) {
64
78
  // Factors in front of the continued fraction.
65
79
  var bt =
66
80
  x === 0 || x === 1 ? 0 : Math.exp(gammaln(a + b) - gammaln(a) - gammaln(b) + a * Math.log(x) + b * Math.log(1 - x));
67
- if (x < 0 || x > 1) return false;
81
+ if (x < 0 || x > 1) return 0;
68
82
  if (x < (a + 1) / (a + b + 2))
69
83
  // Use continued fraction directly.
70
84
  return (bt * betacf(x, a, b)) / a;
@@ -72,6 +86,13 @@ export function ibeta(x, a, b) {
72
86
  return 1 - (bt * betacf(1 - x, b, a)) / b;
73
87
  }
74
88
 
89
+ /**
90
+ * betacf function
91
+ * @param {number} x
92
+ * @param {number} a
93
+ * @param {number} b
94
+ * @returns {number}
95
+ */
75
96
  export function betacf(x, a, b) {
76
97
  var fpmin = 1e-30;
77
98
  var m = 1;
@@ -112,6 +133,11 @@ export function betacf(x, a, b) {
112
133
  return h;
113
134
  }
114
135
 
136
+ /**
137
+ * gammaln function
138
+ * @param {number} x
139
+ * @returns {number}
140
+ */
115
141
  export function gammaln(x) {
116
142
  var j = 0;
117
143
  var cof = [
@@ -126,6 +152,12 @@ export function gammaln(x) {
126
152
  return Math.log((2.506628274631 * ser) / xx) - tmp;
127
153
  }
128
154
 
155
+ /**
156
+ * qt function
157
+ * @param {number} p
158
+ * @param {number} dof
159
+ * @returns {number}
160
+ */
129
161
  export function qt(p, dof) {
130
162
  var x = ibetainv(2 * Math.min(p, 1 - p), 0.5 * dof, 0.5);
131
163
  x = Math.sqrt((dof * (1 - x)) / x);
@@ -0,0 +1,71 @@
1
+ import { convertArrowColumn, isArrowTable } from '@uwdata/mosaic-core';
2
+
3
+ /**
4
+ * @typedef {Array | Int8Array | Uint8Array | Uint8ClampedArray
5
+ * | Int16Array | Uint16Array | Int32Array | Uint32Array
6
+ * | Float32Array | Float64Array
7
+ * } Arrayish - an Array or TypedArray
8
+ */
9
+
10
+ /**
11
+ * @typedef {
12
+ * | { numRows: number, columns: Record<string,Arrayish> }
13
+ * | { numRows: number, values: Arrayish; }
14
+ * } DataColumns
15
+ */
16
+
17
+ /**
18
+ * Convert input data to a set of column arrays.
19
+ * @param {any} data The input data.
20
+ * @returns {DataColumns} An object with named column arrays.
21
+ */
22
+ export function toDataColumns(data) {
23
+ return isArrowTable(data)
24
+ ? arrowToColumns(data)
25
+ : arrayToColumns(data);
26
+ }
27
+
28
+ /**
29
+ * Convert an Arrow table to a set of column arrays.
30
+ * @param {import('apache-arrow').Table} data An Apache Arrow Table.
31
+ * @returns {DataColumns} An object with named column arrays.
32
+ */
33
+ function arrowToColumns(data) {
34
+ const { numRows, numCols, schema: { fields } } = data;
35
+ const columns = {};
36
+
37
+ for (let col = 0; col < numCols; ++col) {
38
+ const name = fields[col].name;
39
+ if (columns[name]) {
40
+ console.warn(`Redundant column name "${name}". Skipping...`);
41
+ } else {
42
+ columns[name] = convertArrowColumn(data.getChildAt(col));
43
+ }
44
+ }
45
+
46
+ return { numRows, columns };
47
+ }
48
+
49
+ /**
50
+ * Convert an array of values to a set of column arrays.
51
+ * If the array values are objects, build out named columns.
52
+ * We use the keys of the first object as the column names.
53
+ * Otherwise, use a special "values" array.
54
+ * @param {object[]} data An array of data objects.
55
+ * @returns {DataColumns} An object with named column arrays.
56
+ */
57
+ function arrayToColumns(data) {
58
+ const numRows = data.length;
59
+ if (typeof data[0] === 'object') {
60
+ const names = numRows ? Object.keys(data[0]) : [];
61
+ const columns = {};
62
+ if (names.length > 0) {
63
+ names.forEach(name => {
64
+ columns[name] = data.map(d => d[name]);
65
+ });
66
+ }
67
+ return { numRows, columns };
68
+ } else {
69
+ return { numRows, values: data };
70
+ }
71
+ }