Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3151,7 +3151,7 @@ Given an iterable *data* and some *value* accessor, returns an array (a column)
* an array of values - returning the same
* null or undefined - returning the same

If *type* is specified, it must be Array or a similar class that implements the [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) interface such as a typed array. When *type* is Array or a typed array class, the return value of valueof will be an instance of the same (or null or undefined). If *type* is not specified, valueof may return either an array or a typed array (or null or undefined).
If *type* is specified, it must be Array or a similar class that implements the [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) interface such as a typed array. When *type* is Array or a typed array class, the return value of valueof will be an instance of the same (or null or undefined). When *type* is a typed array, values will be implicitly coerced numbers, and if *type* is Float64Array, Float32Array, or a subclass of the same, null values will be implicitly replaced with NaN. If *type* is not specified, valueof may return either an array or a typed array (or null or undefined).

Plot.valueof is not guaranteed to return a new array. When a transform method is used, or when the given *value* is an array that is compatible with the requested *type*, the array may be returned as-is without making a copy.

Expand Down
10 changes: 6 additions & 4 deletions src/marks/contour.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {blur2, contours, geoPath, map, max, min, nice, range, ticks, thresholdSturges} from "d3";
import {blur2, contours, geoPath, max, min, nice, range, ticks, thresholdSturges} from "d3";
import {Channels} from "../channel.js";
import {create} from "../context.js";
import {labelof, identity, arrayify} from "../options.js";
import {labelof, identity, arrayify, map} from "../options.js";
import {Position} from "../projection.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, styles} from "../style.js";
import {initializer} from "../transforms/basic.js";
Expand Down Expand Up @@ -156,7 +156,9 @@ function contourGeometry({thresholds, interval, ...options}) {
const {contour} = contours().size([w, h]).smooth(this.smooth);
const contourData = [];
const contourFacets = [];
for (const V of VV) contourFacets.push(range(contourData.length, contourData.push(...T.map((t) => contour(V, t)))));
for (const V of VV) {
contourFacets.push(range(contourData.length, contourData.push(...map(T, (t) => contour(V, t)))));
}

// Rescale the contour multipolygon from grid to screen coordinates.
for (const {coordinates} of contourData) {
Expand Down Expand Up @@ -188,7 +190,7 @@ function contourGeometry({thresholds, interval, ...options}) {
function maybeTicks(thresholds, V, min, max) {
if (typeof thresholds?.range === "function") return thresholds.range(thresholds.floor(min), max);
if (typeof thresholds === "function") thresholds = thresholds(V, min, max);
if (typeof thresholds !== "number") return arrayify(thresholds, Array);
if (typeof thresholds !== "number") return arrayify(thresholds);
const tz = ticks(...nice(min, max, thresholds), thresholds);
while (tz[tz.length - 1] >= max) tz.pop();
while (tz[1] < min) tz.shift();
Expand Down
5 changes: 2 additions & 3 deletions src/marks/density.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {contourDensity, create, geoPath} from "d3";
import {Mark} from "../mark.js";
import {isTypedArray, maybeTuple, maybeZ} from "../options.js";
import {coerceNumbers, maybeTuple, maybeZ, TypedArray} from "../options.js";
import {Position} from "../projection.js";
import {coerceNumbers} from "../scales.js";
import {
applyFrameAnchor,
applyDirectStyles,
Expand Down Expand Up @@ -129,7 +128,7 @@ function densityInitializer(options, fillDensity, strokeDensity) {
// If explicit thresholds were not specified, find the maximum density of
// all grids and use this to compute thresholds.
let T = thresholds;
if (!isTypedArray(T)) {
if (!(T instanceof TypedArray)) {
let maxValue = 0;
for (const facetContours of facetsContours) {
for (const [, contour] of facetContours) {
Expand Down
3 changes: 1 addition & 2 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {geoPath, line as shapeLine} from "d3";
import {create} from "../context.js";
import {curveAuto, PathCurve} from "../curve.js";
import {Mark} from "../mark.js";
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {coerceNumbers} from "../scales.js";
import {coerceNumbers, indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {
applyDirectStyles,
applyIndirectStyles,
Expand Down
2 changes: 1 addition & 1 deletion src/marks/link.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {geoPath, pathRound as path} from "d3";
import {create} from "../context.js";
import {curveAuto, PathCurve} from "../curve.js";
import {Mark} from "../mark.js";
import {coerceNumbers} from "../scales.js";
import {coerceNumbers} from "../options.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform} from "../style.js";
import {markers, applyMarkers} from "./marker.js";

Expand Down
81 changes: 58 additions & 23 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,39 @@ import {color, descending, range as rangei, quantile} from "d3";
import {maybeTimeInterval, maybeUtcInterval} from "./time.js";

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
const TypedArray = Object.getPrototypeOf(Uint8Array);
export const TypedArray = Object.getPrototypeOf(Uint8Array);
const objectToString = Object.prototype.toString;

/** @jsdoc valueof */
export function valueof(data, value, type) {
const valueType = typeof value;
return valueType === "string"
? map(data, field(value), type)
? maybeTypedMap(data, field(value), type)
: valueType === "function"
? map(data, value, type)
? maybeTypedMap(data, value, type)
: valueType === "number" || value instanceof Date || valueType === "boolean"
? map(data, constant(value), type)
: value && typeof value.transform === "function"
? arrayify(value.transform(data), type)
: arrayify(value, type); // preserve undefined type
? maybeTypedArrayify(value.transform(data), type)
: maybeTypedArrayify(value, type);
}

function maybeTypedMap(data, f, type) {
return map(data, type?.prototype instanceof TypedArray ? floater(f) : f, type);
}

function maybeTypedArrayify(data, type) {
return type === undefined
? arrayify(data) // preserve undefined type
: data instanceof type
? data
: type.prototype instanceof TypedArray && !(data instanceof TypedArray)
? type.from(data, coerceNumber)
: type.from(data);
}

function floater(f) {
return (d, i) => coerceNumber(f(d, i));
}

export const field = (name) => (d) => d[name];
Expand All @@ -42,6 +60,38 @@ export function percentile(reduce) {
return (I, f) => quantile(I, p, f);
}

// If the values are specified as a typed array, no coercion is required.
export function coerceNumbers(values) {
return values instanceof TypedArray ? values : map(values, coerceNumber, Float64Array);
}

// Unlike Mark’s number, here we want to convert null and undefined to NaN since
// the result will be stored in a Float64Array and we don’t want null to be
// coerced to zero. We use Number instead of unary + to allow BigInt coercion.
function coerceNumber(x) {
return x == null ? NaN : Number(x);
}

export function coerceDates(values) {
return map(values, coerceDate);
}

// When coercing strings to dates, we only want to allow the ISO 8601 format
// since the built-in string parsing of the Date constructor varies across
// browsers. (In the future, this could be made more liberal if desired, though
// it is still generally preferable to do date parsing yourself explicitly,
// rather than rely on Plot.) Any non-string values are coerced to number first
// and treated as milliseconds since UNIX epoch.
export function coerceDate(x) {
return x instanceof Date && !isNaN(x)
? x
: typeof x === "string"
? isoParse(x)
: x == null || isNaN((x = +x))
? undefined
: new Date(x);
}

// Some channels may allow a string constant to be specified; to differentiate
// string constants (e.g., "red") from named fields (e.g., "date"), this
// function tests whether the given value is a CSS color string and returns a
Expand Down Expand Up @@ -72,20 +122,9 @@ export function keyword(input, name, allowed) {
return i;
}

// Promotes the specified data to an array or typed array as needed. If an array
// type is provided (e.g., Array), then the returned array will strictly be of
// the specified type; otherwise, any array or typed array may be returned. If
// the specified data is null or undefined, returns the value as-is.
export function arrayify(data, type) {
return data == null
? data
: type === undefined
? data instanceof Array || data instanceof TypedArray
? data
: Array.from(data)
: data instanceof type
? data
: type.from(data);
// Promotes the specified data to an array as needed.
export function arrayify(data) {
return data == null || data instanceof Array || data instanceof TypedArray ? data : Array.from(data);
}

// An optimization of type.from(values, f): if the given values are already an
Expand All @@ -100,10 +139,6 @@ export function slice(values, type = Array) {
return values instanceof type ? values.slice() : type.from(values);
}

export function isTypedArray(values) {
return values instanceof TypedArray;
}

// Disambiguates an options object (e.g., {y: "x2"}) from a primitive value.
export function isObject(option) {
return option?.toString === objectToString;
Expand Down
3 changes: 1 addition & 2 deletions src/projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import {
geoTransverseMercator
} from "d3";
import {valueObject} from "./channel.js";
import {constant, isObject} from "./options.js";
import {coerceNumbers} from "./scales.js";
import {coerceNumbers, constant, isObject} from "./options.js";
import {warn} from "./warnings.js";

const pi = Math.PI;
Expand Down
38 changes: 3 additions & 35 deletions src/scales.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {parse as isoParse} from "isoformat";
import {
isOrdinal,
isTemporal,
isTemporalString,
isNumericString,
isScaleOptions,
isTypedArray,
map,
slice
slice,
coerceNumbers,
coerceDates
} from "./options.js";
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
import {
Expand Down Expand Up @@ -499,38 +499,6 @@ function coerceSymbols(values) {
return map(values, maybeSymbol);
}

function coerceDates(values) {
return map(values, coerceDate);
}

// If the values are specified as a typed array, no coercion is required.
export function coerceNumbers(values) {
return isTypedArray(values) ? values : map(values, coerceNumber, Float64Array);
}

// Unlike Mark’s number, here we want to convert null and undefined to NaN,
// since the result will be stored in a Float64Array and we don’t want null to
// be coerced to zero.
export function coerceNumber(x) {
return x == null ? NaN : Number(x);
}

// When coercing strings to dates, we only want to allow the ISO 8601 format
// since the built-in string parsing of the Date constructor varies across
// browsers. (In the future, this could be made more liberal if desired, though
// it is still generally preferable to do date parsing yourself explicitly,
// rather than rely on Plot.) Any non-string values are coerced to number first
// and treated as milliseconds since UNIX epoch.
export function coerceDate(x) {
return x instanceof Date && !isNaN(x)
? x
: typeof x === "string"
? isoParse(x)
: x == null || isNaN((x = +x))
? undefined
: new Date(x);
}

/** @jsdoc scale */
export function scale(options = {}) {
let scale;
Expand Down
9 changes: 5 additions & 4 deletions src/transforms/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import {
valueof,
identity,
coerceDate,
coerceNumbers,
maybeColumn,
maybeInterval,
maybeTuple,
Expand All @@ -22,7 +24,6 @@ import {
isIterable,
map
} from "../options.js";
import {coerceDate, coerceNumber} from "../scales.js";
import {basic} from "./basic.js";
import {
hasOutput,
Expand Down Expand Up @@ -230,7 +231,7 @@ function maybeBin(options) {
let V = valueof(data, value);
let T; // bin thresholds
if (isTemporal(V) || isTimeThresholds(thresholds)) {
V = map(V, coerceDate, Float64Array);
V = map(V, coerceDate, Float64Array); // like coerceDates, but faster
let [min, max] = typeof domain === "function" ? domain(V) : domain;
let t = typeof thresholds === "function" && !isInterval(thresholds) ? thresholds(V, min, max) : thresholds;
if (typeof t === "number") t = utcTickInterval(min, max, t);
Expand All @@ -243,7 +244,7 @@ function maybeBin(options) {
}
T = t;
} else {
V = map(V, coerceNumber, Float64Array); // TODO deduplicate with code above
V = coerceNumbers(V);
let [min, max] = typeof domain === "function" ? domain(V) : domain;
let t = typeof thresholds === "function" && !isInterval(thresholds) ? thresholds(V, min, max) : thresholds;
if (typeof t === "number") {
Expand Down Expand Up @@ -371,7 +372,7 @@ function Bin(EX, EY) {

// non-cumulative distribution
function bin1(E, T, V) {
T = T.map(coerceNumber); // for faster bisection; TODO skip if already typed
T = coerceNumbers(T); // for faster bisection
return (I) => {
const B = E.map(() => []);
for (const i of I) B[bisect(T, V[i]) - 1]?.push(i); // TODO quantization?
Expand Down
3 changes: 1 addition & 2 deletions src/transforms/dodge.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import IntervalTree from "interval-tree-1d";
import {finite, positive} from "../defined.js";
import {identity, maybeNamed, number, valueof} from "../options.js";
import {coerceNumbers} from "../scales.js";
import {initializer} from "./basic.js";
import {Position} from "../projection.js";

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