forked from observablehq/plot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathoptions.js
442 lines (393 loc) · 15 KB
/
options.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
import {parse as isoParse} from "isoformat";
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);
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)
: valueType === "function"
? map(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
}
export const field = (name) => (d) => d[name];
export const indexOf = (d, i) => i;
/** @jsdoc identity */
export const identity = {transform: (d) => d};
export const zero = () => 0;
export const one = () => 1;
export const yes = () => true;
export const string = (x) => (x == null ? x : `${x}`);
export const number = (x) => (x == null ? x : +x);
export const boolean = (x) => (x == null ? x : !!x);
export const first = (x) => (x ? x[0] : undefined);
export const second = (x) => (x ? x[1] : undefined);
export const third = (x) => (x ? x[2] : undefined);
export const constant = (x) => () => x;
// Converts a string like “p25” into a function that takes an index I and an
// accessor function f, returning the corresponding percentile value.
export function percentile(reduce) {
const p = +`${reduce}`.slice(1) / 100;
return (I, f) => quantile(I, p, f);
}
// 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
// tuple [channel, constant] where one of the two is undefined, and the other is
// the given value. If you wish to reference a named field that is also a valid
// CSS color, use an accessor (d => d.red) instead.
export function maybeColorChannel(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null ? [undefined, "none"] : isColor(value) ? [undefined, value] : [value, undefined];
}
// Similar to maybeColorChannel, this tests whether the given value is a number
// indicating a constant, and otherwise assumes that it’s a channel value.
export function maybeNumberChannel(value, defaultValue) {
if (value === undefined) value = defaultValue;
return value === null || typeof value === "number" ? [undefined, value] : [value, undefined];
}
// Validates the specified optional string against the allowed list of keywords.
export function maybeKeyword(input, name, allowed) {
if (input != null) return keyword(input, name, allowed);
}
// Validates the specified required string against the allowed list of keywords.
export function keyword(input, name, allowed) {
const i = `${input}`.toLowerCase();
if (!allowed.includes(i)) throw new Error(`invalid ${name}: ${input}`);
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);
}
// An optimization of type.from(values, f): if the given values are already an
// instanceof the desired array type, the faster values.map method is used.
export function map(values, f, type = Array) {
return values instanceof type ? values.map(f) : type.from(values, f);
}
// An optimization of type.from(values): if the given values are already an
// instanceof the desired array type, the faster values.slice method is used.
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;
}
// Disambiguates a scale options object (e.g., {color: {type: "linear"}}) from
// some other option (e.g., {color: "red"}). When creating standalone legends,
// this is used to test whether a scale is defined; this should be consistent
// with inferScaleType when there are no channels associated with the scale, and
// if this returns true, then normalizeScale must return non-null.
export function isScaleOptions(option) {
return isObject(option) && (option.type !== undefined || option.domain !== undefined);
}
// Disambiguates an options object (e.g., {y: "x2"}) from a channel value
// definition expressed as a channel transform (e.g., {transform: …}).
export function isOptions(option) {
return isObject(option) && typeof option.transform !== "function";
}
// Disambiguates a sort transform (e.g., {sort: "date"}) from a channel domain
// sort definition (e.g., {sort: {y: "x"}}).
export function isDomainSort(sort) {
return isOptions(sort) && sort.value === undefined && sort.channel === undefined;
}
// For marks specified either as [0, x] or [x1, x2], such as areas and bars.
export function maybeZero(x, x1, x2, x3 = identity) {
if (x1 === undefined && x2 === undefined) {
// {x} or {}
(x1 = 0), (x2 = x === undefined ? x3 : x);
} else if (x1 === undefined) {
// {x, x2} or {x2}
x1 = x === undefined ? 0 : x;
} else if (x2 === undefined) {
// {x, x1} or {x1}
x2 = x === undefined ? 0 : x;
}
return [x1, x2];
}
// For marks that have x and y channels (e.g., cell, dot, line, text).
export function maybeTuple(x, y) {
return x === undefined && y === undefined ? [first, second] : [x, y];
}
// A helper for extracting the z channel, if it is variable. Used by transforms
// that require series, such as moving average and normalize.
export function maybeZ({z, fill, stroke} = {}) {
if (z === undefined) [z] = maybeColorChannel(fill);
if (z === undefined) [z] = maybeColorChannel(stroke);
return z;
}
// Returns a Uint32Array with elements [0, 1, 2, … data.length - 1].
export function range(data) {
const n = data.length;
const r = new Uint32Array(n);
for (let i = 0; i < n; ++i) r[i] = i;
return r;
}
// Returns a filtered range of data given the test function.
export function where(data, test) {
return range(data).filter((i) => test(data[i], i, data));
}
// Returns an array [values[index[0]], values[index[1]], …].
export function take(values, index) {
return map(index, (i) => values[i]);
}
// Based on InternMap (d3.group).
export function keyof(value) {
return value !== null && typeof value === "object" ? value.valueOf() : value;
}
export function maybeInput(key, options) {
if (options[key] !== undefined) return options[key];
switch (key) {
case "x1":
case "x2":
key = "x";
break;
case "y1":
case "y2":
key = "y";
break;
}
return options[key];
}
/** @jsdoc column */
export function column(source) {
// Defines a column whose values are lazily populated by calling the returned
// setter. If the given source is labeled, the label is propagated to the
// returned column definition.
let value;
return [
{
transform: () => value,
label: labelof(source)
},
(v) => (value = v)
];
}
// Like column, but allows the source to be null.
export function maybeColumn(source) {
return source == null ? [source] : column(source);
}
export function labelof(value, defaultValue) {
return typeof value === "string" ? value : value && value.label !== undefined ? value.label : defaultValue;
}
// Assuming that both x1 and x2 and lazy columns (per above), this derives a new
// a column that’s the average of the two, and which inherits the column label
// (if any). Both input columns are assumed to be quantitative. If either column
// is temporal, the returned column is also temporal.
export function mid(x1, x2) {
return {
transform(data) {
const X1 = x1.transform(data);
const X2 = x2.transform(data);
return isTemporal(X1) || isTemporal(X2)
? map(X1, (_, i) => new Date((+X1[i] + +X2[i]) / 2))
: map(X1, (_, i) => (+X1[i] + +X2[i]) / 2, Float64Array);
},
label: x1.label
};
}
// If interval is not nullish, converts interval shorthand such as a number (for
// multiples) or a time interval name (such as “day”) to a {floor, offset,
// range} object similar to a D3 time interval.
export function maybeInterval(interval, type) {
if (interval == null) return;
if (typeof interval === "number") {
const n = interval;
return {
floor: (d) => n * Math.floor(d / n),
offset: (d) => d + n, // note: no optional step for simplicity
range: (lo, hi) => rangei(Math.ceil(lo / n), hi / n).map((x) => n * x)
};
}
if (typeof interval === "string") return (type === "time" ? maybeTimeInterval : maybeUtcInterval)(interval);
if (typeof interval.floor !== "function") throw new Error("invalid interval; missing floor method");
if (typeof interval.offset !== "function") throw new Error("invalid interval; missing offset method");
return interval;
}
// This distinguishes between per-dimension options and a standalone value.
export function maybeValue(value) {
return value === undefined || isOptions(value) ? value : {value};
}
// Coerces the given channel values (if any) to numbers. This is useful when
// values will be interpolated into other code, such as an SVG transform, and
// where we don’t wish to allow unexpected behavior for weird input.
export function numberChannel(source) {
return source == null
? null
: {
transform: (data) => valueof(data, source, Float64Array),
label: labelof(source)
};
}
export function isTuples(data) {
if (!isIterable(data)) return false;
for (const d of data) {
if (d == null) continue;
return typeof d === "object" && "0" in d && "1" in d;
}
}
export function isIterable(value) {
return value && typeof value[Symbol.iterator] === "function";
}
export function isTextual(values) {
for (const value of values) {
if (value == null) continue;
return typeof value !== "object" || value instanceof Date;
}
}
export function isOrdinal(values) {
for (const value of values) {
if (value == null) continue;
const type = typeof value;
return type === "string" || type === "boolean";
}
}
export function isTemporal(values) {
for (const value of values) {
if (value == null) continue;
return value instanceof Date;
}
}
// Are these strings that might represent dates? This is stricter than ISO 8601
// because we want to ignore false positives on numbers; for example, the string
// "1192" is more likely to represent a number than a date even though it is
// valid ISO 8601 representing 1192-01-01.
export function isTemporalString(values) {
for (const value of values) {
if (value == null) continue;
return typeof value === "string" && isNaN(value) && isoParse(value);
}
}
// Are these strings that might represent numbers? This is stricter than
// coercion because we want to ignore false positives on e.g. empty strings.
export function isNumericString(values) {
for (const value of values) {
if (value == null) continue;
if (typeof value !== "string") return false;
if (!value.trim()) continue;
return !isNaN(value);
}
}
export function isNumeric(values) {
for (const value of values) {
if (value == null) continue;
return typeof value === "number";
}
}
// Returns true if every non-null value in the specified iterable of values
// passes the specified predicate, and there is at least one non-null value;
// returns false if at least one non-null value does not pass the specified
// predicate; otherwise returns undefined (as if all values are null).
export function isEvery(values, is) {
let every;
for (const value of values) {
if (value == null) continue;
if (!is(value)) return false;
every = true;
}
return every;
}
// Mostly relies on d3-color, with a few extra color keywords. Currently this
// strictly requires that the value be a string; we might want to apply string
// coercion here, though note that d3-color instances would need to support
// valueOf to work correctly with InternMap.
// https://www.w3.org/TR/SVG11/painting.html#SpecifyingPaint
export function isColor(value) {
if (typeof value !== "string") return false;
value = value.toLowerCase().trim();
return (
value === "none" ||
value === "currentcolor" ||
(value.startsWith("url(") && value.endsWith(")")) || // <funciri>, e.g. pattern or gradient
(value.startsWith("var(") && value.endsWith(")")) || // CSS variable
color(value) !== null
);
}
export function isNoneish(value) {
return value == null || isNone(value);
}
export function isNone(value) {
return /^\s*none\s*$/i.test(value);
}
export function isRound(value) {
return /^\s*round\s*$/i.test(value);
}
export function maybeFrameAnchor(value = "middle") {
return keyword(value, "frameAnchor", [
"middle",
"top-left",
"top",
"top-right",
"right",
"bottom-right",
"bottom",
"bottom-left",
"left"
]);
}
// Like a sort comparator, returns a positive value if the given array of values
// is in ascending order, a negative value if the values are in descending
// order. Assumes monotonicity; only tests the first and last values.
export function orderof(values) {
if (values == null) return;
const first = values[0];
const last = values[values.length - 1];
return descending(first, last);
}
// Unlike {...defaults, ...options}, this ensures that any undefined (but
// present) properties in options inherit the given default value.
export function inherit(options = {}, ...rest) {
let o = options;
for (const defaults of rest) {
for (const key in defaults) {
if (o[key] === undefined) {
const value = defaults[key];
if (o === options) o = {...o, [key]: value};
else o[key] = value;
}
}
}
return o;
}
// Given an iterable of named things (objects with a name property), returns a
// corresponding object with properties associated with the given name.
export function Named(things) {
console.warn("named iterables are deprecated; please use an object instead");
const names = new Set();
return Object.fromEntries(
Array.from(things, (thing) => {
const {name} = thing;
if (name == null) throw new Error("missing name");
const key = `${name}`;
if (key === "__proto__") throw new Error(`illegal name: ${key}`);
if (names.has(key)) throw new Error(`duplicate name: ${key}`);
names.add(key);
return [name, thing];
})
);
}
export function maybeNamed(things) {
return isIterable(things) ? Named(things) : things;
}