@uwdata/mosaic-plot 0.6.1 → 0.7.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/LICENSE +19 -0
- package/dist/mosaic-plot.js +395 -352
- package/dist/mosaic-plot.min.js +11 -11
- package/package.json +4 -4
- package/src/interactors/Interval1D.js +1 -1
- package/src/interactors/Interval2D.js +2 -2
- package/src/interactors/PanZoom.js +2 -2
- package/src/interactors/util/get-field.js +2 -2
- package/src/legend.js +4 -2
- package/src/marks/ConnectedMark.js +5 -8
- package/src/marks/DenseLineMark.js +11 -4
- package/src/marks/Grid2DMark.js +1 -1
- package/src/marks/Mark.js +45 -24
- package/src/marks/RasterMark.js +1 -1
- package/src/marks/RasterTileMark.js +1 -1
- package/src/marks/util/channel-scale.js +1 -2
- package/src/marks/util/extent.js +3 -5
- package/src/marks/util/grid.js +10 -12
- package/src/marks/util/interpolate.js +9 -0
- package/src/marks/util/is-color.js +3 -0
- package/src/marks/util/to-data-array.js +2 -2
- package/src/plot-renderer.js +9 -13
- package/src/transforms/bin.js +2 -2
- package/src/marks/util/arrow.js +0 -55
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-plot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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.13",
|
|
32
|
-
"@uwdata/mosaic-core": "^0.
|
|
33
|
-
"@uwdata/mosaic-sql": "^0.
|
|
32
|
+
"@uwdata/mosaic-core": "^0.7.0",
|
|
33
|
+
"@uwdata/mosaic-sql": "^0.7.0",
|
|
34
34
|
"d3": "^7.8.5",
|
|
35
35
|
"isoformat": "^0.2.1"
|
|
36
36
|
},
|
|
37
|
-
"gitHead": "
|
|
37
|
+
"gitHead": "4680b922f15579b7b527f31507ed71a12230ec35"
|
|
38
38
|
}
|
|
@@ -21,7 +21,7 @@ export class Interval1D {
|
|
|
21
21
|
this.pixelSize = pixelSize || 1;
|
|
22
22
|
this.selection = selection;
|
|
23
23
|
this.peers = peers;
|
|
24
|
-
this.field = field || getField(mark,
|
|
24
|
+
this.field = field || getField(mark, channel);
|
|
25
25
|
this.style = style && sanitizeStyles(style);
|
|
26
26
|
this.brush = channel === 'y' ? brushY() : brushX();
|
|
27
27
|
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
@@ -22,8 +22,8 @@ export class Interval2D {
|
|
|
22
22
|
this.pixelSize = pixelSize || 1;
|
|
23
23
|
this.selection = selection;
|
|
24
24
|
this.peers = peers;
|
|
25
|
-
this.xfield = xfield || getField(mark,
|
|
26
|
-
this.yfield = yfield || getField(mark,
|
|
25
|
+
this.xfield = xfield || getField(mark, 'x');
|
|
26
|
+
this.yfield = yfield || getField(mark, 'y');
|
|
27
27
|
this.style = style && sanitizeStyles(style);
|
|
28
28
|
this.brush = brush();
|
|
29
29
|
this.brush.on('brush end', ({ selection }) => this.publish(selection));
|
|
@@ -18,8 +18,8 @@ export class PanZoom {
|
|
|
18
18
|
this.mark = mark;
|
|
19
19
|
this.xsel = x;
|
|
20
20
|
this.ysel = y;
|
|
21
|
-
this.xfield = xfield || getField(mark,
|
|
22
|
-
this.yfield = yfield || getField(mark,
|
|
21
|
+
this.xfield = xfield || getField(mark, 'x');
|
|
22
|
+
this.yfield = yfield || getField(mark, 'y');
|
|
23
23
|
this.zoom = extent(zoom, [0, Infinity], [1, 1]);
|
|
24
24
|
this.panx = this.xsel && panx;
|
|
25
25
|
this.pany = this.ysel && pany;
|
package/src/legend.js
CHANGED
|
@@ -56,8 +56,10 @@ function findMark({ marks }, channel) {
|
|
|
56
56
|
: null;
|
|
57
57
|
if (channels == null) return null;
|
|
58
58
|
for (let i = marks.length - 1; i > -1; --i) {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
for (const channel of channels) {
|
|
60
|
+
if (marks[i].channelField(channel, { exact: true })) {
|
|
61
|
+
return marks[i];
|
|
62
|
+
}
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
return null;
|
|
@@ -6,29 +6,26 @@ import { Mark } from './Mark.js';
|
|
|
6
6
|
export class ConnectedMark extends Mark {
|
|
7
7
|
constructor(type, source, encodings) {
|
|
8
8
|
const dim = type.endsWith('X') ? 'y' : type.endsWith('Y') ? 'x' : null;
|
|
9
|
-
const req = { [dim]: ['min', 'max'] };
|
|
9
|
+
const req = dim ? { [dim]: ['min', 'max'] } : undefined;
|
|
10
10
|
super(type, source, encodings, req);
|
|
11
11
|
this.dim = dim;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
query(filter = []) {
|
|
15
|
-
const { plot, dim, source
|
|
15
|
+
const { plot, dim, source } = this;
|
|
16
16
|
const { optimize = true } = source.options || {};
|
|
17
17
|
const q = super.query(filter);
|
|
18
18
|
if (!dim) return q;
|
|
19
19
|
|
|
20
20
|
const ortho = dim === 'x' ? 'y' : 'x';
|
|
21
|
-
const value = this.channelField(ortho)?.as;
|
|
22
|
-
const { field, as } = this.channelField(dim);
|
|
23
|
-
const { type } = stats[field.column];
|
|
21
|
+
const value = this.channelField(ortho, { exact: true })?.as;
|
|
22
|
+
const { field, as, type, min, max } = this.channelField(dim);
|
|
24
23
|
const isContinuous = type === 'date' || type === 'number';
|
|
25
24
|
|
|
26
25
|
if (optimize && isContinuous && value) {
|
|
27
26
|
// TODO: handle stacked data!
|
|
28
|
-
const { column } = field;
|
|
29
|
-
const { max, min } = stats[column];
|
|
30
27
|
const size = dim === 'x' ? plot.innerWidth() : plot.innerHeight();
|
|
31
|
-
const [lo, hi] = filteredExtent(filter,
|
|
28
|
+
const [lo, hi] = filteredExtent(filter, field) || [min, max];
|
|
32
29
|
const [expr] = binExpr(this, dim, size, [lo, hi], 1, as);
|
|
33
30
|
const cols = q.select()
|
|
34
31
|
.map(c => c.as)
|
|
@@ -47,10 +47,17 @@ export class DenseLineMark extends RasterMark {
|
|
|
47
47
|
function stripXY(mark, filter) {
|
|
48
48
|
if (Array.isArray(filter) && !filter.length) return filter;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
50
|
+
// get column expressions for x and y encoding channels
|
|
51
|
+
const { column: xc } = mark.channelField('x');
|
|
52
|
+
const { column: yc } = mark.channelField('y');
|
|
53
|
+
|
|
54
|
+
// test if a range predicate filters the x or y channels
|
|
55
|
+
const test = p => {
|
|
56
|
+
const col = `${p.field}`;
|
|
57
|
+
return p.op !== 'BETWEEN' || col !== xc && col !== yc;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// filter boolean 'and' operations
|
|
54
61
|
const filterAnd = p => p.op === 'AND'
|
|
55
62
|
? and(p.children.filter(c => test(c)))
|
|
56
63
|
: p;
|
package/src/marks/Grid2DMark.js
CHANGED
|
@@ -39,7 +39,7 @@ export class Grid2DMark extends Mark {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
setPlot(plot, index) {
|
|
42
|
-
const update = () => { if (this.
|
|
42
|
+
const update = () => { if (this.hasFieldInfo()) this.requestUpdate(); };
|
|
43
43
|
plot.addAttributeListener('domainX', update);
|
|
44
44
|
plot.addAttributeListener('domainY', update);
|
|
45
45
|
return super.setPlot(plot, index);
|
package/src/marks/Mark.js
CHANGED
|
@@ -19,6 +19,8 @@ const fieldEntry = (channel, field) => ({
|
|
|
19
19
|
});
|
|
20
20
|
const valueEntry = (channel, value) => ({ channel, value });
|
|
21
21
|
|
|
22
|
+
// checks if a data source is an explicit array of values
|
|
23
|
+
// as opposed to a database table refernece
|
|
22
24
|
export const isDataArray = source => Array.isArray(source);
|
|
23
25
|
|
|
24
26
|
export class Mark extends MosaicClient {
|
|
@@ -94,45 +96,47 @@ export class Mark extends MosaicClient {
|
|
|
94
96
|
return this.source == null || isDataArray(this.source);
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
hasFieldInfo() {
|
|
100
|
+
return !!this._fieldInfo;
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
channel(channel) {
|
|
98
104
|
return this.channels.find(c => c.channel === channel);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
channelField(
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
107
|
+
channelField(channel, { exact } = {}) {
|
|
108
|
+
const c = exact
|
|
109
|
+
? this.channel(channel)
|
|
110
|
+
: this.channels.find(c => c.channel.startsWith(channel));
|
|
111
|
+
return c?.field ? c : null;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
fields() {
|
|
111
115
|
if (this.hasOwnData()) return null;
|
|
112
|
-
const { source: { table }, channels, reqs } = this;
|
|
113
116
|
|
|
117
|
+
const { source: { table }, channels, reqs } = this;
|
|
114
118
|
const fields = new Map;
|
|
115
119
|
for (const { channel, field } of channels) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
reqs[channel]?.forEach(s => entry.add(s));
|
|
123
|
-
field.stats?.forEach(s => entry.add(s));
|
|
124
|
-
}
|
|
120
|
+
if (!field) continue;
|
|
121
|
+
const stats = field.stats?.stats || [];
|
|
122
|
+
const key = field.stats?.column ?? field;
|
|
123
|
+
const entry = fields.get(key) ?? fields.set(key, new Set).get(key);
|
|
124
|
+
stats.forEach(s => entry.add(s));
|
|
125
|
+
reqs[channel]?.forEach(s => entry.add(s));
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
127
|
+
|
|
128
|
+
return Array.from(fields, ([c, s]) => ({ table, column: c, stats: s }));
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
fieldInfo(info) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
const lookup = Object.fromEntries(info.map(x => [x.column, x]));
|
|
133
|
+
for (const entry of this.channels) {
|
|
134
|
+
const { field } = entry;
|
|
135
|
+
if (field) {
|
|
136
|
+
Object.assign(entry, lookup[field.stats?.column ?? field]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
this._fieldInfo = true;
|
|
136
140
|
return this;
|
|
137
141
|
}
|
|
138
142
|
|
|
@@ -169,6 +173,13 @@ export class Mark extends MosaicClient {
|
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Helper method for setting a channel option in a Plot specification.
|
|
178
|
+
* Checks if a constant value or a data field is needed.
|
|
179
|
+
* Also avoids misinterpretation of data values as color names.
|
|
180
|
+
* @param {*} c a visual encoding channel spec
|
|
181
|
+
* @returns the Plot channel option
|
|
182
|
+
*/
|
|
172
183
|
export function channelOption(c) {
|
|
173
184
|
// use a scale override for color channels to sidestep
|
|
174
185
|
// https://github.com/observablehq/plot/issues/1593
|
|
@@ -177,6 +188,16 @@ export function channelOption(c) {
|
|
|
177
188
|
: c.as;
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Default query construction for a mark.
|
|
193
|
+
* Tracks aggregates by checking fields for an aggregate flag.
|
|
194
|
+
* If aggregates are found, groups by all non-aggregate fields.
|
|
195
|
+
* @param {*} channels array of visual encoding channel specs.
|
|
196
|
+
* @param {*} table the table to query.
|
|
197
|
+
* @param {*} skip an optional array of channels to skip.
|
|
198
|
+
* Mark subclasses can skip channels that require special handling.
|
|
199
|
+
* @returns a Query instance
|
|
200
|
+
*/
|
|
180
201
|
export function markQuery(channels, table, skip = []) {
|
|
181
202
|
const q = Query.from({ source: table });
|
|
182
203
|
const dims = new Set;
|
package/src/marks/RasterMark.js
CHANGED
|
@@ -23,7 +23,7 @@ export class RasterMark extends Grid2DMark {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
setPlot(plot, index) {
|
|
26
|
-
const update = () => { if (this.
|
|
26
|
+
const update = () => { if (this.hasFieldInfo()) this.rasterize(); };
|
|
27
27
|
plot.addAttributeListener('schemeColor', update);
|
|
28
28
|
super.setPlot(plot, index);
|
|
29
29
|
}
|
|
@@ -18,7 +18,7 @@ export class RasterTileMark extends Grid2DMark {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
setPlot(plot, index) {
|
|
21
|
-
const update = () => { if (this.
|
|
21
|
+
const update = () => { if (this.hasFieldInfo()) this.rasterize(); };
|
|
22
22
|
plot.addAttributeListener('schemeColor', update);
|
|
23
23
|
super.setPlot(plot, index);
|
|
24
24
|
}
|
|
@@ -5,8 +5,7 @@ export function channelScale(mark, channel) {
|
|
|
5
5
|
|
|
6
6
|
let scaleType = plot.getAttribute(`${channel}Scale`);
|
|
7
7
|
if (!scaleType) {
|
|
8
|
-
const {
|
|
9
|
-
const { type } = mark.stats[field.column];
|
|
8
|
+
const { type } = mark.channelField(channel);
|
|
10
9
|
scaleType = type === 'date' ? 'time' : 'linear';
|
|
11
10
|
}
|
|
12
11
|
|
package/src/marks/util/extent.js
CHANGED
|
@@ -6,16 +6,14 @@ export const yext = { y: ['min', 'max'] };
|
|
|
6
6
|
export const xyext = { ...xext, ...yext };
|
|
7
7
|
|
|
8
8
|
export function plotExtent(mark, filter, channel, domainAttr, niceAttr) {
|
|
9
|
-
const { plot
|
|
9
|
+
const { plot } = mark;
|
|
10
10
|
const domain = plot.getAttribute(domainAttr);
|
|
11
11
|
const nice = plot.getAttribute(niceAttr);
|
|
12
12
|
|
|
13
13
|
if (Array.isArray(domain) && !domain[Transient]) {
|
|
14
14
|
return domain;
|
|
15
15
|
} else {
|
|
16
|
-
const {
|
|
17
|
-
const { column } = field;
|
|
18
|
-
const { min, max } = stats[column];
|
|
16
|
+
const { column, min, max } = mark.channelField(channel);
|
|
19
17
|
const dom = filteredExtent(filter, column) || (nice
|
|
20
18
|
? scaleLinear().domain([min, max]).nice().domain()
|
|
21
19
|
: [min, max]);
|
|
@@ -39,7 +37,7 @@ export function filteredExtent(filter, column) {
|
|
|
39
37
|
let lo;
|
|
40
38
|
let hi;
|
|
41
39
|
const visitor = (type, clause) => {
|
|
42
|
-
if (type === 'BETWEEN' && clause.field
|
|
40
|
+
if (type === 'BETWEEN' && `${clause.field}` === column) {
|
|
43
41
|
const { range } = clause;
|
|
44
42
|
if (range && (lo == null || range[0] < lo)) lo = range[0];
|
|
45
43
|
if (range && (hi == null || range[1] > hi)) hi = range[1];
|
package/src/marks/util/grid.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { InternSet, ascending } from 'd3';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
convertArrowArrayType,
|
|
4
|
+
convertArrowColumn,
|
|
5
|
+
isArrowTable
|
|
6
|
+
} from '@uwdata/mosaic-core';
|
|
3
7
|
|
|
4
8
|
function arrayType(values, name = 'density') {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return typeof values[0][name] === 'number' ? Float64Array : Array;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function grid1d(n, values) {
|
|
13
|
-
const Type = arrayType(values);
|
|
14
|
-
return valuesToGrid(new Type(n), values);
|
|
9
|
+
return isArrowTable(values)
|
|
10
|
+
? convertArrowArrayType(values.getChild(name).type)
|
|
11
|
+
: typeof values[0]?.[name] === 'number' ? Float64Array : Array;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
function
|
|
14
|
+
export function grid1d(n, values, name = 'density') {
|
|
15
|
+
const grid = new (arrayType(values))(n);
|
|
18
16
|
if (isArrowTable(values)) {
|
|
19
17
|
// optimize access for Arrow tables
|
|
20
18
|
const numRows = values.numRows;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Delaunay, randomLcg } from 'd3';
|
|
2
2
|
|
|
3
|
+
// Derived from Observable Plot’s interpolatorBarycentric:
|
|
4
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L283-L334
|
|
5
|
+
|
|
3
6
|
export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
|
|
4
7
|
return (index, width, height, X, Y, V, W) => {
|
|
5
8
|
// Interpolate the interior of all triangles with barycentric coordinates
|
|
@@ -122,6 +125,9 @@ function ray(j, X, Y) {
|
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// Derived from Observable Plot’s interpolateNearest:
|
|
129
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L410-L428
|
|
130
|
+
|
|
125
131
|
export function interpolateNearest(index, width, height, X, Y, V, W) {
|
|
126
132
|
const delaunay = Delaunay.from(index, (i) => X[i], (i) => Y[i]);
|
|
127
133
|
// memoization of delaunay.find for the line start (iy) and pixel (ix)
|
|
@@ -137,6 +143,9 @@ export function interpolateNearest(index, width, height, X, Y, V, W) {
|
|
|
137
143
|
return W;
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// Derived from Observable Plot’s interpolatorRandomWalk:
|
|
147
|
+
// https://github.com/observablehq/plot/blob/41a63e372453d2f95e7a046839dfd245d21e7660/src/marks/raster.js#L430-L462
|
|
148
|
+
|
|
140
149
|
// https://observablehq.com/@observablehq/walk-on-spheres-precision
|
|
141
150
|
export function interpolatorRandomWalk({random = randomLcg(42), minDistance = 0.5, maxSteps = 2} = {}) {
|
|
142
151
|
return (index, width, height, X, Y, V, W) => {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { color } from 'd3';
|
|
2
2
|
|
|
3
|
+
// Derived from Observable Plot’s isColor:
|
|
4
|
+
// https://github.com/observablehq/plot/blob/a063b226fec284c5b0e973701fdbbb244ef9ac2c/src/options.js#L462-L477
|
|
5
|
+
|
|
3
6
|
// Mostly relies on d3-color, with a few extra color keywords. Currently this
|
|
4
7
|
// strictly requires that the value be a string; we might want to apply string
|
|
5
8
|
// coercion here, though note that d3-color instances would need to support
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { convertArrowValue, isArrowTable } from '@uwdata/mosaic-core';
|
|
2
2
|
|
|
3
3
|
export function toDataArray(data) {
|
|
4
4
|
return isArrowTable(data) ? arrowToObjects(data) : data;
|
|
@@ -34,7 +34,7 @@ export function arrowToObjects(data) {
|
|
|
34
34
|
for (let j = 0; j < numCols; ++j) {
|
|
35
35
|
const child = batch.getChildAt(j);
|
|
36
36
|
const { name, type } = schema.fields[j];
|
|
37
|
-
const valueOf =
|
|
37
|
+
const valueOf = convertArrowValue(type);
|
|
38
38
|
|
|
39
39
|
// for each row in the current batch...
|
|
40
40
|
for (let o = k, i = 0; i < numRows; ++i, ++o) {
|
package/src/plot-renderer.js
CHANGED
|
@@ -9,8 +9,6 @@ const OPTIONS_ONLY_MARKS = new Set([
|
|
|
9
9
|
'graticule'
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
12
|
// construct Plot output
|
|
15
13
|
// see https://github.com/observablehq/plot
|
|
16
14
|
export async function plotRenderer(plot) {
|
|
@@ -76,17 +74,17 @@ function setSymbolAttributes(plot, svg, attributes, symbols) {
|
|
|
76
74
|
|
|
77
75
|
function inferLabels(spec, plot) {
|
|
78
76
|
const { marks } = plot;
|
|
79
|
-
inferLabel('x', spec, marks
|
|
80
|
-
inferLabel('y', spec, marks
|
|
77
|
+
inferLabel('x', spec, marks);
|
|
78
|
+
inferLabel('y', spec, marks);
|
|
81
79
|
inferLabel('fx', spec, marks);
|
|
82
80
|
inferLabel('fy', spec, marks);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
function inferLabel(key, spec, marks
|
|
83
|
+
function inferLabel(key, spec, marks) {
|
|
86
84
|
const scale = spec[key] || {};
|
|
87
85
|
if (scale.axis === null || scale.label !== undefined) return; // nothing to do
|
|
88
86
|
|
|
89
|
-
const fields = marks.map(mark => mark.channelField(
|
|
87
|
+
const fields = marks.map(mark => mark.channelField(key)?.field);
|
|
90
88
|
if (fields.every(x => x == null)) return; // no columns found
|
|
91
89
|
|
|
92
90
|
// check for consistent columns / labels
|
|
@@ -100,7 +98,7 @@ function inferLabel(key, spec, marks, channels = [key]) {
|
|
|
100
98
|
} else if (candCol === undefined && candLabel === undefined) {
|
|
101
99
|
candCol = column;
|
|
102
100
|
candLabel = label;
|
|
103
|
-
type = getType(marks[i].data,
|
|
101
|
+
type = getType(marks[i].data, key) || 'number';
|
|
104
102
|
} else if (candLabel !== label) {
|
|
105
103
|
candLabel = undefined;
|
|
106
104
|
} else if (candCol !== column) {
|
|
@@ -149,13 +147,11 @@ function annotateMarks(svg, indices) {
|
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
152
|
-
function getType(data,
|
|
150
|
+
function getType(data, channel) {
|
|
153
151
|
for (const row of data) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
return v instanceof Date ? 'date' : typeof v;
|
|
158
|
-
}
|
|
152
|
+
const v = row[channel] ?? row[channel+'1'] ?? row[channel+'2'];
|
|
153
|
+
if (v != null) {
|
|
154
|
+
return v instanceof Date ? 'date' : typeof v;
|
|
159
155
|
}
|
|
160
156
|
}
|
|
161
157
|
}
|
package/src/transforms/bin.js
CHANGED
|
@@ -24,12 +24,12 @@ function binField(mark, channel, column, options) {
|
|
|
24
24
|
return {
|
|
25
25
|
column,
|
|
26
26
|
label: column,
|
|
27
|
-
get stats() { return ['min', 'max']; },
|
|
27
|
+
get stats() { return { column, stats: ['min', 'max'] }; },
|
|
28
28
|
get columns() { return [column]; },
|
|
29
29
|
get basis() { return column; },
|
|
30
30
|
toString() {
|
|
31
31
|
const { apply, sqlApply, sqlInvert } = channelScale(mark, channel);
|
|
32
|
-
const { min, max } = mark.
|
|
32
|
+
const { min, max } = mark.channelField(channel);
|
|
33
33
|
const b = bins(apply(min), apply(max), options);
|
|
34
34
|
const col = sqlApply(column);
|
|
35
35
|
const base = b.min === 0 ? col : `(${col} - ${b.min})`;
|
package/src/marks/util/arrow.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
export const INTEGER = 2;
|
|
2
|
-
export const FLOAT = 3;
|
|
3
|
-
export const DECIMAL = 7;
|
|
4
|
-
export const TIMESTAMP = 10;
|
|
5
|
-
|
|
6
|
-
export function isArrowTable(values) {
|
|
7
|
-
return typeof values?.getChild === 'function';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function convertArrowType(type) {
|
|
11
|
-
switch (type.typeId) {
|
|
12
|
-
case INTEGER:
|
|
13
|
-
case FLOAT:
|
|
14
|
-
case DECIMAL:
|
|
15
|
-
return Float64Array;
|
|
16
|
-
default:
|
|
17
|
-
return Array;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function convertArrow(type) {
|
|
22
|
-
const { typeId } = type;
|
|
23
|
-
|
|
24
|
-
// map timestamp numbers to date objects
|
|
25
|
-
if (typeId === TIMESTAMP) {
|
|
26
|
-
return v => v == null ? v : new Date(v);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// map bignum to number
|
|
30
|
-
if (typeId === INTEGER && type.bitWidth >= 64) {
|
|
31
|
-
return v => v == null ? v : Number(v);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// otherwise use Arrow JS defaults
|
|
35
|
-
return v => v;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function convertArrowColumn(column) {
|
|
39
|
-
const { type } = column;
|
|
40
|
-
const { typeId } = type;
|
|
41
|
-
|
|
42
|
-
// map bignum to number
|
|
43
|
-
if (typeId === INTEGER && type.bitWidth >= 64) {
|
|
44
|
-
const size = column.length;
|
|
45
|
-
const array = new Float64Array(size);
|
|
46
|
-
for (let row = 0; row < size; ++row) {
|
|
47
|
-
const v = column.get(row);
|
|
48
|
-
array[row] = v == null ? NaN : Number(v);
|
|
49
|
-
}
|
|
50
|
-
return array;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// otherwise use Arrow JS defaults
|
|
54
|
-
return column.toArray();
|
|
55
|
-
}
|