Skip to content

Commit 26d3ce0

Browse files
committed
keep in sync with #1008
1 parent 2094944 commit 26d3ce0

File tree

4 files changed

+229
-59
lines changed

4 files changed

+229
-59
lines changed

src/data.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
3-
export type Datum = any; // A Datum… who knows what it is?
4-
51
/**
62
* The marks's data contains the data for the mark; typically an array
73
* of objects or values, but can also be defined as an iterable compatible
84
* with Array.from.
95
*/
106
export type Data = ArrayLike<Datum> | Iterable<Datum>;
7+
export type Datum = unknown;
118

129
/**
1310
* An array or typed array constructor, or any class that implements Array.from
1411
*/
15-
export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor;
12+
export type ArrayType = ArrayConstructor | TypedArrayConstructor;
1613

1714
/**
1815
* The data is then arrayified, and a range of indices is computed, serving as pointers
1916
* into a the column representation of Plot.valueof
2017
*/
21-
export type DataArray = ArrayLike<Datum>;
18+
export type DataArray = Datum[] | TypedArray;
2219

2320
/**
2421
* Channels are arrays of values
@@ -35,5 +32,30 @@ export type index = number; // integer
3532
export type Series = index[] | Uint32Array; // a Series is a list of pointers into columnar data
3633
export type Facets = Series[];
3734

38-
export type NumericArray = number[] | Float32Array | Float64Array;
35+
export type NumericArray = number[] | TypedArray;
3936
export type ValueArray = NumericArray | Value[];
37+
38+
/**
39+
* Typed arrays are preserved through arrayify
40+
*/
41+
export type TypedArray =
42+
| Int8Array
43+
| Uint8Array
44+
| Int16Array
45+
| Uint16Array
46+
| Int32Array
47+
| Uint32Array
48+
| Uint8ClampedArray
49+
| Float32Array
50+
| Float64Array;
51+
52+
export type TypedArrayConstructor =
53+
| Int8ArrayConstructor
54+
| Uint8ArrayConstructor
55+
| Int16ArrayConstructor
56+
| Uint16ArrayConstructor
57+
| Int32ArrayConstructor
58+
| Uint32ArrayConstructor
59+
| Uint8ClampedArrayConstructor
60+
| Float32ArrayConstructor
61+
| Float64ArrayConstructor;

src/options.ts

Lines changed: 114 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import type {ArrayType, Value, DataArray, Datum, index, Data, Series, ValueArray} from "./data.js";
2+
import type {
3+
ArrayType,
4+
Value,
5+
DataArray,
6+
Datum,
7+
index,
8+
Data,
9+
Series,
10+
ValueArray,
11+
TypedArray,
12+
TypedArrayConstructor
13+
} from "./data.js";
314
import {parse as isoParse} from "isoformat";
4-
import {color, descending, quantile, TypedArray} from "d3";
15+
import {color, descending, quantile} from "d3";
516

617
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
718
const TypedArray = Object.getPrototypeOf(Uint8Array);
@@ -50,17 +61,25 @@ export function valueof(
5061
value: ValueAccessor,
5162
arrayType?: ArrayType
5263
): ValueArray | Float32Array | Float64Array | null | undefined {
53-
return data == null
54-
? data
55-
: typeof value === "string"
56-
? map(data, field(value), arrayType)
57-
: typeof value === "function"
58-
? map(data, value, arrayType)
59-
: typeof value === "number" || value instanceof Date || typeof value === "boolean"
60-
? map(data, constant(value), arrayType)
61-
: value && typeof (value as TransformMethod).transform === "function"
62-
? arrayify((value as TransformMethod).transform(data), arrayType)
63-
: arrayify(value as ValueArray, arrayType); // preserve undefined type
64+
if (value == null) return value;
65+
if (isTransform(value)) {
66+
data = value.transform(data) as Data | null | undefined;
67+
value = (d) => d as Value;
68+
}
69+
if (data == null) {
70+
return data;
71+
} else if (typeof value === "string") {
72+
return map(data, field(value), arrayType);
73+
} else if (typeof value === "function") {
74+
return map(data, value, arrayType);
75+
} else if (typeof value === "number" || value instanceof Date || typeof value === "boolean") {
76+
return map(data, constant(value), arrayType);
77+
}
78+
return arrayify(value, arrayType); // preserve undefined type
79+
}
80+
81+
function isTransform(value: ValueAccessor): value is TransformMethod {
82+
return !!value && isObject(value) && typeof (value as {transform?: any}).transform == "function";
6483
}
6584

