Skip to content

Commit 0e74a1e

Browse files
committed
implicit number coercion for valueof and arrayify
1 parent 0a9f32c commit 0e74a1e

File tree

10 files changed

+69
-74
lines changed

10 files changed

+69
-74
lines changed

src/channel.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {ascending, descending, rollup, sort} from "d3";
2-
import {first, isColor, isEvery, isIterable, labelof, map, maybeValue, range, floatMap, valueof} from "./options.js";
2+
import {first, isColor, isEvery, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
33
import {registry} from "./scales/index.js";
44
import {isSymbol, maybeSymbol} from "./symbols.js";
55
import {maybeReduce} from "./transforms/group.js";
@@ -121,7 +121,7 @@ function findScaleChannel(channels, scale) {
121121
function difference(channels, k1, k2) {
122122
const X1 = values(channels, k1);
123123
const X2 = values(channels, k2);
124-
return floatMap(X2, (x2, i) => Math.abs(x2 - X1[i]));
124+
return map(X2, (x2, i) => Math.abs(x2 - X1[i]), Float64Array);
125125
}
126126

127127
function values(channels, name, alias) {

src/marks/density.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {contourDensity, create, geoPath} from "d3";
22
import {Mark} from "../mark.js";
3-
import {isTypedArray, maybeTuple, maybeZ} from "../options.js";
3+
import {coerceNumbers, isTypedArray, maybeTuple, maybeZ} from "../options.js";
44
import {Position} from "../projection.js";
5-
import {coerceNumbers} from "../scales.js";
65
import {
76
applyFrameAnchor,
87
applyDirectStyles,

src/marks/line.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import {geoPath, line as shapeLine} from "d3";
22
import {create} from "../context.js";
33
import {curveAuto, PathCurve} from "../curve.js";
44
import {Mark} from "../mark.js";
5-
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
6-
import {coerceNumbers} from "../scales.js";
5+
import {coerceNumbers, indexOf, identity, maybeTuple, maybeZ} from "../options.js";
76
import {
87
applyDirectStyles,
98
applyIndirectStyles,

src/marks/link.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {geoPath, pathRound as path} from "d3";
22
import {create} from "../context.js";
33
import {curveAuto, PathCurve} from "../curve.js";
44
import {Mark} from "../mark.js";
5-
import {coerceNumbers} from "../scales.js";
5+
import {coerceNumbers} from "../options.js";
66
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
77
import {markers, applyMarkers} from "./marker.js";
88

src/marks/raster.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {blurImage, Delaunay, randomLcg, rgb} from "d3";
22
import {valueObject} from "../channel.js";
33
import {create} from "../context.js";
4-
import {floatMap, first, second, third, isTuples, isNumeric, isTemporal, take, identity} from "../options.js";
4+
import {map, first, second, third, isTuples, isNumeric, isTemporal, take, identity} from "../options.js";
55
import {maybeColorChannel, maybeNumberChannel} from "../options.js";
66
import {Mark} from "../mark.js";
77
import {applyAttr, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString} from "../style.js";
@@ -115,8 +115,8 @@ export class Raster extends AbstractRaster {
115115
if (this.interpolate) {
116116
const kx = w / dx;
117117
const ky = h / dy;
118-
const IX = floatMap(X, (x) => (x - x1) * kx);
119-
const IY = floatMap(Y, (y) => (y - y1) * ky);
118+
const IX = map(X, (x) => (x - x1) * kx, Float64Array);
119+
const IY = map(Y, (y) => (y - y1) * ky, Float64Array);
120120
if (F) F = this.interpolate(index, w, h, IX, IY, F);
121121
if (FO) FO = this.interpolate(index, w, h, IX, IY, FO);
122122
}

src/options.js

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@ const objectToString = Object.prototype.toString;
99
/** @jsdoc valueof */
1010
export function valueof(data, value, type) {
1111
const valueType = typeof value;
12-
const access =
13-
type && Object.getPrototypeOf(type) === TypedArray
14-
? (value) => floatMap(data, value, type)
15-
: (value) => map(data, value);
1612
return valueType === "string"
17-
? access(field(value))
13+
? maybeTypedMap(data, field(value), type)
1814
: valueType === "function"
19-
? access(value)
15+
? maybeTypedMap(data, value, type)
2016
: valueType === "number" || value instanceof Date || valueType === "boolean"
21-
? access(constant(value))
17+
? map(data, constant(value), type)
2218
: value && typeof value.transform === "function"
2319
? arrayify(value.transform(data), type)
2420
: arrayify(value, type); // preserve undefined type
2521
}
2622

23+
// When valueof is asked to produce a typed array (i.e., numbers) we implicitly
24+
// apply null-safe type coercion.
25+
function maybeTypedMap(data, f, type) {
26+
return map(data, isTypedArray(type?.prototype) ? floater(f) : f, type);
27+
}
28+
29+
function floater(f) {
30+
return (d, i) => coerceNumber(f(d, i));
31+
}
32+
2733
export const field = (name) => (d) => d[name];
2834
export const indexOf = (d, i) => i;
2935
/** @jsdoc identity */
@@ -46,6 +52,38 @@ export function percentile(reduce) {
4652
return (I, f) => quantile(I, p, f);
4753
}
4854

55+
// If the values are specified as a typed array, no coercion is required.
56+
export function coerceNumbers(values) {
57+
return isTypedArray(values) ? values : map(values, coerceNumber, Float64Array);
58+
}
59+
60+
// Unlike Mark’s number, here we want to convert null and undefined to NaN since
61+
// the result will be stored in a Float64Array and we don’t want null to be
62+
// coerced to zero. We use Number instead of unary + to allow BigInt coercion.
63+
export function coerceNumber(x) {
64+
return x == null ? NaN : Number(x);
65+
}
66+
67+
export function coerceDates(values) {
68+
return map(values, coerceDate);
69+
}
70+
71+
// When coercing strings to dates, we only want to allow the ISO 8601 format
72+
// since the built-in string parsing of the Date constructor varies across
73+
// browsers. (In the future, this could be made more liberal if desired, though
74+
// it is still generally preferable to do date parsing yourself explicitly,
75+
// rather than rely on Plot.) Any non-string values are coerced to number first
76+
// and treated as milliseconds since UNIX epoch.
77+
export function coerceDate(x) {
78+
return x instanceof Date && !isNaN(x)
79+
? x
80+
: typeof x === "string"
81+
? isoParse(x)
82+
: x == null || isNaN((x = +x))
83+
? undefined
84+
: new Date(x);
85+
}
86+
4987
// Some channels may allow a string constant to be specified; to differentiate
5088
// string constants (e.g., "red") from named fields (e.g., "date"), this
5189
// function tests whether the given value is a CSS color string and returns a
@@ -79,7 +117,9 @@ export function keyword(input, name, allowed) {
79117
// Promotes the specified data to an array or typed array as needed. If an array
80118
// type is provided (e.g., Array), then the returned array will strictly be of
81119
// the specified type; otherwise, any array or typed array may be returned. If
82-
// the specified data is null or undefined, returns the value as-is.
120+
// the specified data is null or undefined, returns the value as-is. When
121+
// converting a non-typed array to a typed array, null-safe number coercion is
122+
// implicitly applied.
83123
export function arrayify(data, type) {
84124
return data == null
85125
? data
@@ -89,30 +129,13 @@ export function arrayify(data, type) {
89129
: Array.from(data)
90130
: data instanceof type
91131
? data
92-
: type.from(data);
132+
: type.from(data, isTypedArray(type.prototype) && !isTypedArray(data) ? coerceNumber : undefined);
93133
}
94134

95135
// An optimization of type.from(values, f): if the given values are already an
96136
// instanceof the desired array type, the faster values.map method is used.
97-
export function map(values, f) {
98-
return values instanceof Array ? values.map(f) : Array.from(values, f);
99-
}
100-
101-
// Unlike Mark’s number, this converts null and undefined to NaN, since the
102-
// result will be stored in a Float64Array and we don’t want null to be coerced
103-
// to zero. Using Number to coerce BigInts.
104-
function toFloat(x) {
105-
return x == null ? NaN : Number(x);
106-
}
107-
108-
export function floatMap(values, f, type = Float64Array) {
109-
return f === undefined
110-
? values instanceof type
111-
? values.map(toFloat)
112-
: type.from(values, toFloat)
113-
: values instanceof type
114-
? values.map((d, i) => toFloat(f(d, i)))
115-
: type.from(values, (d, i) => toFloat(f(d, i)));
137+
export function map(values, f, type = Array) {
138+
return values instanceof type ? values.map(f) : type.from(values, f);
116139
}
117140

118141
// An optimization of type.from(values): if the given values are already an
@@ -252,7 +275,7 @@ export function mid(x1, x2) {
252275
const X2 = x2.transform(data);
253276
return isTemporal(X1) || isTemporal(X2)
254277
? map(X1, (_, i) => new Date((+X1[i] + +X2[i]) / 2))
255-
: floatMap(X1, (_, i) => (+X1[i] + +X2[i]) / 2);
278+
: map(X1, (_, i) => (+X1[i] + +X2[i]) / 2, Float64Array);
256279
},
257280
label: x1.label
258281
};

src/projection.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import {
1818
geoTransverseMercator
1919
} from "d3";
2020
import {valueObject} from "./channel.js";
21-
import {constant, isObject} from "./options.js";
22-
import {coerceNumbers} from "./scales.js";
21+
import {coerceNumbers, constant, isObject} from "./options.js";
2322
import {warn} from "./warnings.js";
2423

2524
const pi = Math.PI;

src/scales.js

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import {parse as isoParse} from "isoformat";
21
import {
32
isOrdinal,
43
isTemporal,
54
isTemporalString,
65
isNumericString,
76
isScaleOptions,
8-
isTypedArray,
97
map,
108
slice,
11-
floatMap
9+
coerceNumbers,
10+
coerceDates
1211
} from "./options.js";
1312
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
1413
import {
@@ -500,31 +499,6 @@ function coerceSymbols(values) {
500499
return map(values, maybeSymbol);
501500
}
502501

503-
function coerceDates(values) {
504-
return map(values, coerceDate);
505-
}
506-
507-
// If the values are specified as a typed array, no coercion is required.
508-
export function coerceNumbers(values) {
509-
return isTypedArray(values) ? values : floatMap(values);
510-
}
511-
512-
// When coercing strings to dates, we only want to allow the ISO 8601 format
513-
// since the built-in string parsing of the Date constructor varies across
514-
// browsers. (In the future, this could be made more liberal if desired, though
515-
// it is still generally preferable to do date parsing yourself explicitly,
516-
// rather than rely on Plot.) Any non-string values are coerced to number first
517-
// and treated as milliseconds since UNIX epoch.
518-
export function coerceDate(x) {
519-
return x instanceof Date && !isNaN(x)
520-
? x
521-
: typeof x === "string"
522-
? isoParse(x)
523-
: x == null || isNaN((x = +x))
524-
? undefined
525-
: new Date(x);
526-
}
527-
528502
/** @jsdoc scale */
529503
export function scale(options = {}) {
530504
let scale;

src/transforms/bin.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
import {
1212
valueof,
1313
identity,
14+
coerceDate,
15+
coerceNumbers,
1416
maybeColumn,
1517
maybeInterval,
1618
maybeTuple,
@@ -20,9 +22,8 @@ import {
2022
labelof,
2123
isTemporal,
2224
isIterable,
23-
floatMap
25+
map
2426
} from "../options.js";
25-
import {coerceDate, coerceNumbers} from "../scales.js";
2627
import {basic} from "./basic.js";
2728
import {
2829
hasOutput,
@@ -230,7 +231,7 @@ function maybeBin(options) {
230231
let V = valueof(data, value);
231232
let T; // bin thresholds
232233
if (isTemporal(V) || isTimeThresholds(thresholds)) {
233-
V = floatMap(V, coerceDate);
234+
V = map(V, coerceDate, Float64Array); // like coerceDates, but faster
234235
let [min, max] = typeof domain === "function" ? domain(V) : domain;
235236
let t = typeof thresholds === "function" && !isInterval(thresholds) ? thresholds(V, min, max) : thresholds;
236237
if (typeof t === "number") t = utcTickInterval(min, max, t);

src/transforms/dodge.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import IntervalTree from "interval-tree-1d";
22
import {finite, positive} from "../defined.js";
3-
import {floatMap, maybeNamed, number} from "../options.js";
3+
import {identity, maybeNamed, number, valueof} from "../options.js";
44
import {initializer} from "./basic.js";
55
import {Position} from "../projection.js";
66

@@ -72,7 +72,7 @@ function dodge(y, x, anchor, padding, options) {
7272
if (!channels[x]) throw new Error(`missing channel: ${x}`);
7373
({[x]: X} = Position(channels, scales, context));
7474
const r = R ? undefined : this.r !== undefined ? this.r : options.r !== undefined ? number(options.r) : 3;
75-
if (R) R = floatMap(R.value, scales[R.scale]);
75+
if (R) R = valueof(R.value, scales[R.scale] || identity, Float64Array);
7676
let [ky, ty] = anchor(dimensions);
7777
const compare = ky ? compareAscending : compareSymmetric;
7878
const Y = new Float64Array(X.length);

0 commit comments

Comments
 (0)