@uwdata/mosaic-plot 0.8.0 → 0.10.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.
- package/dist/mosaic-plot.js +2150 -1439
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +6 -6
- package/src/index.js +1 -0
- package/src/interactors/Highlight.js +6 -3
- package/src/interactors/Interval1D.js +8 -8
- package/src/interactors/Interval2D.js +10 -15
- package/src/interactors/Nearest.js +79 -33
- package/src/interactors/PanZoom.js +4 -7
- package/src/interactors/Toggle.js +19 -28
- package/src/legend.js +26 -8
- package/src/marks/Density1DMark.js +1 -1
- package/src/marks/ErrorBarMark.js +50 -0
- package/src/marks/Grid2DMark.js +2 -2
- package/src/marks/HexbinMark.js +39 -49
- package/src/marks/Mark.js +28 -21
- package/src/marks/RasterMark.js +9 -3
- package/src/marks/RasterTileMark.js +8 -2
- package/src/marks/RegressionMark.js +2 -2
- package/src/marks/util/is-constant-option.js +2 -1
- package/src/marks/util/permute.js +10 -0
- package/src/marks/util/stats.js +88 -0
- package/src/plot-attributes.js +1 -0
- package/src/plot-renderer.js +21 -22
- package/src/plot.js +14 -2
- package/src/transforms/bin-step.js +43 -0
- package/src/transforms/bin.js +40 -48
- package/src/transforms/time-interval.js +53 -0
- package/src/marks/util/to-data-columns.js +0 -71
package/src/marks/HexbinMark.js
CHANGED
|
@@ -25,72 +25,62 @@ export class HexbinMark extends Mark {
|
|
|
25
25
|
if (this.hasOwnData()) return null;
|
|
26
26
|
const { plot, binWidth, channels, source } = this;
|
|
27
27
|
|
|
28
|
-
// get x / y extents, may update plot domainX / domainY
|
|
29
|
-
const [x1, x2] = extentX(this, filter);
|
|
30
|
-
const [y1, y2] = extentY(this, filter);
|
|
31
|
-
|
|
32
|
-
// Adjust screen-space coordinates by top/left
|
|
33
|
-
// margins as this is what Observable Plot does.
|
|
34
|
-
// TODO use zero margins when faceted?
|
|
35
|
-
const ox = 0.5 - plot.getAttribute('marginLeft');
|
|
36
|
-
const oy = 0 - plot.getAttribute('marginTop');
|
|
37
|
-
const dx = `${binWidth}::DOUBLE`;
|
|
38
|
-
const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
|
|
39
|
-
const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
|
|
40
|
-
const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
|
|
41
|
-
|
|
42
28
|
// Extract channel information, update top-level query
|
|
43
29
|
// and extract dependent columns for aggregates
|
|
44
30
|
let x, y;
|
|
45
|
-
const
|
|
31
|
+
const dims = new Set;
|
|
46
32
|
const cols = {};
|
|
47
|
-
let orderby;
|
|
48
33
|
for (const c of channels) {
|
|
49
34
|
if (c.channel === 'orderby') {
|
|
50
|
-
|
|
35
|
+
// ignore ordering, as we will aggregate
|
|
51
36
|
} else if (c.channel === 'x') {
|
|
52
37
|
x = c;
|
|
53
38
|
} else if (c.channel === 'y') {
|
|
54
39
|
y = c;
|
|
55
40
|
} else if (Object.hasOwn(c, 'field')) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
41
|
+
const { as, field } = c;
|
|
42
|
+
cols[as] = field;
|
|
43
|
+
if (!field.aggregate) {
|
|
44
|
+
dims.add(as);
|
|
59
45
|
}
|
|
60
46
|
}
|
|
61
47
|
}
|
|
62
48
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const q = Query.select({
|
|
67
|
-
[x.as]: sql`${x1}::DOUBLE + ((x + 0.5 * (y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
|
|
68
|
-
[y.as]: sql`${y2}::DOUBLE - (y * ${dy} + ${oy})::DOUBLE / ${yr}`,
|
|
69
|
-
...cols
|
|
70
|
-
}).groupby('x', 'y');
|
|
71
|
-
|
|
72
|
-
if (orderby) q.orderby(orderby);
|
|
49
|
+
// get x / y extents, may update plot xDomain / yDomain
|
|
50
|
+
const [x1, x2] = extentX(this, filter);
|
|
51
|
+
const [y1, y2] = extentY(this, filter);
|
|
73
52
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
const
|
|
53
|
+
// Adjust screen-space coordinates by top/left
|
|
54
|
+
// margins as this is what Observable Plot does.
|
|
55
|
+
const ox = 0.5 - plot.getAttribute('marginLeft');
|
|
56
|
+
const oy = 0 - plot.getAttribute('marginTop');
|
|
57
|
+
const dx = `${binWidth}::DOUBLE`;
|
|
58
|
+
const dy = `${binWidth * (1.5 / Math.sqrt(3))}::DOUBLE`;
|
|
59
|
+
const xr = `${plot.innerWidth() / (x2 - x1)}::DOUBLE`;
|
|
60
|
+
const yr = `${plot.innerHeight() / (y2 - y1)}::DOUBLE`;
|
|
77
61
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
px: sql`(${xx} - ${ox}) / ${dx} - 0.5 * (pj & 1)`,
|
|
85
|
-
pi: sql`ROUND(px)::INTEGER`,
|
|
86
|
-
tt: sql`ABS(py-pj) * 3 > 1 AND (px-pi)**2 + (py-pj)**2 > (px - pi - 0.5 * CASE WHEN px < pi THEN -1 ELSE 1 END)**2 + (py - pj - CASE WHEN py < pj THEN -1 ELSE 1 END)**2`,
|
|
87
|
-
x: sql`CASE WHEN tt THEN (pi + (CASE WHEN px < pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE pi END`,
|
|
88
|
-
y: sql`CASE WHEN tt THEN (pj + CASE WHEN py < pj THEN -1 ELSE 1 END)::INTEGER ELSE pj END`
|
|
62
|
+
// Top-level query maps from screen space back to data values.
|
|
63
|
+
// Doing so ensures that Plot generates correct data-driven scales.
|
|
64
|
+
return Query.select({
|
|
65
|
+
[x.as]: sql`${x1}::DOUBLE + ((_x + 0.5 * (_y & 1)) * ${dx} + ${ox})::DOUBLE / ${xr}`,
|
|
66
|
+
[y.as]: sql`${y2}::DOUBLE - (_y * ${dy} + ${oy})::DOUBLE / ${yr}`,
|
|
67
|
+
...cols
|
|
89
68
|
})
|
|
90
|
-
.
|
|
91
|
-
.from(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
69
|
+
.groupby('_x', '_y', ...dims)
|
|
70
|
+
.from(
|
|
71
|
+
// Subquery performs hex binning in screen space and also passes
|
|
72
|
+
// original columns through (the DB should optimize this).
|
|
73
|
+
Query.select({
|
|
74
|
+
_py: sql`(${yr} * (${y2}::DOUBLE - ${y.field}) - ${oy}) / ${dy}`,
|
|
75
|
+
_pj: sql`ROUND(_py)::INTEGER`,
|
|
76
|
+
_px: sql`(${xr} * (${x.field} - ${x1}::DOUBLE) - ${ox}) / ${dx} - 0.5 * (_pj & 1)`,
|
|
77
|
+
_pi: sql`ROUND(_px)::INTEGER`,
|
|
78
|
+
_tt: sql`ABS(_py-_pj) * 3 > 1 AND (_px-_pi)**2 + (_py-_pj)**2 > (_px - _pi - 0.5 * CASE WHEN _px < _pi THEN -1 ELSE 1 END)**2 + (_py - _pj - CASE WHEN _py < _pj THEN -1 ELSE 1 END)**2`,
|
|
79
|
+
_x: sql`CASE WHEN _tt THEN (_pi + (CASE WHEN _px < _pi THEN -0.5 ELSE 0.5 END) + (CASE WHEN _pj & 1 <> 0 THEN 0.5 ELSE -0.5 END))::INTEGER ELSE _pi END`,
|
|
80
|
+
_y: sql`CASE WHEN _tt THEN (_pj + CASE WHEN _py < _pj THEN -1 ELSE 1 END)::INTEGER ELSE _pj END`
|
|
81
|
+
}, '*')
|
|
82
|
+
.from(source.table)
|
|
83
|
+
.where(isNotNull(x.field), isNotNull(y.field), filter)
|
|
84
|
+
);
|
|
95
85
|
}
|
|
96
86
|
}
|
package/src/marks/Mark.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { MosaicClient } from '@uwdata/mosaic-core';
|
|
1
|
+
import { MosaicClient, toDataColumns } from '@uwdata/mosaic-core';
|
|
2
2
|
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 { toDataColumns } from './util/to-data-columns.js';
|
|
7
6
|
import { Transform } from '../symbols.js';
|
|
8
7
|
|
|
9
8
|
const isColorChannel = channel => channel === 'stroke' || channel === 'fill';
|
|
@@ -91,7 +90,7 @@ export class Mark extends MosaicClient {
|
|
|
91
90
|
|
|
92
91
|
/**
|
|
93
92
|
* @param {import('../plot.js').Plot} plot The plot.
|
|
94
|
-
* @param {number} index
|
|
93
|
+
* @param {number} index
|
|
95
94
|
*/
|
|
96
95
|
setPlot(plot, index) {
|
|
97
96
|
this.plot = plot;
|
|
@@ -181,24 +180,8 @@ export class Mark extends MosaicClient {
|
|
|
181
180
|
* @returns {object[]}
|
|
182
181
|
*/
|
|
183
182
|
plotSpecs() {
|
|
184
|
-
const { type, detail, channels } = this;
|
|
185
|
-
|
|
186
|
-
const { numRows: length, values, columns } = this.data || {};
|
|
187
|
-
|
|
188
|
-
// populate plot specification options
|
|
189
|
-
const options = {};
|
|
190
|
-
const side = {};
|
|
191
|
-
for (const c of channels) {
|
|
192
|
-
const obj = detail.has(c.channel) ? side : options;
|
|
193
|
-
obj[c.channel] = channelOption(c, columns);
|
|
194
|
-
}
|
|
195
|
-
if (detail.size) options.channels = side;
|
|
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;
|
|
183
|
+
const { type, data, detail, channels } = this;
|
|
184
|
+
return markPlotSpec(type, detail, channels, data);
|
|
202
185
|
}
|
|
203
186
|
}
|
|
204
187
|
|
|
@@ -258,3 +241,27 @@ export function markQuery(channels, table, skip = []) {
|
|
|
258
241
|
|
|
259
242
|
return q;
|
|
260
243
|
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Generate an array of Plot mark specifications.
|
|
248
|
+
* @returns {object[]}
|
|
249
|
+
*/
|
|
250
|
+
export function markPlotSpec(type, detail, channels, data, options = {}) {
|
|
251
|
+
// @ts-ignore
|
|
252
|
+
const { numRows: length, values, columns } = data ?? {};
|
|
253
|
+
|
|
254
|
+
// populate plot specification options
|
|
255
|
+
const side = {};
|
|
256
|
+
for (const c of channels) {
|
|
257
|
+
const obj = detail.has(c.channel) ? side : options;
|
|
258
|
+
obj[c.channel] = channelOption(c, columns);
|
|
259
|
+
}
|
|
260
|
+
if (detail.size) options.channels = side;
|
|
261
|
+
|
|
262
|
+
// if provided raw source values (not objects) pass as-is
|
|
263
|
+
// otherwise we pass columnar data directy in the options
|
|
264
|
+
const specData = values ?? (data ? { length } : null);
|
|
265
|
+
const spec = [{ type, data: specData, options }];
|
|
266
|
+
return spec;
|
|
267
|
+
}
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -2,6 +2,7 @@ import { ascending } from 'd3';
|
|
|
2
2
|
import { scale } from '@observablehq/plot';
|
|
3
3
|
import { gridDomainContinuous, gridDomainDiscrete } from './util/grid.js';
|
|
4
4
|
import { isColor } from './util/is-color.js';
|
|
5
|
+
import { indices, permute } from './util/permute.js';
|
|
5
6
|
import { alphaScheme, alphaConstant, colorConstant, colorCategory, colorScheme, createCanvas } from './util/raster.js';
|
|
6
7
|
import { DENSITY, Grid2DMark } from './Grid2DMark.js';
|
|
7
8
|
import { Fixed, Transient } from '../symbols.js';
|
|
@@ -46,13 +47,18 @@ export class RasterMark extends Grid2DMark {
|
|
|
46
47
|
const alphaData = columns[alphaProp] ?? [];
|
|
47
48
|
const colorData = columns[colorProp] ?? [];
|
|
48
49
|
|
|
50
|
+
// determine raster order
|
|
51
|
+
const idx = numRows > 1 && colorProp && this.groupby?.includes(colorProp)
|
|
52
|
+
? permute(colorData, this.plot.getAttribute('colorDomain'))
|
|
53
|
+
: indices(numRows);
|
|
54
|
+
|
|
49
55
|
// generate rasters
|
|
50
56
|
this.data = {
|
|
51
57
|
numRows,
|
|
52
58
|
columns: {
|
|
53
59
|
src: Array.from({ length: numRows }, (_, i) => {
|
|
54
|
-
color?.(img.data, w, h, colorData[i]);
|
|
55
|
-
alpha?.(img.data, w, h, alphaData[i]);
|
|
60
|
+
color?.(img.data, w, h, colorData[idx[i]]);
|
|
61
|
+
alpha?.(img.data, w, h, alphaData[idx[i]]);
|
|
56
62
|
ctx.putImageData(img, 0, 0);
|
|
57
63
|
return canvas.toDataURL();
|
|
58
64
|
})
|
|
@@ -196,7 +202,7 @@ function colorScale(mark, prop) {
|
|
|
196
202
|
const domainFixed = domainAttr === Fixed;
|
|
197
203
|
const domainTransient = domainAttr?.[Transient];
|
|
198
204
|
const domain = (!domainFixed && !domainTransient && domainAttr) || (
|
|
199
|
-
flat ? data.sort(ascending)
|
|
205
|
+
flat ? data.slice().sort(ascending)
|
|
200
206
|
: discrete ? gridDomainDiscrete(data)
|
|
201
207
|
: gridDomainContinuous(data)
|
|
202
208
|
);
|
|
@@ -2,6 +2,7 @@ import { coordinator } from '@uwdata/mosaic-core';
|
|
|
2
2
|
import { Query, count, isBetween, lt, lte, neq, sql, sum } from '@uwdata/mosaic-sql';
|
|
3
3
|
import { binExpr } from './util/bin-expr.js';
|
|
4
4
|
import { extentX, extentY } from './util/extent.js';
|
|
5
|
+
import { indices, permute } from './util/permute.js';
|
|
5
6
|
import { createCanvas } from './util/raster.js';
|
|
6
7
|
import { Grid2DMark } from './Grid2DMark.js';
|
|
7
8
|
import { rasterEncoding } from './RasterMark.js';
|
|
@@ -181,13 +182,18 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
181
182
|
const alphaData = columns[alphaProp] ?? [];
|
|
182
183
|
const colorData = columns[colorProp] ?? [];
|
|
183
184
|
|
|
185
|
+
// determine raster order
|
|
186
|
+
const idx = numRows > 1 && colorProp && this.groupby?.includes(colorProp)
|
|
187
|
+
? permute(colorData, this.plot.getAttribute('colorDomain'))
|
|
188
|
+
: indices(numRows);
|
|
189
|
+
|
|
184
190
|
// generate rasters
|
|
185
191
|
this.data = {
|
|
186
192
|
numRows,
|
|
187
193
|
columns: {
|
|
188
194
|
src: Array.from({ length: numRows }, (_, i) => {
|
|
189
|
-
color?.(img.data, w, h, colorData[i]);
|
|
190
|
-
alpha?.(img.data, w, h, alphaData[i]);
|
|
195
|
+
color?.(img.data, w, h, colorData[idx[i]]);
|
|
196
|
+
alpha?.(img.data, w, h, alphaData[idx[i]]);
|
|
191
197
|
ctx.putImageData(img, 0, 0);
|
|
192
198
|
return canvas.toDataURL();
|
|
193
199
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { range } from 'd3';
|
|
2
|
+
import { toDataColumns } from '@uwdata/mosaic-core';
|
|
2
3
|
import {
|
|
3
4
|
Query, max, min, castDouble, isNotNull,
|
|
4
5
|
regrIntercept, regrSlope, regrCount,
|
|
@@ -7,7 +8,6 @@ import {
|
|
|
7
8
|
import { qt } from './util/stats.js';
|
|
8
9
|
import { Mark, channelOption } from './Mark.js';
|
|
9
10
|
import { handleParam } from './util/handle-param.js';
|
|
10
|
-
import { toDataColumns } from './util/to-data-columns.js';
|
|
11
11
|
|
|
12
12
|
export class RegressionMark extends Mark {
|
|
13
13
|
constructor(source, options) {
|
|
@@ -118,7 +118,7 @@ function concat(a, b) {
|
|
|
118
118
|
|
|
119
119
|
function linePoints(fit) {
|
|
120
120
|
// eslint-disable-next-line no-unused-vars
|
|
121
|
-
const { x0, x1, xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
|
|
121
|
+
const { x0 = [], x1 = [], xm, intercept, slope, n, ssx, ssy, ...rest } = fit.columns;
|
|
122
122
|
const predict = (x, i) => intercept[i] + x * slope[i];
|
|
123
123
|
const x = concat(x0, x1);
|
|
124
124
|
const y = concat(x0.map(predict), x1.map(predict));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function indices(length) {
|
|
2
|
+
return Array.from({ length }, (_, i) => i);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function permute(data, order) {
|
|
6
|
+
const ord = order.reduce((acc, val, i) => (acc[val] = i, acc), {});
|
|
7
|
+
const idx = indices(data.length);
|
|
8
|
+
idx.sort((a, b) => ord[data[a]] - ord[data[b]]);
|
|
9
|
+
return idx;
|
|
10
|
+
}
|
package/src/marks/util/stats.js
CHANGED
|
@@ -163,3 +163,91 @@ export function qt(p, dof) {
|
|
|
163
163
|
x = Math.sqrt((dof * (1 - x)) / x);
|
|
164
164
|
return p > 0.5 ? x : -x;
|
|
165
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Approximate inverse error function.
|
|
169
|
+
* @param {number} x
|
|
170
|
+
* @returns {number}
|
|
171
|
+
*/
|
|
172
|
+
export function erfinv(x) {
|
|
173
|
+
// Implementation from "Approximating the erfinv function" by Mike Giles,
|
|
174
|
+
// GPU Computing Gems, volume 2, 2010.
|
|
175
|
+
// Ported from Apache Commons Math, http://www.apache.org/licenses/LICENSE-2.0
|
|
176
|
+
|
|
177
|
+
// beware that the logarithm argument must be
|
|
178
|
+
// computed as (1.0 - x) * (1.0 + x),
|
|
179
|
+
// it must NOT be simplified as 1.0 - x * x as this
|
|
180
|
+
// would induce rounding errors near the boundaries +/-1
|
|
181
|
+
let w = - Math.log((1 - x) * (1 + x));
|
|
182
|
+
let p;
|
|
183
|
+
|
|
184
|
+
if (w < 6.25) {
|
|
185
|
+
w -= 3.125;
|
|
186
|
+
p = -3.6444120640178196996e-21;
|
|
187
|
+
p = -1.685059138182016589e-19 + p * w;
|
|
188
|
+
p = 1.2858480715256400167e-18 + p * w;
|
|
189
|
+
p = 1.115787767802518096e-17 + p * w;
|
|
190
|
+
p = -1.333171662854620906e-16 + p * w;
|
|
191
|
+
p = 2.0972767875968561637e-17 + p * w;
|
|
192
|
+
p = 6.6376381343583238325e-15 + p * w;
|
|
193
|
+
p = -4.0545662729752068639e-14 + p * w;
|
|
194
|
+
p = -8.1519341976054721522e-14 + p * w;
|
|
195
|
+
p = 2.6335093153082322977e-12 + p * w;
|
|
196
|
+
p = -1.2975133253453532498e-11 + p * w;
|
|
197
|
+
p = -5.4154120542946279317e-11 + p * w;
|
|
198
|
+
p = 1.051212273321532285e-09 + p * w;
|
|
199
|
+
p = -4.1126339803469836976e-09 + p * w;
|
|
200
|
+
p = -2.9070369957882005086e-08 + p * w;
|
|
201
|
+
p = 4.2347877827932403518e-07 + p * w;
|
|
202
|
+
p = -1.3654692000834678645e-06 + p * w;
|
|
203
|
+
p = -1.3882523362786468719e-05 + p * w;
|
|
204
|
+
p = 0.0001867342080340571352 + p * w;
|
|
205
|
+
p = -0.00074070253416626697512 + p * w;
|
|
206
|
+
p = -0.0060336708714301490533 + p * w;
|
|
207
|
+
p = 0.24015818242558961693 + p * w;
|
|
208
|
+
p = 1.6536545626831027356 + p * w;
|
|
209
|
+
} else if (w < 16.0) {
|
|
210
|
+
w = Math.sqrt(w) - 3.25;
|
|
211
|
+
p = 2.2137376921775787049e-09;
|
|
212
|
+
p = 9.0756561938885390979e-08 + p * w;
|
|
213
|
+
p = -2.7517406297064545428e-07 + p * w;
|
|
214
|
+
p = 1.8239629214389227755e-08 + p * w;
|
|
215
|
+
p = 1.5027403968909827627e-06 + p * w;
|
|
216
|
+
p = -4.013867526981545969e-06 + p * w;
|
|
217
|
+
p = 2.9234449089955446044e-06 + p * w;
|
|
218
|
+
p = 1.2475304481671778723e-05 + p * w;
|
|
219
|
+
p = -4.7318229009055733981e-05 + p * w;
|
|
220
|
+
p = 6.8284851459573175448e-05 + p * w;
|
|
221
|
+
p = 2.4031110387097893999e-05 + p * w;
|
|
222
|
+
p = -0.0003550375203628474796 + p * w;
|
|
223
|
+
p = 0.00095328937973738049703 + p * w;
|
|
224
|
+
p = -0.0016882755560235047313 + p * w;
|
|
225
|
+
p = 0.0024914420961078508066 + p * w;
|
|
226
|
+
p = -0.0037512085075692412107 + p * w;
|
|
227
|
+
p = 0.005370914553590063617 + p * w;
|
|
228
|
+
p = 1.0052589676941592334 + p * w;
|
|
229
|
+
p = 3.0838856104922207635 + p * w;
|
|
230
|
+
} else if (Number.isFinite(w)) {
|
|
231
|
+
w = Math.sqrt(w) - 5.0;
|
|
232
|
+
p = -2.7109920616438573243e-11;
|
|
233
|
+
p = -2.5556418169965252055e-10 + p * w;
|
|
234
|
+
p = 1.5076572693500548083e-09 + p * w;
|
|
235
|
+
p = -3.7894654401267369937e-09 + p * w;
|
|
236
|
+
p = 7.6157012080783393804e-09 + p * w;
|
|
237
|
+
p = -1.4960026627149240478e-08 + p * w;
|
|
238
|
+
p = 2.9147953450901080826e-08 + p * w;
|
|
239
|
+
p = -6.7711997758452339498e-08 + p * w;
|
|
240
|
+
p = 2.2900482228026654717e-07 + p * w;
|
|
241
|
+
p = -9.9298272942317002539e-07 + p * w;
|
|
242
|
+
p = 4.5260625972231537039e-06 + p * w;
|
|
243
|
+
p = -1.9681778105531670567e-05 + p * w;
|
|
244
|
+
p = 7.5995277030017761139e-05 + p * w;
|
|
245
|
+
p = -0.00021503011930044477347 + p * w;
|
|
246
|
+
p = -0.00013871931833623122026 + p * w;
|
|
247
|
+
p = 1.0103004648645343977 + p * w;
|
|
248
|
+
p = 4.8499064014085844221 + p * w;
|
|
249
|
+
} else {
|
|
250
|
+
p = Infinity;
|
|
251
|
+
}
|
|
252
|
+
return p * x;
|
|
253
|
+
}
|
package/src/plot-attributes.js
CHANGED
package/src/plot-renderer.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as Plot from '@observablehq/plot';
|
|
2
2
|
import { setAttributes } from './plot-attributes.js';
|
|
3
3
|
import { Fixed } from './symbols.js';
|
|
4
|
-
import { isArrowTable } from '@uwdata/mosaic-core';
|
|
5
4
|
|
|
6
5
|
const OPTIONS_ONLY_MARKS = new Set([
|
|
7
6
|
'frame',
|
|
@@ -10,6 +9,19 @@ const OPTIONS_ONLY_MARKS = new Set([
|
|
|
10
9
|
'graticule'
|
|
11
10
|
]);
|
|
12
11
|
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
const SELECT_TRANSFORMS = new Map([
|
|
14
|
+
['first', Plot.selectFirst],
|
|
15
|
+
['last', Plot.selectLast],
|
|
16
|
+
['maxX', Plot.selectMaxX],
|
|
17
|
+
['maxY', Plot.selectMaxY],
|
|
18
|
+
['minX', Plot.selectMinX],
|
|
19
|
+
['minY', Plot.selectMinY],
|
|
20
|
+
['nearest', Plot.pointer],
|
|
21
|
+
['nearestX', Plot.pointerX],
|
|
22
|
+
['nearestXY', Plot.pointerY]
|
|
23
|
+
]);
|
|
24
|
+
|
|
13
25
|
// construct Plot output
|
|
14
26
|
// see https://github.com/observablehq/plot
|
|
15
27
|
export async function plotRenderer(plot) {
|
|
@@ -24,27 +36,13 @@ export async function plotRenderer(plot) {
|
|
|
24
36
|
const indices = [];
|
|
25
37
|
for (const mark of marks) {
|
|
26
38
|
for (const { type, data, options } of mark.plotSpecs()) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let val = v;
|
|
35
|
-
if (typeof v === 'string') {
|
|
36
|
-
val = data.getChild(v) ?? v;
|
|
37
|
-
} else if (typeof v === 'object') {
|
|
38
|
-
const value = data.getChild(v.value);
|
|
39
|
-
val = value ? {value} : v;
|
|
40
|
-
}
|
|
41
|
-
return [k, val]
|
|
42
|
-
})
|
|
43
|
-
);
|
|
44
|
-
spec.marks.push(Plot[type]({length: data.numRows}, opts));
|
|
45
|
-
} else {
|
|
46
|
-
spec.marks.push(Plot[type](data, options));
|
|
47
|
-
}
|
|
39
|
+
// prepare mark options
|
|
40
|
+
const { select, ...rest } = options;
|
|
41
|
+
const opt = SELECT_TRANSFORMS.get(select)?.(rest) ?? rest;
|
|
42
|
+
const arg = OPTIONS_ONLY_MARKS.has(type) ? [opt] : [data, opt];
|
|
43
|
+
|
|
44
|
+
// instantiate Plot mark and add to spec
|
|
45
|
+
spec.marks.push(Plot[type](...arg));
|
|
48
46
|
indices.push(mark.index);
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -165,6 +163,7 @@ function annotateMarks(svg, indices) {
|
|
|
165
163
|
}
|
|
166
164
|
|
|
167
165
|
function getType(data, channel) {
|
|
166
|
+
if (!data) return;
|
|
168
167
|
const { columns } = data;
|
|
169
168
|
const col = columns[channel] ?? columns[channel+'1'] ?? columns[channel+'2'];
|
|
170
169
|
if (col) {
|
package/src/plot.js
CHANGED
|
@@ -42,8 +42,9 @@ export class Plot {
|
|
|
42
42
|
innerHeight(defaultValue = 400) {
|
|
43
43
|
const { top, bottom } = this.margins();
|
|
44
44
|
let h = this.getAttribute('height');
|
|
45
|
-
if (h == null
|
|
46
|
-
|
|
45
|
+
if (h == null) {
|
|
46
|
+
// TODO could apply more nuanced logic here?
|
|
47
|
+
h = maybeAspectRatio(this, top, bottom) || defaultValue;
|
|
47
48
|
this.setAttribute('height', h, { silent: true });
|
|
48
49
|
}
|
|
49
50
|
return h - top - bottom;
|
|
@@ -159,3 +160,14 @@ export class Plot {
|
|
|
159
160
|
this.legends.push({ legend, include });
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
|
|
164
|
+
function maybeAspectRatio(plot, top, bottom) {
|
|
165
|
+
const ar = plot.getAttribute('aspectRatio');
|
|
166
|
+
if (ar == null) return;
|
|
167
|
+
const x = plot.getAttribute('xDomain');
|
|
168
|
+
const y = plot.getAttribute('yDomain');
|
|
169
|
+
if (!x || !y) return;
|
|
170
|
+
const dx = Math.abs(x[1] - x[0]);
|
|
171
|
+
const dy = Math.abs(y[1] - y[0]);
|
|
172
|
+
return dy * plot.innerWidth() / (ar * dx) + top + bottom;
|
|
173
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function binStep(span, steps, minstep = 0, logb = Math.LN10) {
|
|
2
|
+
let v;
|
|
3
|
+
|
|
4
|
+
const level = Math.ceil(Math.log(steps) / logb);
|
|
5
|
+
let step = Math.max(
|
|
6
|
+
minstep,
|
|
7
|
+
Math.pow(10, Math.round(Math.log(span) / logb) - level)
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
// increase step size if too many bins
|
|
11
|
+
while (Math.ceil(span / step) > steps) { step *= 10; }
|
|
12
|
+
|
|
13
|
+
// decrease step size if allowed
|
|
14
|
+
const div = [5, 2];
|
|
15
|
+
for (let i = 0, n = div.length; i < n; ++i) {
|
|
16
|
+
v = step / div[i];
|
|
17
|
+
if (v >= minstep && span / v <= steps) step = v;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return step;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function bins(min, max, options) {
|
|
24
|
+
let { step, steps, minstep = 0, nice = true } = options;
|
|
25
|
+
|
|
26
|
+
if (nice !== false) {
|
|
27
|
+
// use span to determine step size
|
|
28
|
+
const span = max - min;
|
|
29
|
+
const logb = Math.LN10;
|
|
30
|
+
step = step || binStep(span, steps || 25, minstep, logb);
|
|
31
|
+
|
|
32
|
+
// adjust min/max relative to step
|
|
33
|
+
let v = Math.log(step);
|
|
34
|
+
const precision = v >= 0 ? 0 : ~~(-v / logb) + 1;
|
|
35
|
+
const eps = Math.pow(10, -precision - 1);
|
|
36
|
+
v = Math.floor(min / step + eps) * step;
|
|
37
|
+
min = min < v ? v - step : v;
|
|
38
|
+
max = Math.ceil(max / step) * step;
|
|
39
|
+
steps = Math.round((max - min) / step);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { min, max, steps };
|
|
43
|
+
}
|
package/src/transforms/bin.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { dateBin } from '@uwdata/mosaic-sql';
|
|
1
2
|
import { Transform } from '../symbols.js';
|
|
2
3
|
import { channelScale } from '../marks/util/channel-scale.js';
|
|
4
|
+
import { bins } from './bin-step.js';
|
|
5
|
+
import { timeInterval } from './time-interval.js';
|
|
3
6
|
|
|
4
|
-
const EXTENT = new Set([
|
|
7
|
+
const EXTENT = new Set([
|
|
8
|
+
'rectY-x', 'rectX-y', 'rect-x', 'rect-y', 'ruleY-x', 'ruleX-y'
|
|
9
|
+
]);
|
|
5
10
|
|
|
6
|
-
export function
|
|
11
|
+
export function hasExtent(mark, channel) {
|
|
12
|
+
return EXTENT.has(`${mark.type}-${channel}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function bin(field, options = {}) {
|
|
7
16
|
const fn = (mark, channel) => {
|
|
8
|
-
if (
|
|
17
|
+
if (hasExtent(mark, channel)) {
|
|
9
18
|
return {
|
|
10
19
|
[`${channel}1`]: binField(mark, channel, field, options),
|
|
11
20
|
[`${channel}2`]: binField(mark, channel, field, { ...options, offset: 1 })
|
|
@@ -24,56 +33,39 @@ function binField(mark, channel, column, options) {
|
|
|
24
33
|
return {
|
|
25
34
|
column,
|
|
26
35
|
label: column,
|
|
27
|
-
get stats() { return { column, stats: ['min', 'max'] }; },
|
|
28
36
|
get columns() { return [column]; },
|
|
29
37
|
get basis() { return column; },
|
|
38
|
+
get stats() { return { column, stats: ['min', 'max'] }; },
|
|
30
39
|
toString() {
|
|
31
|
-
const {
|
|
32
|
-
const {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
const { type, min, max } = mark.channelField(channel);
|
|
41
|
+
const { interval: i, steps, offset = 0 } = options;
|
|
42
|
+
const interval = i ?? (
|
|
43
|
+
type === 'date' || hasTimeScale(mark, channel) ? 'date' : 'number'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (interval === 'number') {
|
|
47
|
+
// perform number binning
|
|
48
|
+
const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
|
|
49
|
+
const b = bins(apply(min), apply(max), options);
|
|
50
|
+
const col = sqlApply(column);
|
|
51
|
+
const base = b.min === 0 ? col : `(${col} - ${b.min})`;
|
|
52
|
+
const alpha = `${(b.max - b.min) / b.steps}::DOUBLE`;
|
|
53
|
+
const off = offset ? `${offset} + ` : '';
|
|
54
|
+
const expr = `${b.min} + ${alpha} * (${off}FLOOR(${base} / ${alpha}))`;
|
|
55
|
+
return `${sqlInvert(expr)}`;
|
|
56
|
+
} else {
|
|
57
|
+
// perform date/time binning
|
|
58
|
+
const { interval: unit, step = 1 } = interval === 'date'
|
|
59
|
+
? timeInterval(min, max, steps || 40)
|
|
60
|
+
: options;
|
|
61
|
+
const off = offset ? ` + INTERVAL ${offset * step} ${unit}` : '';
|
|
62
|
+
return `(${dateBin(column, unit, step)}${off})`;
|
|
63
|
+
}
|
|
40
64
|
}
|
|
41
65
|
};
|
|
42
66
|
}
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (nice !== false) {
|
|
48
|
-
// use span to determine step size
|
|
49
|
-
const span = max - min;
|
|
50
|
-
const maxb = steps;
|
|
51
|
-
const logb = Math.LN10;
|
|
52
|
-
const level = Math.ceil(Math.log(maxb) / logb);
|
|
53
|
-
let step = Math.max(
|
|
54
|
-
minstep,
|
|
55
|
-
Math.pow(10, Math.round(Math.log(span) / logb) - level)
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
// increase step size if too many bins
|
|
59
|
-
while (Math.ceil(span / step) > maxb) { step *= 10; }
|
|
60
|
-
|
|
61
|
-
// decrease step size if allowed
|
|
62
|
-
const div = [5, 2];
|
|
63
|
-
let v;
|
|
64
|
-
for (let i = 0, n = div.length; i < n; ++i) {
|
|
65
|
-
v = step / div[i];
|
|
66
|
-
if (v >= minstep && span / v <= maxb) step = v;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
v = Math.log(step);
|
|
70
|
-
const precision = v >= 0 ? 0 : ~~(-v / logb) + 1;
|
|
71
|
-
const eps = Math.pow(10, -precision - 1);
|
|
72
|
-
v = Math.floor(min / step + eps) * step;
|
|
73
|
-
min = min < v ? v - step : v;
|
|
74
|
-
max = Math.ceil(max / step) * step;
|
|
75
|
-
steps = Math.round((max - min) / step);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return { min, max, steps };
|
|
68
|
+
function hasTimeScale(mark, channel) {
|
|
69
|
+
const scale = mark.plot.getAttribute(`${channel}Scale`);
|
|
70
|
+
return scale === 'utc' || scale === 'time';
|
|
79
71
|
}
|