6685
/**
@@ -76,9 +95,9 @@ export type ValueAccessor =
7695
| ValueArray
7796
| null
7897
| undefined;
79-
type AccessorFunction = ((d: any) => V) | ((d: any, i: number) => V);
98+
type AccessorFunction = ((d: Datum) => Value) & ((d: Datum, i: number) => Value);
8099
type TransformMethod = {
81-
transform: (data: Data | null | undefined) => Data;
100+
transform: (data: Data | null | undefined) => ArrayLike<Value> | Iterable<Value> | null | undefined;
82101
};
83102

84103
// Type: the field accessor might crash if the datum is not a generic object
@@ -120,9 +139,13 @@ export function percentile(reduce: percentile) {
120139
// tuple [channel, constant] where one of the two is undefined, and the other is
121140
// the given value. If you wish to reference a named field that is also a valid
122141
// CSS color, use an accessor (d => d.red) instead.
123-
export function maybeColorChannel(value: ValueAccessor, defaultValue?: string): [ValueAccessor?, string?] {
142+
export function maybeColorChannel(value: string | ValueAccessor, defaultValue?: string): [ValueAccessor?, string?] {
124143
if (value === undefined) value = defaultValue;
125-
return value === null ? [undefined, "none"] : isColor(value) ? [undefined, value as string] : [value, undefined];
144+
return value === null
145+
? [undefined, "none"]
146+
: isColor(value as string)
147+
? [undefined, value as string]
148+
: [value as ValueAccessor, undefined];
126149
}
127150

128151
// Similar to maybeColorChannel, this tests whether the given value is a number
@@ -148,26 +171,28 @@ export function keyword(input: string | null | undefined, name: string, allowed:
148171
// type is provided (e.g., Array), then the returned array will strictly be of
149172
// the specified type; otherwise, any array or typed array may be returned. If
150173
// the specified data is null or undefined, returns the value as-is.
151-
export function arrayify(data: undefined, type: ArrayType | undefined): undefined;
152-
export function arrayify(data: null, type: ArrayType | undefined): null;
153-
export function arrayify(data: Data): Array<Datum> | TypedArray;
154-
export function arrayify(data: Data, type: ArrayConstructor): Array<Datum>;
155-
export function arrayify(data: Data, type: Float32ArrayConstructor): Float32Array;
156-
export function arrayify(data: Data, type: Float64ArrayConstructor): Float64Array;
157-
export function arrayify(data: Data, type?: ArrayType): Array<Datum> | Float32Array | Float64Array;
158-
export function arrayify(data: Data | null | undefined): Array<Datum> | TypedArray | null | undefined;
159-
export function arrayify(data: Data | null | undefined, type: ArrayConstructor): Array<Datum> | null | undefined;
160-
export function arrayify(data: Data | null | undefined, type: Float32ArrayConstructor): Float32Array | null | undefined;
161-
export function arrayify(data: Data | null | undefined, type: Float64ArrayConstructor): Float64Array | null | undefined;
162-
export function arrayify(
163-
data: Data | null | undefined,
164-
type?: ArrayType
165-
): TypedArray | Array<Datum> | null | undefined {
174+
export function arrayify<N extends null | undefined>(data: N, type: ArrayType | undefined): N;
175+
export function arrayify<N extends null | undefined | TypedArray>(data: N): N;
176+
export function arrayify<N extends null | undefined | TypedArray>(data: N, type: undefined): N;
177+
export function arrayify(data: TypedArray, type: TypedArrayConstructor | undefined): TypedArray;
178+
export function arrayify(data: Value[] | Iterable<Value>, type: ArrayConstructor | undefined): Value[];
179+
export function arrayify(data: ValueArray | Iterable<Value>): ValueArray;
180+
export function arrayify(data: ValueArray | Iterable<Value>, type: ArrayConstructor | undefined): ValueArray;
181+
export function arrayify(data: ValueArray | Iterable<Value>, type: TypedArrayConstructor): TypedArray;
182+
export function arrayify(data: ValueArray, type: ArrayType | undefined): ValueArray;
183+
export function arrayify(data: Data, type?: ArrayType): DataArray | TypedArray;
184+
export function arrayify(data: Data): DataArray;
185+
export function arrayify(data: Data, type: ArrayConstructor): DataArray;
186+
export function arrayify(data: Data, type: TypedArrayConstructor): TypedArray;
187+
export function arrayify(data: Data, type?: ArrayType): DataArray | TypedArray;
188+
export function arrayify(data: Data | null | undefined, type?: ArrayType): DataArray | ValueArray | null | undefined {
166189
return data == null
167190
? data
168191
: type === undefined
169-
? data instanceof Array || data instanceof TypedArray
170-
? (data as ValueArray)
192+
? data instanceof Array
193+
? data
194+
: data instanceof TypedArray
195+
? (data as TypedArray)
171196
: Array.from(data)
172197
: data instanceof type
173198
? data
@@ -176,14 +201,47 @@ export function arrayify(
176201

177202
// An optimization of type.from(values, f): if the given values are already an
178203
// instanceof the desired array type, the faster values.map method is used.
179-
type U = Data;
180-
type V = Value;
181-
export function map<V extends Value>(values: U, f: (d: any, i: number) => V, type: ArrayConstructor): V[];
182-
export function map(values: U, f: (d: any, i: number) => number, type: Float32ArrayConstructor): Float32Array;
183-
export function map(values: U, f: (d: any, i: number) => number, type: Float64ArrayConstructor): Float64Array;
184-
export function map(values: U, f: AccessorFunction, type: ArrayType | undefined): V[];
185-
export function map(values: U, f: AccessorFunction): V[];
186-
export function map(values: U, f: (d: any, i: number) => any, type: ArrayType = Array) {
204+
export function map(index: Series, f: (d: index, i: number) => Value): ValueArray;
205+
export function map(values: ValueArray, f: (d: Value, i: number) => Value): ValueArray;
206+
export function map(
207+
values: ValueArray,
208+
f: (d: any, i: number) => Value,
209+
type: ArrayConstructor | undefined
210+
): ValueArray;
211+
export function map(
212+
values: ValueArray,
213+
f: (d: Value, i: number) => number,
214+
type: Float32ArrayConstructor
215+
): Float32Array;
216+
export function map(
217+
values: ValueArray,
218+
f: (d: Value, i: number) => number,
219+
type: Float64ArrayConstructor
220+
): Float64Array;
221+
export function map(
222+
values: ArrayLike<Value> | Iterable<Value>,
223+
f: (d: any, i: number) => number,
224+
type: Float32ArrayConstructor
225+
): Float32Array;
226+
export function map(
227+
values: ArrayLike<Value> | Iterable<Value>,
228+
f: (d: any, i: number) => number,
229+
type: Float64ArrayConstructor
230+
): Float64Array;
231+
export function map(values: Data, f: AccessorFunction, type: ArrayType | undefined): Value[];
232+
export function map(values: Data, f: AccessorFunction): Value[];
233+
export function map(values: Data, f: (d: any, i: number) => any, type: ArrayType | undefined): Value[];
234+
export function map(values: Data, f: (d: any, i: number) => Value, type: ArrayConstructor): Value[];
235+
export function map(values: Data, f: (d: any, i: number) => number, type: Float32ArrayConstructor): Float32Array;
236+
export function map(values: Data, f: (d: any, i: number) => number, type: Float64ArrayConstructor): Float64Array;
237+
export function map(values: Data, f: AccessorFunction, type: ArrayType | undefined): Value[];
238+
export function map(values: Data, f: AccessorFunction): Value[];
239+
export function map(values: Data, f: (d: any, i: number) => any, type: ArrayType | undefined): Value[];
240+
export function map(
241+
values: Data | ArrayLike<Value> | Iterable<Value>,
242+
f: (d: any, i: number) => any,
243+
type: ArrayType = Array
244+
): ValueArray {
187245
return values instanceof type ? values.map(f) : (type as ArrayConstructor).from(values, f);
188246
}
189247

@@ -227,12 +285,12 @@ export function isDomainSort(sort: any): boolean {
227285
}
228286

229287
// For marks specified either as [0, x] or [x1, x2], such as areas and bars.
230-
type Identity = {transform: (d: Datum) => Datum};
231-
export function maybeZero(
288+
type Identity<T extends Datum> = {transform: (d: T) => T};
289+
export function maybeZero<T extends Datum>(
232290
x: ValueAccessor | undefined,
233291
x1: ValueAccessor | undefined | 0,
234-
x2: ValueAccessor | undefined | Identity | 0,
235-
x3 = identity
292+
x2: ValueAccessor | undefined | Identity<T> | 0,
293+
x3 = identity as Identity<T>
236294
) {
237295
if (x1 === undefined && x2 === undefined) {
238296
// {x} or {}
@@ -256,7 +314,11 @@ export function maybeTuple<T>(x: T | undefined, y: T | undefined): [T | undefine
256314

257315
// A helper for extracting the z channel, if it is variable. Used by transforms
258316
// that require series, such as moving average and normalize.
259-
type ZOptions = {fill?: ValueAccessor; stroke?: ValueAccessor; z?: ValueAccessor};
317+
type ZOptions = {
318+
fill?: ValueAccessor | string;
319+
stroke?: ValueAccessor | string;
320+
z?: ValueAccessor;
321+
};
260322
export function maybeZ({z, fill, stroke}: ZOptions = {}) {
261323
if (z === undefined) [z] = maybeColorChannel(fill);
262324
if (z === undefined) [z] = maybeColorChannel(stroke);
@@ -333,8 +395,8 @@ export function maybeColumn(source: ValueAccessor | ((data: DataArray) => void))
333395
export function labelof(value: any, defaultValue?: string) {
334396
return typeof value === "string"
335397
? value
336-
: value && (value as {label?: string}).label !== undefined
337-
? (value as {label?: string}).label
398+
: value && (value as {label: string}).label != null
399+
? (value as {label: string}).label
338400
: defaultValue;
339401
}
340402

@@ -348,8 +410,8 @@ export function mid(x1: getColumn, x2: getColumn) {
348410
const X1 = x1.transform();
349411
const X2 = x2.transform();
350412
return isTemporal(X1) || isTemporal(X2)
351-
? map(X1, (_: Date, i: index) => new Date((+(X1[i] as number) + +(X2[i] as number)) / 2))
352-
: map(X1, (_: Date, i: index) => (+(X1[i] as number) + +(X2[i] as number)) / 2, Float64Array);
413+
? map(X1, (_, i) => new Date((+(X1[i] as number) + +(X2[i] as number)) / 2))
414+
: map(X1, (_, i) => (+(X1[i] as number) + +(X2[i] as number)) / 2, Float64Array);
353415
},
354416
label: x1.label
355417
};
@@ -363,7 +425,7 @@ export function maybeValue(value: any) {
363425
// Coerces the given channel values (if any) to numbers. This is useful when
364426
// values will be interpolated into other code, such as an SVG transform, and
365427
// where we don’t wish to allow unexpected behavior for weird input.
366-
export function numberChannel(source: ValueArray) {
428+
export function numberChannel(source: any) {
367429
return source == null
368430
? null
369431
: {

test/array/arrayify-test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import assert from "assert";
2+
import {arrayify} from "../../src/options.js";
3+
4+
it("arrayify null, undefined", () => {
5+
for (const a of [null, undefined]) {
6+
assert.strictEqual(arrayify(a), a);
7+
assert.strictEqual(arrayify(a, Float32Array), a);
8+
assert.strictEqual(arrayify(a, Float64Array), a);
9+
assert.strictEqual(arrayify(a, Array), a);
10+
}
11+
});
12+
13+
it("arrayify typed arrays", () => {
14+
const a = new Uint8ClampedArray(10);
15+
assert.strictEqual(arrayify(a), a);
16+
assert.strictEqual("" + arrayify(a, Float64Array), "" + a);
17+
assert.notStrictEqual(arrayify(a, Float64Array), a);
18+
assert.strictEqual(arrayify(a, Float64Array)[Symbol.toStringTag], "Float64Array");
19+
assert.strictEqual("" + arrayify(a, Array), "" + a);
20+
assert.notStrictEqual(arrayify(a, Array), a);
21+
assert.strictEqual(arrayify(a, Array)[Symbol.toStringTag], undefined);
22+
});
23+
24+
it("arrayify arrays", () => {
25+
const a = [1, "test", 1.5];
26+
assert.strictEqual(arrayify(a), a);
27+
assert.strictEqual(arrayify(a, undefined), a);
28+
assert.strictEqual(arrayify(a, Array), a);
29+
assert.deepStrictEqual("" + arrayify(a, Float64Array), "" + [1, NaN, 1.5]);
30+
assert.deepStrictEqual("" + arrayify(a, Uint16Array), "" + [1, 0, 1]);
31+
});

test/array/valueof-test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import assert from "assert";
2+
import {valueof} from "../../src/options.js";
3+
4+
it("valueof reads arrays", () => {
5+
assert.deepStrictEqual(
6+
valueof([1, 2, 3], (d: unknown) => d as number),
7+
[1, 2, 3]
8+
);
9+
assert.deepStrictEqual(
10+
valueof([1, 2, 3], (d: unknown) => `${d}`, Array),
11+
["1", "2", "3"]
12+
);
13+
assert.deepStrictEqual(
14+
valueof([1, 2, 3], (d: unknown) => d as number, Float64Array),
15+
Float64Array.of(1, 2, 3)
16+
);
17+
18+
// data can be functions or other things
19+
assert.deepStrictEqual(
20+
valueof([(d: number) => d, new Promise(() => {})], (d: unknown) => `(${d})`),
21+
["(d=>d)", "([object Promise])"]
22+
);
23+
24+
// data can be nullish and generated by the transform method
25+
assert.deepStrictEqual(valueof(undefined, {transform: () => [1, "text"]}), [1, "text"]);
26+
assert.deepStrictEqual(valueof(null, {transform: () => [1, "text"]}, Float32Array), Float32Array.of(1, NaN));
27+
assert.deepStrictEqual(valueof(null, {transform: () => new Float64Array(2)}, Array), [0, 0]);
28+
29+
// ts type tests
30+
valueof([1, 2, 3], (d: unknown) => d as number, Float32Array);
31+
valueof(["1", 2, 3], (d: unknown) => d as string | number);
32+
valueof(["1", 2, 3], (d: unknown) => d as string | number, Array);
33+
valueof(["1", 2, 3], (d: unknown) => d as string | number, Float64Array);
34+
valueof(["1", 2, 3], (d: unknown) => +(d as number), Float32Array);
35+
valueof(new Set(["1", 2, 3]), (d: unknown) => +(d as number), Float32Array);
36+
});
37+
38+
it("valueof does not crash on non iterable values with an accessor", () => {
39+
for (const n of [null, undefined])
40+
assert.strictEqual(
41+
valueof(n, () => 1),
42+
n
43+
);
44+
});
45+
46+
/*
47+
48+
// field names are inferred
49+
valueof([{a: 1}, {b: 2}], "a");
50+
valueof([{a: 1}, {b: 2}], "b");
51+
52+
// TODO: test for ts failure:
53+
valueof([{a: 1}, {b: 2}], "c");
54+
55+
*/

0 commit comments

Comments
 (0)