From a1c8c3a56baecfdfb89c278ee262c80cfa5e3afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 19 Jul 2022 13:37:05 +0200 Subject: [PATCH 01/23] context js => ts --- src/context.js | 9 --------- src/context.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 9 deletions(-) delete mode 100644 src/context.js create mode 100644 src/context.ts diff --git a/src/context.js b/src/context.js deleted file mode 100644 index 7603225f78..0000000000 --- a/src/context.js +++ /dev/null @@ -1,9 +0,0 @@ -import {creator, select} from "d3"; - -export function Context({document = window.document} = {}) { - return {document}; -} - -export function create(name, {document}) { - return select(creator(name).call(document.documentElement)); -} diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 0000000000..6f739a1857 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,13 @@ +import {creator, select} from "d3"; + +interface IContext { + document: Document; +} + +export function Context({document = window.document} = {}): IContext { + return {document}; +} + +export function create(name: string, {document}: IContext) { + return select(creator(name).call(document.documentElement)); +} From bd69294e53471955aa088a8e888a05ad45da1452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 19 Jul 2022 15:13:17 +0200 Subject: [PATCH 02/23] down to 2 errors in options.ts --- src/{options.js => options.ts} | 196 +++++++++++++++++++++------------ 1 file changed, 125 insertions(+), 71 deletions(-) rename src/{options.js => options.ts} (63%) diff --git a/src/options.js b/src/options.ts similarity index 63% rename from src/options.js rename to src/options.ts index 8f25ecaaeb..09ef09d110 100644 --- a/src/options.js +++ b/src/options.ts @@ -1,3 +1,50 @@ +type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + +type PXX = `p${Digit}${Digit}`; + +type DataSource = Iterable | ArrayLike; + +type DataSourceOptional = DataSource | null | undefined; + +type UnknownFn = (d: unknown) => unknown; + +type UserOption = unknown; + +type Field = string | UnknownFn; + +type ConstantOrFieldOption = number | Field | undefined; + +interface UserOptionsDefined { + x?: ConstantOrFieldOption; + x1?: ConstantOrFieldOption; + x2?: ConstantOrFieldOption; + y?: ConstantOrFieldOption; + y1?: ConstantOrFieldOption; + y2?: ConstantOrFieldOption; + z?: ConstantOrFieldOption; + fill?: ConstantOrFieldOption; + stroke?: ConstantOrFieldOption; + filter?: ConstantOrFieldOption; + transform?: ConstantOrFieldOption; +} + +type UserOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; + +type UserOptions = UserOptionsDefined | undefined; + +type ObjectDatum = Record; + +type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; + +type IAccessor = (d: any, i: number, data?: ArrayLike) => any; + +type booleanish = boolean | undefined; + +interface ITransform { + transform: (data: DataSource) => DataSource; +} + + import {parse as isoParse} from "isoformat"; import {color, descending, quantile} from "d3"; @@ -6,33 +53,33 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; // This allows transforms to behave equivalently to channels. -export function valueof(data, value, arrayType) { +export function valueof(data: DataSource, value: string | IAccessor | number | Date | ITransform, arrayType?: ArrayType) { const type = typeof value; - return type === "string" ? map(data, field(value), arrayType) - : type === "function" ? map(data, value, arrayType) - : type === "number" || value instanceof Date || type === "boolean" ? map(data, constant(value), arrayType) - : value && typeof value.transform === "function" ? arrayify(value.transform(data), arrayType) - : arrayify(value, arrayType); // preserve undefined type + return type === "string" ? map(data, field(value as string), arrayType) + : type === "function" ? map(data, value as IAccessor, arrayType) + : type === "number" || value instanceof Date || type === "boolean" ? map(data, constant(value), arrayType) + : value && typeof (value as ITransform).transform === "function" ? arrayify((value as ITransform).transform(data), arrayType) + : arrayify(value as DataSource, arrayType); // preserve undefined type } -export const field = name => d => d[name]; -export const indexOf = (d, i) => i; -export const identity = {transform: d => d}; +export const field = (name: string) => (d: ObjectDatum) => d[name]; +export const indexOf = (d: ObjectDatum, i: number) => i; +export const identity = {transform: (d: ObjectDatum) => 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 constant = x => () => x; +export const string = (x: any) => x == null ? x : `${x}`; +export const number = (x: any) => x == null ? x : +x; +export const boolean = (x: any) => x == null ? x : !!x; +export const first = (x: any[]) => x ? x[0] : undefined; +export const second = (x: any[]) => x ? x[1] : undefined; +export const constant = (x: any) => () => 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) { +export function percentile(reduce: PXX) { const p = +`${reduce}`.slice(1) / 100; - return (I, f) => quantile(I, p, f); + return (I: Uint32Array, f: (i: number) => number) => quantile(I, p, f); } // Some channels may allow a string constant to be specified; to differentiate @@ -41,7 +88,7 @@ export function percentile(reduce) { // 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) { +export function maybeColorChannel(value: ConstantOrFieldOption, defaultValue?: string) { if (value === undefined) value = defaultValue; return value === null ? [undefined, "none"] : isColor(value) ? [undefined, value] @@ -50,19 +97,19 @@ export function maybeColorChannel(value, defaultValue) { // 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) { +export function maybeNumberChannel(value: number | null | undefined, defaultValue?: number) { 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) { +export function maybeKeyword(input: string | null | undefined, name: string, allowed: string[]) { 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) { +export function keyword(input: string | null | undefined, name: string, allowed: string[]) { const i = `${input}`.toLowerCase(); if (!allowed.includes(i)) throw new Error(`invalid ${name}: ${input}`); return i; @@ -72,7 +119,7 @@ export function keyword(input, name, allowed) { // 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) { +export function arrayify(data: DataSourceOptional, type?: ArrayType) { return data == null ? data : (type === undefined ? (data instanceof Array || data instanceof TypedArray) ? data : Array.from(data) : (data instanceof type ? data : type.from(data))); @@ -80,22 +127,22 @@ export function arrayify(data, type) { // 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) { +export function map(values: DataSource, f: IAccessor, type: ArrayType = 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) { +export function slice(values: DataSource, type = Array) { return values instanceof type ? values.slice() : type.from(values); } -export function isTypedArray(values) { +export function isTypedArray(values: any) { return values instanceof TypedArray; } // Disambiguates an options object (e.g., {y: "x2"}) from a primitive value. -export function isObject(option) { +export function isObject(option: any): boolean { return option?.toString === objectToString; } @@ -104,24 +151,24 @@ export function isObject(option) { // 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); +export function isScaleOptions(option: any): boolean { + 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) { +export function isOptions(option: any) { 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) { +export function isDomainSort(sort: any) { 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) { +export function maybeZero(x: UserOption, x1: UserOption, x2: UserOption, x3: UserOption = identity) { if (x1 === undefined && x2 === undefined) { // {x} or {} x1 = 0, x2 = x === undefined ? x3 : x; } else if (x1 === undefined) { // {x, x2} or {x2} @@ -133,20 +180,20 @@ export function maybeZero(x, x1, x2, x3 = identity) { } // For marks that have x and y channels (e.g., cell, dot, line, text). -export function maybeTuple(x, y) { +export function maybeTuple(x: UserOption, y: UserOption) { 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} = {}) { +export function maybeZ({z, fill, stroke}: UserOptions = {}) { 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) { +export function range(data: ArrayLike): Uint32Array { const n = data.length; const r = new Uint32Array(n); for (let i = 0; i < n; ++i) r[i] = i; @@ -154,21 +201,21 @@ export function range(data) { } // Returns a filtered range of data given the test function. -export function where(data, test) { +export function where(data: ArrayLike, test: IAccessor) { return range(data).filter(i => test(data[i], i, data)); } // Returns an array [values[index[0]], values[index[1]], …]. -export function take(values, index) { +export function take(values: ArrayLike, index: number[]) { return map(index, i => values[i]); } // Based on InternMap (d3.group). -export function keyof(value) { +export function keyof(value: any) { return value !== null && typeof value === "object" ? value.valueOf() : value; } -export function maybeInput(key, options) { +export function maybeInput(key: UserOptionsKey, options: UserOptionsDefined): UserOption { if (options[key] !== undefined) return options[key]; switch (key) { case "x1": case "x2": key = "x"; break; @@ -180,8 +227,15 @@ export function maybeInput(key, options) { // 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. -export function column(source) { - let value; +interface LazyColumnOptions { + transform: () => Array; + label?: string +} +type LazyColumnSetter = (v: Array) => Array; +type LazyColumn = [ LazyColumnOptions | null | undefined, LazyColumnSetter? ]; + +export function column(source: UserOption): LazyColumn { + let value: Array; return [ { transform: () => value, @@ -192,11 +246,11 @@ export function column(source) { } // Like column, but allows the source to be null. -export function maybeColumn(source) { - return source == null ? [source] : column(source); +export function maybeColumn(source: UserOption) { + return source == null ? [source] as LazyColumn : column(source); } -export function labelof(value, defaultValue) { +export function labelof(value: any, defaultValue?: string) { return typeof value === "string" ? value : value && value.label !== undefined ? value.label : defaultValue; @@ -206,11 +260,11 @@ export function labelof(value, defaultValue) { // 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) { +export function mid(x1: LazyColumnOptions, x2: LazyColumnOptions) { return { - transform(data) { - const X1 = x1.transform(data); - const X2 = x2.transform(data); + transform() { + const X1 = x1.transform(); // there was a type error here!! + const X2 = x2.transform(); return isTemporal(X1) || isTemporal(X2) ? map(X1, (_, i) => new Date((+X1[i] + +X2[i]) / 2)) : map(X1, (_, i) => (+X1[i] + +X2[i]) / 2, Float64Array); @@ -220,32 +274,32 @@ export function mid(x1, x2) { } // This distinguishes between per-dimension options and a standalone value. -export function maybeValue(value) { +export function maybeValue(value: any) { 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) { +export function numberChannel(source: any) { return source == null ? null : { - transform: data => valueof(data, source, Float64Array), + transform: (data: DataSource) => valueof(data, source, Float64Array), label: labelof(source) }; } -export function isIterable(value) { +export function isIterable(value: any): boolean { return value && typeof value[Symbol.iterator] === "function"; } -export function isTextual(values) { +export function isTextual(values: any[]): booleanish { for (const value of values) { if (value == null) continue; return typeof value !== "object" || value instanceof Date; } } -export function isOrdinal(values) { +export function isOrdinal(values: any[]): booleanish { for (const value of values) { if (value == null) continue; const type = typeof value; @@ -253,7 +307,7 @@ export function isOrdinal(values) { } } -export function isTemporal(values) { +export function isTemporal(values: any[]): booleanish { for (const value of values) { if (value == null) continue; return value instanceof Date; @@ -264,30 +318,30 @@ export function isTemporal(values) { // 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) { +export function isTemporalString(values: any[]): booleanish { for (const value of values) { if (value == null) continue; - return typeof value === "string" && isNaN(value) && isoParse(value); + return typeof value === "string" && isNaN(value as unknown as number) && !!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) { +export function isNumericString(values: any[]): booleanish { for (const value of values) { if (value == null || value === "") continue; - return typeof value === "string" && !isNaN(value); + return typeof value === "string" && !isNaN(value as unknown as number); } } -export function isNumeric(values) { +export function isNumeric(values: any[]): booleanish { for (const value of values) { if (value == null) continue; return typeof value === "number"; } } -export function isFirst(values, is) { +export function isFirst(values: any[], is: (d: any) => boolean): booleanish { for (const value of values) { if (value == null) continue; return is(value); @@ -298,7 +352,7 @@ export function isFirst(values, is) { // an empty array, this tests all defined values and only returns true if all of // them are valid colors. It also returns true for an empty array, and thus // should generally be used in conjunction with isFirst. -export function isEvery(values, is) { +export function isEvery(values: any[], is: (d: any) => boolean): boolean { for (const value of values) { if (value == null) continue; if (!is(value)) return false; @@ -311,25 +365,25 @@ export function isEvery(values, is) { // 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) { +export function isColor(value: UserOption): boolean { if (typeof value !== "string") return false; - value = value.toLowerCase().trim(); + value = value.toLowerCase().trim(); // !! typescript does not infer value as string (??) return value === "none" || value === "currentcolor" - || (value.startsWith("url(") && value.endsWith(")")) // , e.g. pattern or gradient - || (value.startsWith("var(") && value.endsWith(")")) // CSS variable - || color(value) !== null; + || ((value as string).startsWith("url(") && (value as string).endsWith(")")) // , e.g. pattern or gradient + || ((value as string).startsWith("var(") && (value as string).endsWith(")")) // CSS variable + || color(value as string) !== null; } -export function isNoneish(value) { +export function isNoneish(value: undefined | null | string) { return value == null || isNone(value); } -export function isNone(value) { +export function isNone(value: string) { return /^\s*none\s*$/i.test(value); } -export function isRound(value) { +export function isRound(value: string) { return /^\s*round\s*$/i.test(value); } @@ -340,7 +394,7 @@ export function maybeFrameAnchor(value = "middle") { // 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 order(values) { +export function order(values : null | undefined | any[]) { if (values == null) return; const first = values[0]; const last = values[values.length - 1]; @@ -349,7 +403,7 @@ export function order(values) { // Unlike {...defaults, ...options}, this ensures that any undefined (but // present) properties in options inherit the given default value. -export function inherit(options = {}, ...rest) { +export function inherit(options: Record = {}, ...rest : Array>) { let o = options; for (const defaults of rest) { for (const key in defaults) { From f37061ce1c5cfd1ccd50737a713f53ee72da553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 19 Jul 2022 17:20:10 +0200 Subject: [PATCH 03/23] no more errors, but 39 warnings about _any_ --- src/options.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/options.ts b/src/options.ts index 09ef09d110..add35ed499 100644 --- a/src/options.ts +++ b/src/options.ts @@ -122,13 +122,13 @@ export function keyword(input: string | null | undefined, name: string, allowed: export function arrayify(data: DataSourceOptional, type?: ArrayType) { return data == null ? data : (type === undefined ? (data instanceof Array || data instanceof TypedArray) ? data : Array.from(data) - : (data instanceof type ? data : type.from(data))); + : (data instanceof type ? data : (type as ArrayConstructor).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: DataSource, f: IAccessor, type: ArrayType = Array) { - return values instanceof type ? values.map(f) : type.from(values, f); + return values instanceof type ? values.map(f) : (type as ArrayConstructor).from(values, f); } // An optimization of type.from(values): if the given values are already an @@ -152,7 +152,7 @@ export function isObject(option: any): boolean { // 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: any): boolean { - return isObject(option) && option.type !== undefined || option.domain !== undefined; + return isObject(option) && (option.type !== undefined || option.domain !== undefined); } // Disambiguates an options object (e.g., {y: "x2"}) from a channel value From caa9e17758f34e6e6b7272a33305ab258b412e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 09:57:28 +0200 Subject: [PATCH 04/23] stats.ts --- src/{stats.js => stats.ts} | 52 +++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) rename src/{stats.js => stats.ts} (83%) diff --git a/src/stats.js b/src/stats.ts similarity index 83% rename from src/stats.js rename to src/stats.ts index f99d97d3cb..c61ff9cc69 100644 --- a/src/stats.js +++ b/src/stats.ts @@ -20,12 +20,12 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -export function ibetainv(p, a, b) { - var EPS = 1e-8; - var a1 = a - 1; - var b1 = b - 1; - var j = 0; - var lna, lnb, pp, t, u, err, x, al, h, w, afac; +export function ibetainv(p: number, a: number, b: number) { + const EPS = 1e-8; + const a1 = a - 1; + const b1 = b - 1; + let j = 0; + let lna, lnb, pp, t, u, err, x, al, h, w; if (p <= 0) return 0; if (p >= 1) return 1; if (a >= 1 && b >= 1) { @@ -48,10 +48,10 @@ export function ibetainv(p, a, b) { if (p < t / w) x = Math.pow(a * w * p, 1 / a); else x = 1 - Math.pow(b * w * (1 - p), 1 / b); } - afac = -gammaln(a) - gammaln(b) + gammaln(a + b); + const afac = -gammaln(a) - gammaln(b) + gammaln(a + b); for (; j < 10; j++) { if (x === 0 || x === 1) return x; - err = ibeta(x, a, b) - p; + err = ibeta(x, a, b) as number - p; t = Math.exp(a1 * Math.log(x) + b1 * Math.log(1 - x) + afac); u = err / t; x -= t = u / (1 - 0.5 * Math.min(1, u * (a1 / x - b1 / (1 - x)))); @@ -62,9 +62,9 @@ export function ibetainv(p, a, b) { return x; } -export function ibeta(x, a, b) { +export function ibeta(x: number, a: number, b: number) { // Factors in front of the continued fraction. - var bt = + const bt = x === 0 || x === 1 ? 0 : Math.exp( @@ -82,15 +82,15 @@ export function ibeta(x, a, b) { return 1 - (bt * betacf(1 - x, b, a)) / b; } -export function betacf(x, a, b) { - var fpmin = 1e-30; - var m = 1; - var qab = a + b; - var qap = a + 1; - var qam = a - 1; - var c = 1; - var d = 1 - (qab * x) / qap; - var m2, aa, del, h; +export function betacf(x: number, a: number, b: number) { + const fpmin = 1e-30; + let m = 1; + const qab = a + b; + const qap = a + 1; + const qam = a - 1; + let c = 1; + let d = 1 - (qab * x) / qap; + let m2, aa, del, h; // These q's will be used in factors that occur in the coefficients if (Math.abs(d) < fpmin) d = fpmin; @@ -122,22 +122,22 @@ export function betacf(x, a, b) { return h; } -export function gammaln(x) { - var j = 0; - var cof = [ +export function gammaln(x: number) { + let j = 0; + const cof = [ 76.18009172947146, -86.5053203294167, 24.01409824083091, -1.231739572450155, 0.1208650973866179e-2, -0.5395239384953e-5 ]; - var ser = 1.000000000190015; - var xx, y, tmp; + let ser = 1.000000000190015; + let xx, y, tmp; tmp = (y = xx = x) + 5.5; tmp -= (xx + 0.5) * Math.log(tmp); for (; j < 6; j++) ser += cof[j] / ++y; return Math.log((2.506628274631 * ser) / xx) - tmp; } -export function qt(p, dof) { - var x = ibetainv(2 * Math.min(p, 1 - p), 0.5 * dof, 0.5); +export function qt(p: number, dof: number) { + let x = ibetainv(2 * Math.min(p, 1 - p), 0.5 * dof, 0.5); x = Math.sqrt((dof * (1 - x)) / x); return p > 0.5 ? x : -x; } From c30ac1166134466a6ddf2ccf974fc5bdd57d4869 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 10:10:35 +0200 Subject: [PATCH 05/23] symbols.ts --- src/{symbols.js => symbols.ts} | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) rename src/{symbols.js => symbols.ts} (72%) diff --git a/src/symbols.js b/src/symbols.ts similarity index 72% rename from src/symbols.js rename to src/symbols.ts index e35aaec694..9d1f91647b 100644 --- a/src/symbols.js +++ b/src/symbols.ts @@ -1,10 +1,14 @@ +type SymbolString = "asterisk" | "circle" | "cross" | "diamond" | "diamond2" | "hexagon" | "plus" | "square" | "square2" | "star" | "times" | "triangle" | "triangle2" | "wye"; +type SymbolObject = {draw: (context: CanvasPath, size: number) => void}; +type MaybeSymbol = SymbolString | SymbolObject | null | undefined; + import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3"; import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3"; export const sqrt3 = Math.sqrt(3); export const sqrt4_3 = 2 / sqrt3; -const symbolHexagon = { +const symbolHexagon: SymbolObject = { draw(context, size) { const rx = Math.sqrt(size / Math.PI), ry = rx * sqrt4_3, hy = ry / 2; context.moveTo(0, ry); @@ -34,24 +38,24 @@ const symbols = new Map([ ["wye", symbolWye] ]); -function isSymbolObject(value) { - return value && typeof value.draw === "function"; +function isSymbolObject(value: MaybeSymbol) { + return value && typeof (value as SymbolObject).draw === "function"; } -export function isSymbol(value) { +export function isSymbol(value: MaybeSymbol) { if (isSymbolObject(value)) return true; if (typeof value !== "string") return false; return symbols.has(value.toLowerCase()); } -export function maybeSymbol(symbol) { +export function maybeSymbol(symbol: MaybeSymbol) { if (symbol == null || isSymbolObject(symbol)) return symbol; const value = symbols.get(`${symbol}`.toLowerCase()); if (value) return value; throw new Error(`invalid symbol: ${symbol}`); } -export function maybeSymbolChannel(symbol) { +export function maybeSymbolChannel(symbol: MaybeSymbol) { if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol]; if (typeof symbol === "string") { const value = symbols.get(`${symbol}`.toLowerCase()); From b5cd4e9873b5e8d1d4eee31e4fac6332c7a288ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 10:13:47 +0200 Subject: [PATCH 06/23] scales/index.ts (no change!) --- src/scales/{index.js => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/scales/{index.js => index.ts} (100%) diff --git a/src/scales/index.js b/src/scales/index.ts similarity index 100% rename from src/scales/index.js rename to src/scales/index.ts From aabfbee1c98feace4e1ae940e90c7ef295fa4d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 10:41:35 +0200 Subject: [PATCH 07/23] scales/schemes.ts --- src/scales/{schemes.js => schemes.ts} | 95 ++++++++++++++------------- 1 file changed, 49 insertions(+), 46 deletions(-) rename src/scales/{schemes.js => schemes.ts} (62%) diff --git a/src/scales/schemes.js b/src/scales/schemes.ts similarity index 62% rename from src/scales/schemes.js rename to src/scales/schemes.ts index 923d72c59e..3a94a8902c 100644 --- a/src/scales/schemes.js +++ b/src/scales/schemes.ts @@ -1,3 +1,6 @@ +type ColorInterpolator = (t: number) => string; // t is in [0, 1] +type OrdinalScheme = string[] | (({length}: {length: number}) => string[]); // n is the number of colors + import { interpolateBlues, interpolateBrBG, @@ -91,27 +94,27 @@ const ordinalSchemes = new Map([ ["tableau10", schemeTableau10], // diverging - ["brbg", scheme11(schemeBrBG, interpolateBrBG)], - ["prgn", scheme11(schemePRGn, interpolatePRGn)], - ["piyg", scheme11(schemePiYG, interpolatePiYG)], - ["puor", scheme11(schemePuOr, interpolatePuOr)], - ["rdbu", scheme11(schemeRdBu, interpolateRdBu)], - ["rdgy", scheme11(schemeRdGy, interpolateRdGy)], - ["rdylbu", scheme11(schemeRdYlBu, interpolateRdYlBu)], - ["rdylgn", scheme11(schemeRdYlGn, interpolateRdYlGn)], - ["spectral", scheme11(schemeSpectral, interpolateSpectral)], + ["brbg", scheme11(schemeBrBG as Array, interpolateBrBG)], + ["prgn", scheme11(schemePRGn as Array, interpolatePRGn)], + ["piyg", scheme11(schemePiYG as Array, interpolatePiYG)], + ["puor", scheme11(schemePuOr as Array, interpolatePuOr)], + ["rdbu", scheme11(schemeRdBu as Array, interpolateRdBu)], + ["rdgy", scheme11(schemeRdGy as Array, interpolateRdGy)], + ["rdylbu", scheme11(schemeRdYlBu as Array, interpolateRdYlBu)], + ["rdylgn", scheme11(schemeRdYlGn as Array, interpolateRdYlGn)], + ["spectral", scheme11(schemeSpectral as Array, interpolateSpectral)], // reversed diverging (for temperature data) - ["burd", scheme11r(schemeRdBu, interpolateRdBu)], - ["buylrd", scheme11r(schemeRdYlBu, interpolateRdYlBu)], + ["burd", scheme11r(schemeRdBu as Array, interpolateRdBu)], + ["buylrd", scheme11r(schemeRdYlBu as Array, interpolateRdYlBu)], // sequential (single-hue) - ["blues", scheme9(schemeBlues, interpolateBlues)], - ["greens", scheme9(schemeGreens, interpolateGreens)], - ["greys", scheme9(schemeGreys, interpolateGreys)], - ["oranges", scheme9(schemeOranges, interpolateOranges)], - ["purples", scheme9(schemePurples, interpolatePurples)], - ["reds", scheme9(schemeReds, interpolateReds)], + ["blues", scheme9(schemeBlues as Array, interpolateBlues)], + ["greens", scheme9(schemeGreens as Array, interpolateGreens)], + ["greys", scheme9(schemeGreys as Array, interpolateGreys)], + ["oranges", scheme9(schemeOranges as Array, interpolateOranges)], + ["purples", scheme9(schemePurples as Array, interpolatePurples)], + ["reds", scheme9(schemeReds as Array, interpolateReds)], // sequential (multi-hue) ["turbo", schemei(interpolateTurbo)], @@ -123,26 +126,26 @@ const ordinalSchemes = new Map([ ["cubehelix", schemei(interpolateCubehelixDefault)], ["warm", schemei(interpolateWarm)], ["cool", schemei(interpolateCool)], - ["bugn", scheme9(schemeBuGn, interpolateBuGn)], - ["bupu", scheme9(schemeBuPu, interpolateBuPu)], - ["gnbu", scheme9(schemeGnBu, interpolateGnBu)], - ["orrd", scheme9(schemeOrRd, interpolateOrRd)], - ["pubu", scheme9(schemePuBu, interpolatePuBu)], - ["pubugn", scheme9(schemePuBuGn, interpolatePuBuGn)], - ["purd", scheme9(schemePuRd, interpolatePuRd)], - ["rdpu", scheme9(schemeRdPu, interpolateRdPu)], - ["ylgn", scheme9(schemeYlGn, interpolateYlGn)], - ["ylgnbu", scheme9(schemeYlGnBu, interpolateYlGnBu)], - ["ylorbr", scheme9(schemeYlOrBr, interpolateYlOrBr)], - ["ylorrd", scheme9(schemeYlOrRd, interpolateYlOrRd)], + ["bugn", scheme9(schemeBuGn as Array, interpolateBuGn)], + ["bupu", scheme9(schemeBuPu as Array, interpolateBuPu)], + ["gnbu", scheme9(schemeGnBu as Array, interpolateGnBu)], + ["orrd", scheme9(schemeOrRd as Array, interpolateOrRd)], + ["pubu", scheme9(schemePuBu as Array, interpolatePuBu)], + ["pubugn", scheme9(schemePuBuGn as Array, interpolatePuBuGn)], + ["purd", scheme9(schemePuRd as Array, interpolatePuRd)], + ["rdpu", scheme9(schemeRdPu as Array, interpolateRdPu)], + ["ylgn", scheme9(schemeYlGn as Array, interpolateYlGn)], + ["ylgnbu", scheme9(schemeYlGnBu as Array, interpolateYlGnBu)], + ["ylorbr", scheme9(schemeYlOrBr as Array, interpolateYlOrBr)], + ["ylorrd", scheme9(schemeYlOrRd as Array, interpolateYlOrRd)], // cyclical ["rainbow", schemeicyclical(interpolateRainbow)], ["sinebow", schemeicyclical(interpolateSinebow)] -]); +] as Array<[string, OrdinalScheme]>); -function scheme9(scheme, interpolate) { - return ({length: n}) => { +function scheme9(scheme: string[][], interpolate: ColorInterpolator) { + return ({length: n}: {length: number}) => { if (n === 1) return [scheme[3][1]]; // favor midpoint if (n === 2) return [scheme[3][1], scheme[3][2]]; // favor darker n = Math.max(3, Math.floor(n)); @@ -150,37 +153,37 @@ function scheme9(scheme, interpolate) { }; } -function scheme11(scheme, interpolate) { - return ({length: n}) => { +function scheme11(scheme: string[][], interpolate: ColorInterpolator) { + return ({length: n}: {length: number}) => { if (n === 2) return [scheme[3][0], scheme[3][2]]; // favor diverging extrema n = Math.max(3, Math.floor(n)); return n > 11 ? quantize(interpolate, n) : scheme[n]; }; } -function scheme11r(scheme, interpolate) { - return ({length: n}) => { +function scheme11r(scheme: string[][], interpolate: ColorInterpolator) { + return ({length: n}: {length: number}) => { if (n === 2) return [scheme[3][2], scheme[3][0]]; // favor diverging extrema n = Math.max(3, Math.floor(n)); return n > 11 ? quantize(t => interpolate(1 - t), n) : scheme[n].slice().reverse(); }; } -function schemei(interpolate) { - return ({length: n}) => quantize(interpolate, Math.max(2, Math.floor(n))); +function schemei(interpolate: ColorInterpolator) { + return ({length: n}: {length: number}) => quantize(interpolate, Math.max(2, Math.floor(n))); } -function schemeicyclical(interpolate) { - return ({length: n}) => quantize(interpolate, Math.floor(n) + 1).slice(0, -1); +function schemeicyclical(interpolate: ColorInterpolator) { + return ({length: n}: {length: number}) => quantize(interpolate, Math.floor(n) + 1).slice(0, -1); } -export function ordinalScheme(scheme) { +export function ordinalScheme(scheme: string) { const s = `${scheme}`.toLowerCase(); if (!ordinalSchemes.has(s)) throw new Error(`unknown scheme: ${s}`); - return ordinalSchemes.get(s); + return ordinalSchemes.get(s) as OrdinalScheme; } -export function ordinalRange(scheme, length) { +export function ordinalRange(scheme: string, length: number) { const s = ordinalScheme(scheme); const r = typeof s === "function" ? s({length}) : s; return r.length !== length ? r.slice(0, length) : r; @@ -189,7 +192,7 @@ export function ordinalRange(scheme, length) { // If the specified domain contains only booleans (ignoring null and undefined), // returns a corresponding range where false is mapped to the low color and true // is mapped to the high color of the specified scheme. -export function maybeBooleanRange(domain, scheme = "greys") { +export function maybeBooleanRange(domain: (boolean | number | Date | string | null | undefined)[], scheme = "greys") { const range = new Set(); const [f, t] = ordinalRange(scheme, 2); for (const value of domain) { @@ -253,7 +256,7 @@ const quantitativeSchemes = new Map([ ["sinebow", interpolateSinebow] ]); -export function quantitativeScheme(scheme) { +export function quantitativeScheme(scheme: string) { const s = `${scheme}`.toLowerCase(); if (!quantitativeSchemes.has(s)) throw new Error(`unknown scheme: ${s}`); return quantitativeSchemes.get(s); @@ -273,6 +276,6 @@ const divergingSchemes = new Set([ "buylrd" ]); -export function isDivergingScheme(scheme) { +export function isDivergingScheme(scheme: string | null | undefined) { return scheme != null && divergingSchemes.has(`${scheme}`.toLowerCase()); } From 74934b4db18f5c33e1ae069c61a551dfa087ea56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 12:08:59 +0200 Subject: [PATCH 08/23] marks/marker.ts ; move definitions to common.ts --- src/common.ts | 74 ++++++++++++++++++++++++++++++ src/context.ts | 5 +- src/marks/{marker.js => marker.ts} | 52 +++++++++++++-------- src/options.ts | 60 ++++++------------------ 4 files changed, 124 insertions(+), 67 deletions(-) create mode 100644 src/common.ts rename src/marks/{marker.js => marker.ts} (65%) diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 0000000000..b24d51eac6 --- /dev/null +++ b/src/common.ts @@ -0,0 +1,74 @@ +type UnknownFn = (d: unknown) => unknown; +type Field = string | UnknownFn; +export type DataSource = Iterable | ArrayLike; +export type DataSourceOptional = DataSource | null | undefined; +export type UserOption = unknown; +export type ConstantOrFieldOption = number | Field | undefined; +export interface UserOptionsDefined { + x?: ConstantOrFieldOption; + x1?: ConstantOrFieldOption; + x2?: ConstantOrFieldOption; + y?: ConstantOrFieldOption; + y1?: ConstantOrFieldOption; + y2?: ConstantOrFieldOption; + z?: ConstantOrFieldOption; + fill?: ConstantOrFieldOption; + stroke?: ConstantOrFieldOption; + filter?: ConstantOrFieldOption; + transform?: ConstantOrFieldOption; +} +export type UserOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; +export type UserOptions = UserOptionsDefined | undefined; +export type ObjectDatum = Record; +export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; +export type IAccessor = (d: any, i: number, data?: ArrayLike) => any; +export type booleanish = boolean | undefined; +export interface ITransform { + transform: (data: DataSource) => DataSource; +} + +/** + * The document context, used to create new DOM elements. + * @link https://github.com/observablehq/plot/blob/main/README.md#layout-options + */ +export interface IContext { + document: Document; +} + +/** + * A D3 selection. + */ +export interface ISelection { + attr: (name: string, value: any) => ISelection; +} + +/** + * A mark + * @link https://github.com/observablehq/plot/blob/main/README.md#mark-options + */ +export interface IMark { + marker?: MaybeMarkerFunction; + markerStart?: MaybeMarkerFunction; + markerMid?: MaybeMarkerFunction; + markerEnd?: MaybeMarkerFunction; + stroke?: string; +} + +/* + * Reducers + */ + +type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; +/** + * Percentile reducer, for transforms such as bin, group, map and window + * @link https://github.com/observablehq/plot/blob/main/README.md#bin + */ +export type PXX = `p${Digit}${Digit}`; + +/** + * A marker defines a graphic drawn on vertices of a line or a link mark + * @link https://github.com/observablehq/plot/blob/main/README.md#markers + */ +export type MarkerOption = string | boolean | null | undefined; +export type MarkerFunction = (color: any, context: any) => Element; +export type MaybeMarkerFunction = MarkerFunction | null; diff --git a/src/context.ts b/src/context.ts index 6f739a1857..0143344885 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,9 +1,6 @@ +import {IContext} from "./common.js"; import {creator, select} from "d3"; -interface IContext { - document: Document; -} - export function Context({document = window.document} = {}): IContext { return {document}; } diff --git a/src/marks/marker.js b/src/marks/marker.ts similarity index 65% rename from src/marks/marker.js rename to src/marks/marker.ts index 3b79cc5c5c..883b573553 100644 --- a/src/marks/marker.js +++ b/src/marks/marker.ts @@ -1,17 +1,24 @@ +import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction} from "../common.js"; + import {create} from "../context.js"; -export function markers(mark, { +export function markers(mark: IMark, { marker, markerStart = marker, markerMid = marker, markerEnd = marker +}: { + marker?: MarkerOption, + markerStart?: MarkerOption, + markerMid?: MarkerOption, + markerEnd?: MarkerOption } = {}) { mark.markerStart = maybeMarker(markerStart); mark.markerMid = maybeMarker(markerMid); mark.markerEnd = maybeMarker(markerEnd); } -function maybeMarker(marker) { +function maybeMarker(marker: MarkerOption): MaybeMarkerFunction { if (marker == null || marker === false) return null; if (marker === true) return markerCircleFill; if (typeof marker === "function") return marker; @@ -25,7 +32,7 @@ function maybeMarker(marker) { throw new Error(`invalid marker: ${marker}`); } -function markerArrow(color, context) { +function markerArrow(color: string, context: IContext) { return create("svg:marker", context) .attr("viewBox", "-5 -5 10 10") .attr("markerWidth", 6.67) @@ -37,10 +44,10 @@ function markerArrow(color, context) { .attr("stroke-linecap", "round") .attr("stroke-linejoin", "round") .call(marker => marker.append("path").attr("d", "M-1.5,-3l3,3l-3,3")) - .node(); + .node() as Element; } -function markerDot(color, context) { +function markerDot(color: string, context: IContext) { return create("svg:marker", context) .attr("viewBox", "-5 -5 10 10") .attr("markerWidth", 6.67) @@ -48,10 +55,10 @@ function markerDot(color, context) { .attr("fill", color) .attr("stroke", "none") .call(marker => marker.append("circle").attr("r", 2.5)) - .node(); + .node() as Element; } -function markerCircleFill(color, context) { +function markerCircleFill(color: string, context: IContext) { return create("svg:marker", context) .attr("viewBox", "-5 -5 10 10") .attr("markerWidth", 6.67) @@ -60,10 +67,10 @@ function markerCircleFill(color, context) { .attr("stroke", "white") .attr("stroke-width", 1.5) .call(marker => marker.append("circle").attr("r", 3)) - .node(); + .node() as Element; } -function markerCircleStroke(color, context) { +function markerCircleStroke(color: string, context: IContext) { return create("svg:marker", context) .attr("viewBox", "-5 -5 10 10") .attr("markerWidth", 6.67) @@ -72,31 +79,40 @@ function markerCircleStroke(color, context) { .attr("stroke", color) .attr("stroke-width", 1.5) .call(marker => marker.append("circle").attr("r", 3)) - .node(); + .node() as Element; } let nextMarkerId = 0; -export function applyMarkers(path, mark, {stroke: S} = {}) { - return applyMarkersColor(path, mark, S && (i => S[i])); +export function applyMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke?: string[]} = {}) { + return applyMarkersColor(path, mark, S && ((i: number) => S[i])); } -export function applyGroupedMarkers(path, mark, {stroke: S} = {}) { - return applyMarkersColor(path, mark, S && (([i]) => S[i])); +export function applyGroupedMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke?: string[]} = {}) { + return applyMarkersColor(path, mark, S && (([i]: number[]) => S[i])); } -function applyMarkersColor(path, {markerStart, markerMid, markerEnd, stroke}, strokeof = () => stroke) { +function applyMarkersColor( + path: ISelection, + { + markerStart, + markerMid, + markerEnd, + stroke + }: IMark, + strokeof: ((i: any) => string | undefined) = (() => stroke) // any is really number or number[] +) { const iriByMarkerColor = new Map(); - function applyMarker(marker) { - return function(i) { + function applyMarker(marker: MarkerFunction) { + return function(this: Element, i: number | [number]) { const color = strokeof(i); let iriByColor = iriByMarkerColor.get(marker); if (!iriByColor) iriByMarkerColor.set(marker, iriByColor = new Map()); let iri = iriByColor.get(color); if (!iri) { const context = {document: this.ownerDocument}; - const node = this.parentNode.insertBefore(marker(color, context), this); + const node = (this.parentNode as Element).insertBefore(marker(color, context), this) as Element; const id = `plot-marker-${++nextMarkerId}`; node.setAttribute("id", id); iriByColor.set(color, iri = `url(#${id})`); diff --git a/src/options.ts b/src/options.ts index add35ed499..61d581760d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,48 +1,18 @@ -type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; - -type PXX = `p${Digit}${Digit}`; - -type DataSource = Iterable | ArrayLike; - -type DataSourceOptional = DataSource | null | undefined; - -type UnknownFn = (d: unknown) => unknown; - -type UserOption = unknown; - -type Field = string | UnknownFn; - -type ConstantOrFieldOption = number | Field | undefined; - -interface UserOptionsDefined { - x?: ConstantOrFieldOption; - x1?: ConstantOrFieldOption; - x2?: ConstantOrFieldOption; - y?: ConstantOrFieldOption; - y1?: ConstantOrFieldOption; - y2?: ConstantOrFieldOption; - z?: ConstantOrFieldOption; - fill?: ConstantOrFieldOption; - stroke?: ConstantOrFieldOption; - filter?: ConstantOrFieldOption; - transform?: ConstantOrFieldOption; -} - -type UserOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; - -type UserOptions = UserOptionsDefined | undefined; - -type ObjectDatum = Record; - -type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; - -type IAccessor = (d: any, i: number, data?: ArrayLike) => any; - -type booleanish = boolean | undefined; - -interface ITransform { - transform: (data: DataSource) => DataSource; -} +import { + PXX, + DataSource, + DataSourceOptional, + UserOption, + ConstantOrFieldOption, + UserOptionsDefined, + UserOptionsKey, + UserOptions, + ObjectDatum, + ArrayType, + IAccessor, + booleanish, + ITransform +} from "./common.js"; import {parse as isoParse} from "isoformat"; From 6628b596f507ac2f4bfcfddea8cc0609537a3e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 14:48:43 +0200 Subject: [PATCH 09/23] style.ts --- src/common.ts | 82 ++++++++++++++++++-- src/format.ts | 4 +- src/marks/marker.ts | 5 +- src/options.ts | 23 +++--- src/{style.js => style.ts} | 152 ++++++++++++++++++++++++------------- 5 files changed, 197 insertions(+), 69 deletions(-) rename src/{style.js => style.ts} (63%) diff --git a/src/common.ts b/src/common.ts index b24d51eac6..6c1b3c491f 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,9 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + type UnknownFn = (d: unknown) => unknown; type Field = string | UnknownFn; +export type nullish = null | undefined; export type DataSource = Iterable | ArrayLike; -export type DataSourceOptional = DataSource | null | undefined; +export type DataSourceOptional = DataSource | nullish; export type UserOption = unknown; -export type ConstantOrFieldOption = number | Field | undefined; +export type booleanOption = boolean | nullish; +export type numberOption = number | nullish; +export type stringOption = number | any[] | string | nullish; +export type TextChannel = string[]; +export type NumberChannel = string[]; +type Channel = TextChannel | NumberChannel | any[]; +export type ConstantOrFieldOption = numberOption | stringOption | Field | Channel | nullish; + export interface UserOptionsDefined { x?: ConstantOrFieldOption; x1?: ConstantOrFieldOption; @@ -36,22 +46,84 @@ export interface IContext { } /** - * A D3 selection. + * A restrictive definition of D3 selections */ export interface ISelection { + append: (name: string) => ISelection; attr: (name: string, value: any) => ISelection; + call: (callback: (selection: ISelection, ...args: any[]) => void, ...args: any[]) => ISelection; + each: (callback: (d: any) => void) => ISelection; + filter: (filter: (d: any, i: number) => boolean) => ISelection; + property: (name: string, value: any) => ISelection; + style: (name: string, value: any) => ISelection; + text: (value: any) => ISelection; + [Symbol.iterator]: () => IterableIterator; +} + +/** + * A restrictive definition of D3 scales + */ +export interface IScale { + bandwidth?: () => number; } +/** + * An object of style definitions to apply to DOM elements + */ +export type IStyleObject = Record; + /** * A mark * @link https://github.com/observablehq/plot/blob/main/README.md#mark-options */ export interface IMark { + z?: UserOption; // copy the user option for error messages + clip?: "frame"; + dx: number; + dy: number; marker?: MaybeMarkerFunction; markerStart?: MaybeMarkerFunction; markerMid?: MaybeMarkerFunction; markerEnd?: MaybeMarkerFunction; - stroke?: string; + stroke?: string | nullish; + // common styles + fill?: string | nullish; + fillOpacity?: number | nullish; + strokeWidth?: number | nullish; + strokeOpacity?: number | nullish; + strokeLinejoin?: string | nullish; + strokeLinecap?: string | nullish; + strokeMiterlimit?: number | nullish; + strokeDasharray?: string | nullish; + strokeDashoffset?: string | nullish; + target?: string | nullish; + ariaLabel?: string | nullish; + ariaDescription?: string | nullish; + ariaHidden?: string | nullish; // "true" | "false" | undefined + opacity?: number | nullish; + mixBlendMode?: string | nullish; + paintOrder?: string | nullish; + pointerEvents?: string | nullish; + shapeRendering?: string | nullish; + // other styles, some of which are not supported by all marks + frameAnchor?: string; +} + +/** + * A key: value record of channels values + */ +export type ChannelObject = Record; + +/** + * The dimensions of the plot or the facet + */ +export interface IDimensions { + width: number; + height: number; + marginLeft: number; + marginRight: number; + marginTop: number; + marginBottom: number; } /* @@ -69,6 +141,6 @@ export type PXX = `p${Digit}${Digit}`; * A marker defines a graphic drawn on vertices of a line or a link mark * @link https://github.com/observablehq/plot/blob/main/README.md#markers */ -export type MarkerOption = string | boolean | null | undefined; +export type MarkerOption = string | boolean | nullish; export type MarkerFunction = (color: any, context: any) => Element; export type MaybeMarkerFunction = MarkerFunction | null; diff --git a/src/format.ts b/src/format.ts index 56c0f6658d..b7876a82a7 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import {nullish} from "./common.js"; + import {format as isoFormat} from "isoformat"; import {string} from "./options.js"; import {memoize1} from "./memoize.js"; @@ -26,7 +28,7 @@ export function formatIsoDate(date: Date): string { return isoFormat(date, "Invalid Date"); } -export function formatAuto(locale = "en-US"): (value: any) => string | number | undefined { +export function formatAuto(locale = "en-US"): (value: any) => string | number | nullish { const number = formatNumber(locale); return (v: any) => (v instanceof Date ? formatIsoDate : typeof v === "number" ? number : string)(v); } diff --git a/src/marks/marker.ts b/src/marks/marker.ts index 883b573553..d324787ce2 100644 --- a/src/marks/marker.ts +++ b/src/marks/marker.ts @@ -1,4 +1,5 @@ -import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction} from "../common.js"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction, nullish} from "../common.js"; import {create} from "../context.js"; @@ -100,7 +101,7 @@ function applyMarkersColor( markerEnd, stroke }: IMark, - strokeof: ((i: any) => string | undefined) = (() => stroke) // any is really number or number[] + strokeof: ((i: any) => string | nullish) = (() => stroke) // any is really number or number[] ) { const iriByMarkerColor = new Map(); diff --git a/src/options.ts b/src/options.ts index 61d581760d..c359d14be4 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { PXX, DataSource, @@ -11,7 +12,9 @@ import { ArrayType, IAccessor, booleanish, - ITransform + ITransform, + stringOption, + nullish } from "./common.js"; @@ -38,8 +41,8 @@ export const identity = {transform: (d: ObjectDatum) => d}; export const zero = () => 0; export const one = () => 1; export const yes = () => true; -export const string = (x: any) => x == null ? x : `${x}`; -export const number = (x: any) => x == null ? x : +x; +export const string = (x: any) => x == null ? x as nullish : `${x}`; +export const number = (x: any) => x == null ? x as nullish : +x; export const boolean = (x: any) => x == null ? x : !!x; export const first = (x: any[]) => x ? x[0] : undefined; export const second = (x: any[]) => x ? x[1] : undefined; @@ -58,10 +61,10 @@ export function percentile(reduce: PXX) { // 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: ConstantOrFieldOption, defaultValue?: string) { +export function maybeColorChannel(value: ConstantOrFieldOption, defaultValue?: string): [any, string | undefined] { if (value === undefined) value = defaultValue; return value === null ? [undefined, "none"] - : isColor(value) ? [undefined, value] + : isColor(value) ? [undefined, value as string] : [value, undefined]; } @@ -345,16 +348,16 @@ export function isColor(value: UserOption): boolean { || color(value as string) !== null; } -export function isNoneish(value: undefined | null | string) { +export function isNoneish(value: stringOption) { return value == null || isNone(value); } -export function isNone(value: string) { - return /^\s*none\s*$/i.test(value); +export function isNone(value: stringOption) { + return /^\s*none\s*$/i.test(value as string); } -export function isRound(value: string) { - return /^\s*round\s*$/i.test(value); +export function isRound(value: stringOption) { + return /^\s*round\s*$/i.test(value as string); } export function maybeFrameAnchor(value = "middle") { diff --git a/src/style.js b/src/style.ts similarity index 63% rename from src/style.js rename to src/style.ts index 1bc98a9369..9c2cabb544 100644 --- a/src/style.js +++ b/src/style.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {IMark, IDimensions, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; + import {group, namespaces} from "d3"; import {defined, nonempty} from "./defined.js"; import {formatDefault} from "./format.js"; @@ -9,7 +12,7 @@ export const offset = typeof window !== "undefined" && window.devicePixelRatio > let nextClipId = 0; export function styles( - mark, + mark: IMark, { title, href, @@ -32,7 +35,29 @@ export function styles( paintOrder, pointerEvents, shapeRendering - }, + } : { + title?: stringOption, + href?: stringOption, + ariaLabel: stringOption, + ariaDescription?: stringOption, + ariaHidden: booleanOption, + target?: stringOption, + fill?: stringOption, + fillOpacity?: numberOption, + stroke?: stringOption, + strokeWidth?: numberOption, + strokeOpacity?: numberOption, + strokeLinejoin?: stringOption, + strokeLinecap?: stringOption, + strokeMiterlimit?: numberOption, + strokeDasharray?: stringOption, + strokeDashoffset?: stringOption, + opacity?: numberOption, + mixBlendMode: stringOption, + paintOrder?: stringOption, + pointerEvents?: stringOption, + shapeRendering?: stringOption + } /* options */, { ariaLabel: cariaLabel, fill: defaultFill = "currentColor", @@ -44,7 +69,18 @@ export function styles( strokeLinejoin: defaultStrokeLinejoin, strokeMiterlimit: defaultStrokeMiterlimit, paintOrder: defaultPaintOrder - } + } : { + ariaLabel?: string, + fill?: string, + fillOpacity?: number, + stroke?: string, + strokeOpacity?: number, + strokeWidth?: number, + strokeLinecap?: string, + strokeLinejoin?: string, + strokeMiterlimit?: number, + paintOrder?: string + } /* defaults */ ) { // Some marks don’t support fill (e.g., tick and rule). @@ -139,52 +175,66 @@ export function styles( } // Applies the specified titles via selection.call. -export function applyTitle(selection, L) { +export function applyTitle(selection: ISelection, L?: TextChannel) { if (L) selection.filter(i => nonempty(L[i])).append("title").call(applyText, L); } // Like applyTitle, but for grouped data (lines, areas). -export function applyTitleGroup(selection, L) { - if (L) selection.filter(([i]) => nonempty(L[i])).append("title").call(applyTextGroup, L); +export function applyTitleGroup(selection: ISelection, L?: TextChannel) { + if (L) selection.filter(([i]: [number]) => nonempty(L[i])).append("title").call(applyTextGroup, L); } -export function applyText(selection, T) { - if (T) selection.text(i => formatDefault(T[i])); +export function applyText(selection: ISelection, T: TextChannel) { + selection.text((i: number) => formatDefault(T[i])); } -export function applyTextGroup(selection, T) { - if (T) selection.text(([i]) => formatDefault(T[i])); +export function applyTextGroup(selection: ISelection, T: TextChannel) { + selection.text(([i]: [number]) => formatDefault(T[i])); } -export function applyChannelStyles(selection, {target}, {ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) { - if (AL) applyAttr(selection, "aria-label", i => AL[i]); - if (F) applyAttr(selection, "fill", i => F[i]); - if (FO) applyAttr(selection, "fill-opacity", i => FO[i]); - if (S) applyAttr(selection, "stroke", i => S[i]); - if (SO) applyAttr(selection, "stroke-opacity", i => SO[i]); - if (SW) applyAttr(selection, "stroke-width", i => SW[i]); - if (O) applyAttr(selection, "opacity", i => O[i]); - if (H) applyHref(selection, i => H[i], target); +export function applyChannelStyles( + selection: ISelection, + {target}: {target: string | nullish}, + {ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H} +: {ariaLabel?: TextChannel, title?: TextChannel, fill?: TextChannel, fillOpacity?: NumberChannel, stroke?: TextChannel, strokeOpacity?: NumberChannel, strokeWidth?: NumberChannel, opacity?: NumberChannel, href?: TextChannel} +) { + if (AL) applyAttr(selection, "aria-label", (i: number) => AL[i]); + if (F) applyAttr(selection, "fill", (i: number) => F[i]); + if (FO) applyAttr(selection, "fill-opacity", (i: number) => FO[i]); + if (S) applyAttr(selection, "stroke", (i: number) => S[i]); + if (SO) applyAttr(selection, "stroke-opacity", (i: number) => SO[i]); + if (SW) applyAttr(selection, "stroke-width", (i: number) => SW[i]); + if (O) applyAttr(selection, "opacity", (i: number) => O[i]); + if (H) applyHref(selection, (i: number) => H[i], target); applyTitle(selection, T); } -export function applyGroupedChannelStyles(selection, {target}, {ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) { - if (AL) applyAttr(selection, "aria-label", ([i]) => AL[i]); - if (F) applyAttr(selection, "fill", ([i]) => F[i]); - if (FO) applyAttr(selection, "fill-opacity", ([i]) => FO[i]); - if (S) applyAttr(selection, "stroke", ([i]) => S[i]); - if (SO) applyAttr(selection, "stroke-opacity", ([i]) => SO[i]); - if (SW) applyAttr(selection, "stroke-width", ([i]) => SW[i]); - if (O) applyAttr(selection, "opacity", ([i]) => O[i]); - if (H) applyHref(selection, ([i]) => H[i], target); +export function applyGroupedChannelStyles( + selection: ISelection, + {target}: {target: string | nullish}, + {ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H} +: {ariaLabel?: TextChannel, title?: TextChannel, fill?: TextChannel, fillOpacity?: NumberChannel, stroke?: TextChannel, strokeOpacity?: NumberChannel, strokeWidth?: NumberChannel, opacity?: NumberChannel, href?: TextChannel} +) { + if (AL) applyAttr(selection, "aria-label", ([i]: [number]) => AL[i]); + if (F) applyAttr(selection, "fill", ([i]: [number]) => F[i]); + if (FO) applyAttr(selection, "fill-opacity", ([i]: [number]) => FO[i]); + if (S) applyAttr(selection, "stroke", ([i]: [number]) => S[i]); + if (SO) applyAttr(selection, "stroke-opacity", ([i]: [number]) => SO[i]); + if (SW) applyAttr(selection, "stroke-width", ([i]: [number]) => SW[i]); + if (O) applyAttr(selection, "opacity", ([i]: [number]) => O[i]); + if (H) applyHref(selection, ([i]: [number]) => H[i], target); applyTitleGroup(selection, T); } -function groupAesthetics({ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H}) { - return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined); +function groupAesthetics({ + ariaLabel: AL, title: T, fill: F, fillOpacity: FO, stroke: S, strokeOpacity: SO, strokeWidth: SW, opacity: O, href: H +}: { + ariaLabel?: TextChannel, title?: TextChannel, fill?: TextChannel, fillOpacity?: NumberChannel, stroke?: TextChannel, strokeOpacity?: NumberChannel, strokeWidth?: NumberChannel, opacity?: NumberChannel, href?: TextChannel +}) { + return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined) as (TextChannel | NumberChannel)[]; } -export function groupZ(I, Z, z) { +export function groupZ(I: number[], Z: any[], z: UserOption) { const G = group(I, i => Z[i]); if (z === undefined && G.size > I.length >> 1) { warn(`Warning: the implicit z channel has high cardinality. This may occur when the fill or stroke channel is associated with quantitative data rather than ordinal or categorical data. You can suppress this warning by setting the z option explicitly; if this data represents a single series, set z to null.`); @@ -192,7 +242,7 @@ export function groupZ(I, Z, z) { return G.values(); } -export function* groupIndex(I, position, {z}, channels) { +export function* groupIndex(I: number[], position: NumberChannel[], {z}: IMark, channels: ChannelObject) { const {z: Z} = channels; // group channel const A = groupAesthetics(channels); // aesthetic channels const C = [...position, ...A]; // all channels @@ -200,7 +250,7 @@ export function* groupIndex(I, position, {z}, channels) { // Group the current index by Z (if any). for (const G of Z ? groupZ(I, Z, z) : [I]) { let Ag; // the A-values (aesthetics) of the current group, if any - let Gg; // the current group index (a subset of G, and I), if any + let Gg: number[] | undefined; // the current group index (a subset of G, and I), if any out: for (const i of G) { // If any channel has an undefined value for this index, skip it. @@ -222,7 +272,7 @@ export function* groupIndex(I, position, {z}, channels) { // Otherwise, add the current index to the current group. Then, if any of // the aesthetics don’t match the current group, yield the current group // and start a new group of the current index. - Gg.push(i); + (Gg as number[]).push(i); for (let j = 0; j < A.length; ++j) { const k = keyof(A[j][i]); if (k !== Ag[j]) { @@ -241,13 +291,13 @@ export function* groupIndex(I, position, {z}, channels) { // clip: true clips to the frame // TODO: accept other types of clips (paths, urls, x, y, other marks?…) // https://github.com/observablehq/plot/issues/181 -export function maybeClip(clip) { +export function maybeClip(clip: booleanOption) { if (clip === true) return "frame"; if (clip == null || clip === false) return false; throw new Error(`invalid clip method: ${clip}`); } -export function applyIndirectStyles(selection, mark, scales, dimensions) { +export function applyIndirectStyles(selection: ISelection, mark: IMark, scales: Record<"x" | "y", IScale>, dimensions: IDimensions) { applyAttr(selection, "aria-label", mark.ariaLabel); applyAttr(selection, "aria-description", mark.ariaDescription); applyAttr(selection, "aria-hidden", mark.ariaHidden); @@ -280,33 +330,33 @@ export function applyIndirectStyles(selection, mark, scales, dimensions) { } } -export function applyDirectStyles(selection, mark) { +export function applyDirectStyles(selection: ISelection, mark: IMark) { applyStyle(selection, "mix-blend-mode", mark.mixBlendMode); applyAttr(selection, "opacity", mark.opacity); } -function applyHref(selection, href, target) { - selection.each(function(i) { +function applyHref(selection: ISelection, href: (d: any) => string, target: string | nullish) { + selection.each(function(this: Element, i) { const h = href(i); if (h != null) { const a = this.ownerDocument.createElementNS(namespaces.svg, "a"); a.setAttribute("fill", "inherit"); a.setAttributeNS(namespaces.xlink, "href", h); if (target != null) a.setAttribute("target", target); - this.parentNode.insertBefore(a, this).appendChild(this); + (this.parentNode as Element).insertBefore(a, this).appendChild(this); } }); } -export function applyAttr(selection, name, value) { +export function applyAttr(selection: ISelection, name: string, value: any) { if (value != null) selection.attr(name, value); } -export function applyStyle(selection, name, value) { +export function applyStyle(selection: ISelection, name: string, value: any) { if (value != null) selection.style(name, value); } -export function applyTransform(selection, mark, {x, y}, tx = offset, ty = offset) { +export function applyTransform(selection: ISelection, mark: IMark, {x, y}: {x?: IScale, y?: IScale}, tx = offset, ty = offset) { tx += mark.dx; ty += mark.dy; if (x?.bandwidth) tx += x.bandwidth() / 2; @@ -314,24 +364,24 @@ export function applyTransform(selection, mark, {x, y}, tx = offset, ty = offset if (tx || ty) selection.attr("transform", `translate(${tx},${ty})`); } -export function impliedString(value, impliedValue) { +export function impliedString(value: any, impliedValue: string): string | nullish { if ((value = string(value)) !== impliedValue) return value; } -export function impliedNumber(value, impliedValue) { +export function impliedNumber(value: any, impliedValue: number): number | nullish { if ((value = number(value)) !== impliedValue) return value; } const validClassName = /^-?([_a-z]|[\240-\377]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])([_a-z0-9-]|[\240-\377]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*$/; -export function maybeClassName(name) { +export function maybeClassName(name: UserOption) { if (name === undefined) return `plot-${Math.random().toString(16).slice(2)}`; name = `${name}`; - if (!validClassName.test(name)) throw new Error(`invalid class name: ${name}`); + if (!validClassName.test(name as string)) throw new Error(`invalid class name: ${name}`); return name; } -export function applyInlineStyles(selection, style) { +export function applyInlineStyles(selection: ISelection, style: IStyleObject) { if (typeof style === "string") { selection.property("style", style); } else if (style != null) { @@ -341,9 +391,9 @@ export function applyInlineStyles(selection, style) { } } -export function applyFrameAnchor({frameAnchor}, {width, height, marginTop, marginRight, marginBottom, marginLeft}) { +export function applyFrameAnchor({frameAnchor}: IMark, {width, height, marginTop, marginRight, marginBottom, marginLeft}: IDimensions) { return [ - /left$/.test(frameAnchor) ? marginLeft : /right$/.test(frameAnchor) ? width - marginRight : (marginLeft + width - marginRight) / 2, - /^top/.test(frameAnchor) ? marginTop : /^bottom/.test(frameAnchor) ? height - marginBottom : (marginTop + height - marginBottom) / 2 + /left$/.test(frameAnchor as string) ? marginLeft : /right$/.test(frameAnchor as string) ? width - marginRight : (marginLeft + width - marginRight) / 2, + /^top/.test(frameAnchor as string) ? marginTop : /^bottom/.test(frameAnchor as string) ? height - marginBottom : (marginTop + height - marginBottom) / 2 ]; } From cab78083463977f2311cb2a50ec3b953bd83bb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 15:04:18 +0200 Subject: [PATCH 10/23] import type --- src/context.ts | 2 +- src/format.ts | 2 +- src/options.ts | 2 +- src/style.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/context.ts b/src/context.ts index 0143344885..b327604910 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,4 +1,4 @@ -import {IContext} from "./common.js"; +import type {IContext} from "./common.js"; import {creator, select} from "d3"; export function Context({document = window.document} = {}): IContext { diff --git a/src/format.ts b/src/format.ts index b7876a82a7..6fcf29197d 100644 --- a/src/format.ts +++ b/src/format.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {nullish} from "./common.js"; +import type {nullish} from "./common.js"; import {format as isoFormat} from "isoformat"; import {string} from "./options.js"; diff --git a/src/options.ts b/src/options.ts index c359d14be4..e24695402f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { +import type { PXX, DataSource, DataSourceOptional, diff --git a/src/style.ts b/src/style.ts index 9c2cabb544..8d7c7a3e72 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {IMark, IDimensions, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; +import type {IMark, IDimensions, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; import {group, namespaces} from "d3"; import {defined, nonempty} from "./defined.js"; From e568cb553ecab39602a0ac6fae096f22b13ffd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 16:54:31 +0200 Subject: [PATCH 11/23] transforms/basic.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes a crash on Plot.sort(undefined, {…}) --- src/common.ts | 28 ++++++++---- src/options.ts | 16 +++---- src/style.ts | 4 +- src/transforms/{basic.js => basic.ts} | 61 ++++++++++++++------------- 4 files changed, 61 insertions(+), 48 deletions(-) rename src/transforms/{basic.js => basic.ts} (60%) diff --git a/src/common.ts b/src/common.ts index 6c1b3c491f..b88a163284 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -type UnknownFn = (d: unknown) => unknown; -type Field = string | UnknownFn; export type nullish = null | undefined; export type DataSource = Iterable | ArrayLike; export type DataSourceOptional = DataSource | nullish; @@ -10,11 +8,19 @@ export type booleanOption = boolean | nullish; export type numberOption = number | nullish; export type stringOption = number | any[] | string | nullish; export type TextChannel = string[]; -export type NumberChannel = string[]; -type Channel = TextChannel | NumberChannel | any[]; -export type ConstantOrFieldOption = numberOption | stringOption | Field | Channel | nullish; +export type NumberChannel = number[] | Float32Array | Float64Array; +export type Channel = TextChannel | NumberChannel | any[]; +export type ConstantOrFieldOption = number | string | Channel | Date | ITransform | IAccessor | nullish; +export type Comparator = (a: any, b: any) => number; -export interface UserOptionsDefined { +/** + * Definition for both transform and initializer functions. + */ +export type MaybeFacetArray = number[][] | undefined; +export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: number[][], channels?: any}; + + +export interface MarkOptionsDefined { x?: ConstantOrFieldOption; x1?: ConstantOrFieldOption; x2?: ConstantOrFieldOption; @@ -24,11 +30,15 @@ export interface UserOptionsDefined { z?: ConstantOrFieldOption; fill?: ConstantOrFieldOption; stroke?: ConstantOrFieldOption; + filter?: ConstantOrFieldOption; - transform?: ConstantOrFieldOption; + transform?: TransformFunction | null; + sort?: ConstantOrFieldOption; + reverse?: ConstantOrFieldOption; + initializer?: TransformFunction | null; } -export type UserOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; -export type UserOptions = UserOptionsDefined | undefined; +export type MarkOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; +export type MarkOptions = MarkOptionsDefined | undefined; export type ObjectDatum = Record; export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; export type IAccessor = (d: any, i: number, data?: ArrayLike) => any; diff --git a/src/options.ts b/src/options.ts index e24695402f..ec8fc9a0e1 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,9 +5,9 @@ import type { DataSourceOptional, UserOption, ConstantOrFieldOption, - UserOptionsDefined, - UserOptionsKey, - UserOptions, + MarkOptionsDefined, + MarkOptionsKey, + MarkOptions, ObjectDatum, ArrayType, IAccessor, @@ -26,7 +26,7 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; // This allows transforms to behave equivalently to channels. -export function valueof(data: DataSource, value: string | IAccessor | number | Date | ITransform, arrayType?: ArrayType) { +export function valueof(data: DataSource, value: ConstantOrFieldOption, arrayType?: ArrayType) { const type = typeof value; return type === "string" ? map(data, field(value as string), arrayType) : type === "function" ? map(data, value as IAccessor, arrayType) @@ -94,7 +94,7 @@ export function keyword(input: string | null | undefined, name: string, allowed: // the specified data is null or undefined, returns the value as-is. export function arrayify(data: DataSourceOptional, type?: ArrayType) { return data == null ? data : (type === undefined - ? (data instanceof Array || data instanceof TypedArray) ? data : Array.from(data) + ? (data instanceof Array || data instanceof TypedArray) ? data as any[] : Array.from(data) : (data instanceof type ? data : (type as ArrayConstructor).from(data))); } @@ -159,7 +159,7 @@ export function maybeTuple(x: UserOption, y: UserOption) { // 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}: UserOptions = {}) { +export function maybeZ({z, fill, stroke}: MarkOptions = {}) { if (z === undefined) ([z] = maybeColorChannel(fill)); if (z === undefined) ([z] = maybeColorChannel(stroke)); return z; @@ -188,7 +188,7 @@ export function keyof(value: any) { return value !== null && typeof value === "object" ? value.valueOf() : value; } -export function maybeInput(key: UserOptionsKey, options: UserOptionsDefined): UserOption { +export function maybeInput(key: MarkOptionsKey, options: MarkOptionsDefined): UserOption { if (options[key] !== undefined) return options[key]; switch (key) { case "x1": case "x2": key = "x"; break; @@ -247,7 +247,7 @@ export function mid(x1: LazyColumnOptions, x2: LazyColumnOptions) { } // This distinguishes between per-dimension options and a standalone value. -export function maybeValue(value: any) { +export function maybeValue(value: any): undefined | {channel?: string, value?: any} { return value === undefined || isOptions(value) ? value : {value}; } diff --git a/src/style.ts b/src/style.ts index 8d7c7a3e72..393783e19f 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {IMark, IDimensions, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; +import type {IMark, IDimensions, Channel, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; import {group, namespaces} from "d3"; import {defined, nonempty} from "./defined.js"; @@ -234,7 +234,7 @@ function groupAesthetics({ return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined) as (TextChannel | NumberChannel)[]; } -export function groupZ(I: number[], Z: any[], z: UserOption) { +export function groupZ(I: number[], Z: Channel, z: UserOption) { const G = group(I, i => Z[i]); if (z === undefined && G.size > I.length >> 1) { warn(`Warning: the implicit z channel has high cardinality. This may occur when the fill or stroke channel is associated with quantitative data rather than ordinal or categorical data. You can suppress this warning by setting the z option explicitly; if this data represents a single series, set z to null.`); diff --git a/src/transforms/basic.js b/src/transforms/basic.ts similarity index 60% rename from src/transforms/basic.js rename to src/transforms/basic.ts index 02fafd0cb2..5980834a5c 100644 --- a/src/transforms/basic.js +++ b/src/transforms/basic.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable prefer-const */ +import type {IDimensions, IMark, Comparator, Channel, ConstantOrFieldOption, MarkOptions, MaybeFacetArray, TransformFunction, nullish} from "../common.js"; + import {randomLcg} from "d3"; import {ascendingDefined, descendingDefined} from "../defined.js"; import {arrayify, isDomainSort, isOptions, maybeValue, valueof} from "../options.js"; @@ -11,7 +15,7 @@ export function basic({ transform: t1, initializer: i1, ...options -} = {}, t2) { +}: MarkOptions = {}, t2: TransformFunction): MarkOptions { if (t1 === undefined) { // explicit transform overrides filter, sort, and reverse if (f1 != null) t1 = filterTransform(f1); if (s1 != null && !isDomainSort(s1)) t1 = composeTransform(t1, sortTransform(s1)); @@ -33,7 +37,7 @@ export function initializer({ reverse: r1, initializer: i1, ...options -} = {}, i2) { +}: MarkOptions = {}, i2: TransformFunction): MarkOptions { if (i1 === undefined) { // explicit initializer overrides filter, sort, and reverse if (f1 != null) i1 = filterTransform(f1); if (s1 != null && !isDomainSort(s1)) i1 = composeInitializer(i1, sortTransform(s1)); @@ -45,19 +49,19 @@ export function initializer({ }; } -function composeTransform(t1, t2) { +function composeTransform(t1: TransformFunction | nullish, t2: TransformFunction | nullish) { if (t1 == null) return t2 === null ? undefined : t2; if (t2 == null) return t1 === null ? undefined : t1; - return function(data, facets) { + return function(this: IMark, data: any, facets: MaybeFacetArray) { ({data, facets} = t1.call(this, data, facets)); return t2.call(this, arrayify(data), facets); }; } -function composeInitializer(i1, i2) { +function composeInitializer(i1: TransformFunction | nullish, i2: TransformFunction | nullish) { if (i1 == null) return i2 === null ? undefined : i2; if (i2 == null) return i1 === null ? undefined : i1; - return function(data, facets, channels, scales, dimensions) { + return function(this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales?: any, dimensions?: IDimensions) { let c1, d1, f1, c2, d2, f2; ({data: d1 = data, facets: f1 = facets, channels: c1} = i1.call(this, data, facets, channels, scales, dimensions)); ({data: d2 = d1, facets: f2 = f1, channels: c2} = i2.call(this, d1, f1, {...channels, ...c1}, scales, dimensions)); @@ -65,50 +69,50 @@ function composeInitializer(i1, i2) { }; } -function apply(options, t) { +function apply(options: any, t: any): MarkOptions { return (options.initializer != null ? initializer : basic)(options, t); } -export function filter(value, options) { +export function filter(value: ConstantOrFieldOption, options: MarkOptions) { return apply(options, filterTransform(value)); } -function filterTransform(value) { +function filterTransform(value: ConstantOrFieldOption): TransformFunction { return (data, facets) => { - const V = valueof(data, value); - return {data, facets: facets.map(I => I.filter(i => V[i]))}; + const V = valueof(data, value) || []; + return {data, facets: facets && facets.map(I => I.filter((i: number) => V[i]))}; }; } -export function reverse(options) { +export function reverse(options: MarkOptions) { return {...apply(options, reverseTransform), sort: null}; } -function reverseTransform(data, facets) { - return {data, facets: facets.map(I => I.slice().reverse())}; +function reverseTransform(data: any, facets: MaybeFacetArray) { + return {data, facets: facets && facets.map(I => I.slice().reverse())}; } -export function shuffle({seed, ...options} = {}) { +export function shuffle({seed, ...options}: {seed?: number | null} = {}) { return {...apply(options, sortValue(seed == null ? Math.random : randomLcg(seed))), sort: null}; } -export function sort(value, options) { +export function sort(value: any, options: MarkOptions) { return {...(isOptions(value) && value.channel !== undefined ? initializer : apply)(options, sortTransform(value)), sort: null}; } -function sortTransform(value) { +function sortTransform(value: any): TransformFunction { return (typeof value === "function" && value.length !== 1 ? sortData : sortValue)(value); } -function sortData(compare) { - return (data, facets) => { - const compareData = (i, j) => compare(data[i], data[j]); - return {data, facets: facets.map(I => I.slice().sort(compareData))}; +function sortData(compare: Comparator): TransformFunction { + return (data: any, facets: MaybeFacetArray) => { + const compareData = (i: number, j: number) => compare(data[i], data[j]); + return {data, facets: facets && facets.map(I => I.slice().sort(compareData))}; }; } -function sortValue(value) { - let channel, order; +function sortValue(value: any): TransformFunction { + let channel: string | undefined, order: Comparator; ({channel, value, order = ascendingDefined} = {...maybeValue(value)}); if (typeof order !== "function") { switch (`${order}`.toLowerCase()) { @@ -117,17 +121,16 @@ function sortValue(value) { default: throw new Error(`invalid order: ${order}`); } } - return (data, facets, channels) => { - let V; + return (data: any, facets: MaybeFacetArray, channels: any) => { + let V: Channel | nullish; if (channel === undefined) { V = valueof(data, value); } else { if (channels === undefined) throw new Error("channel sort requires an initializer"); - V = channels[channel]; + V = channels[channel]?.value; if (!V) return {}; // ignore missing channel - V = V.value; } - const compareValue = (i, j) => order(V[i], V[j]); - return {data, facets: facets.map(I => I.slice().sort(compareValue))}; + const compareValue = (i: number, j: number) => order((V as Channel)[i], (V as Channel)[j]); + return {data, facets: facets && facets.map((I: number[]) => I.slice().sort(compareValue))}; }; } From c0a7a527980828f122b4786a0e31cd473beedd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 20 Jul 2022 16:57:56 +0200 Subject: [PATCH 12/23] transforms/identity.ts --- src/transforms/{identity.js => identity.ts} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename src/transforms/{identity.js => identity.ts} (66%) diff --git a/src/transforms/identity.js b/src/transforms/identity.ts similarity index 66% rename from src/transforms/identity.js rename to src/transforms/identity.ts index c32145f8a1..c3e77c6aba 100644 --- a/src/transforms/identity.js +++ b/src/transforms/identity.ts @@ -1,13 +1,14 @@ +import type {MarkOptions} from "../common.js"; import {identity} from "../options.js"; -export function maybeIdentityX(options = {}) { +export function maybeIdentityX(options: MarkOptions = {}) { const {x, x1, x2} = options; return x1 === undefined && x2 === undefined && x === undefined ? {...options, x: identity} : options; } -export function maybeIdentityY(options = {}) { +export function maybeIdentityY(options: MarkOptions = {}) { const {y, y1, y2} = options; return y1 === undefined && y2 === undefined && y === undefined ? {...options, y: identity} From 978a2f04f614f52fe72a5b2edfc74037f9b4e4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 00:29:09 +0200 Subject: [PATCH 13/23] group.ts (almost done!) --- src/common.ts | 59 ++++++++-- src/options.ts | 31 +++--- src/transforms/{group.js => group.ts} | 155 +++++++++++++------------- 3 files changed, 143 insertions(+), 102 deletions(-) rename src/transforms/{group.js => group.ts} (60%) diff --git a/src/common.ts b/src/common.ts index b88a163284..1da6b84f58 100644 --- a/src/common.ts +++ b/src/common.ts @@ -3,7 +3,7 @@ export type nullish = null | undefined; export type DataSource = Iterable | ArrayLike; export type DataSourceOptional = DataSource | nullish; -export type UserOption = unknown; +export type UserOption = unknown; // TODO: remove this type by checking which options are allowed in each case export type booleanOption = boolean | nullish; export type numberOption = number | nullish; export type stringOption = number | any[] | string | nullish; @@ -12,15 +12,54 @@ export type NumberChannel = number[] | Float32Array | Float64Array; export type Channel = TextChannel | NumberChannel | any[]; export type ConstantOrFieldOption = number | string | Channel | Date | ITransform | IAccessor | nullish; export type Comparator = (a: any, b: any) => number; +export type IndexArray = number[] | Uint32Array; /** * Definition for both transform and initializer functions. */ -export type MaybeFacetArray = number[][] | undefined; +export type FacetArray = number[][]; +export type MaybeFacetArray = FacetArray | undefined; export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: number[][], channels?: any}; +/** + * Aggregation options for the group transform + * a string + * * a function - passed the array of values for each group + * * an object with a reduce method, an optionally a scope + */ + export type Reduce1 = { + label?: string; + reduce: (I: IndexArray, X: any, context?: any, extent?: any) => any; + scope?: "data" | "facet" +}; +export type AggregationMethod = Reduce1 | "first" | "last" | "count" | "sum" | "proportion" | "proportion-facet" | "min" | "min-index" | "max" | "max-index" | "mean" | "median" | "mode" | "deviation" | "variance" | ((data?: ArrayLike, extent?: any) => any) | PXX; + +export type Reducer = { + name?: FieldOptionsKey, + output?: (() => void) | LazyColumnOptions, + initialize: (data: any) => void, + scope: (scope?: any, I?: IndexArray) => void, + reduce: (I: IndexArray, data?: any) => any, + label?: string +}; + +export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> & { + data?: any; + reverse?: boolean; +} + -export interface MarkOptionsDefined { +/** + * Plot.column() + */ +export interface LazyColumnOptions { + transform: () => any[]; + label?: string +} +export type LazyColumnSetter = (v: Array) => Array; +export type LazyColumn = [ LazyColumnOptions, LazyColumnSetter? ]; + +export interface FieldOptions { x?: ConstantOrFieldOption; x1?: ConstantOrFieldOption; x2?: ConstantOrFieldOption; @@ -30,16 +69,20 @@ export interface MarkOptionsDefined { z?: ConstantOrFieldOption; fill?: ConstantOrFieldOption; stroke?: ConstantOrFieldOption; - + title?: ConstantOrFieldOption; + href?: ConstantOrFieldOption; filter?: ConstantOrFieldOption; - transform?: TransformFunction | null; sort?: ConstantOrFieldOption; - reverse?: ConstantOrFieldOption; +} + +export interface MarkOptionsDefined extends FieldOptions { + transform?: TransformFunction | null; initializer?: TransformFunction | null; + reverse?: boolean; } -export type MarkOptionsKey = "x" | "x1" | "x2" | "y" | "y1" | "y2" | "z" | "fill" | "stroke"; + +export type FieldOptionsKey = keyof FieldOptions; export type MarkOptions = MarkOptionsDefined | undefined; -export type ObjectDatum = Record; export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; export type IAccessor = (d: any, i: number, data?: ArrayLike) => any; export type booleanish = boolean | undefined; diff --git a/src/options.ts b/src/options.ts index ec8fc9a0e1..ef1bbb49df 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,15 +5,17 @@ import type { DataSourceOptional, UserOption, ConstantOrFieldOption, + LazyColumn, + LazyColumnOptions, MarkOptionsDefined, - MarkOptionsKey, + FieldOptionsKey, MarkOptions, - ObjectDatum, ArrayType, IAccessor, booleanish, ITransform, stringOption, + IndexArray, nullish } from "./common.js"; @@ -35,9 +37,9 @@ export function valueof(data: DataSource, value: ConstantOrFieldOption, arrayTyp : arrayify(value as DataSource, arrayType); // preserve undefined type } -export const field = (name: string) => (d: ObjectDatum) => d[name]; -export const indexOf = (d: ObjectDatum, i: number) => i; -export const identity = {transform: (d: ObjectDatum) => d}; +export const field = (name: string) => (d: any) => d[name]; +export const indexOf = (d: any, i: number) => i; +export const identity = {transform: (d: any) => d}; export const zero = () => 0; export const one = () => 1; export const yes = () => true; @@ -52,7 +54,7 @@ export const constant = (x: any) => () => x; // accessor function f, returning the corresponding percentile value. export function percentile(reduce: PXX) { const p = +`${reduce}`.slice(1) / 100; - return (I: Uint32Array, f: (i: number) => number) => quantile(I, p, f); + return (I: IndexArray, f: (i: number) => number) => quantile(I, p, f); } // Some channels may allow a string constant to be specified; to differentiate @@ -153,7 +155,7 @@ export function maybeZero(x: UserOption, x1: UserOption, x2: UserOption, x3: Use } // For marks that have x and y channels (e.g., cell, dot, line, text). -export function maybeTuple(x: UserOption, y: UserOption) { +export function maybeTuple(x: ConstantOrFieldOption, y: ConstantOrFieldOption): [ConstantOrFieldOption, ConstantOrFieldOption] { return x === undefined && y === undefined ? [first, second] : [x, y]; } @@ -179,7 +181,7 @@ export function where(data: ArrayLike, test: IAccessor) { } // Returns an array [values[index[0]], values[index[1]], …]. -export function take(values: ArrayLike, index: number[]) { +export function take(values: ArrayLike, index: IndexArray) { return map(index, i => values[i]); } @@ -188,7 +190,7 @@ export function keyof(value: any) { return value !== null && typeof value === "object" ? value.valueOf() : value; } -export function maybeInput(key: MarkOptionsKey, options: MarkOptionsDefined): UserOption { +export function maybeInput(key: FieldOptionsKey, options: MarkOptionsDefined) { if (options[key] !== undefined) return options[key]; switch (key) { case "x1": case "x2": key = "x"; break; @@ -200,15 +202,8 @@ export function maybeInput(key: MarkOptionsKey, options: MarkOptionsDefined): Us // 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. -interface LazyColumnOptions { - transform: () => Array; - label?: string -} -type LazyColumnSetter = (v: Array) => Array; -type LazyColumn = [ LazyColumnOptions | null | undefined, LazyColumnSetter? ]; - export function column(source: UserOption): LazyColumn { - let value: Array; + let value: any[]; return [ { transform: () => value, @@ -220,7 +215,7 @@ export function column(source: UserOption): LazyColumn { // Like column, but allows the source to be null. export function maybeColumn(source: UserOption) { - return source == null ? [source] as LazyColumn : column(source); + return source == null ? [source] : column(source); } export function labelof(value: any, defaultValue?: string) { diff --git a/src/transforms/group.js b/src/transforms/group.ts similarity index 60% rename from src/transforms/group.js rename to src/transforms/group.ts index f0758b32e3..eda5f623b7 100644 --- a/src/transforms/group.js +++ b/src/transforms/group.ts @@ -1,30 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, LazyColumnSetter, FacetArray, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, Reducer, OutputOptions} from "../common.js"; + import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3"; import {ascendingDefined} from "../defined.js"; import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeColumn, column, first, identity, take, labelof, range, second, percentile} from "../options.js"; import {basic} from "./basic.js"; // Group on {z, fill, stroke}. -export function groupZ(outputs, options) { +export function groupZ(outputs: OutputOptions, options: MarkOptions) { return groupn(null, null, outputs, options); } // Group on {z, fill, stroke}, then on x. -export function groupX(outputs = {y: "count"}, options = {}) { +export function groupX(outputs: OutputOptions = {y: "count"}, options: MarkOptionsDefined = {}) { const {x = identity} = options; if (x == null) throw new Error("missing channel: x"); return groupn(x, null, outputs, options); } // Group on {z, fill, stroke}, then on y. -export function groupY(outputs = {x: "count"}, options = {}) { +export function groupY(outputs: OutputOptions = {x: "count"}, options: MarkOptionsDefined = {}) { const {y = identity} = options; if (y == null) throw new Error("missing channel: y"); return groupn(null, y, outputs, options); } // Group on {z, fill, stroke}, then on x and y. -export function group(outputs = {fill: "count"}, options = {}) { - let {x, y} = options; +export function group(outputs: OutputOptions = {fill: "count"}, options: MarkOptionsDefined = {}) { + let {x, y}: {x?: ConstantOrFieldOption, y?: ConstantOrFieldOption} = options; ([x, y] = maybeTuple(x, y)); if (x == null) throw new Error("missing channel: x"); if (y == null) throw new Error("missing channel: y"); @@ -32,23 +35,23 @@ export function group(outputs = {fill: "count"}, options = {}) { } function groupn( - x, // optionally group on x - y, // optionally group on y + x: ConstantOrFieldOption, // optionally group on x + y: ConstantOrFieldOption, // optionally group on y { - data: reduceData = reduceIdentity, - filter, - sort, + data: reduceData1 = reduceIdentity, + filter: filter1, + sort: sort1, reverse, - ...outputs // output channel definitions - } = {}, - inputs = {} // input channels and options + ...outputs1 // output channel definitions + }: OutputOptions = {}, + inputs: MarkOptionsDefined = {} // input channels and options ) { // Compute the outputs. - outputs = maybeOutputs(outputs, inputs); - reduceData = maybeReduce(reduceData, identity); - sort = sort == null ? undefined : maybeOutput("sort", sort, inputs); - filter = filter == null ? undefined : maybeEvaluator("filter", filter, inputs); + const outputs = maybeOutputs(outputs1, inputs); + const reduceData = maybeReduce(reduceData1, identity); + const sort = sort1 == null ? undefined : maybeOutput("sort", sort1, inputs); + const filter = filter1 == null ? undefined : maybeEvaluator("filter", filter1, inputs); // Produce x and y output channels as appropriate. const [GX, setGX] = maybeColumn(x); @@ -83,17 +86,17 @@ function groupn( const S = valueof(data, vstroke); const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S}); const groupFacets = []; - const groupData = []; - const GX = X && setGX([]); - const GY = Y && setGY([]); - const GZ = Z && setGZ([]); - const GF = F && setGF([]); - const GS = S && setGS([]); + const groupData: number[] = []; + const GX = X && (setGX as LazyColumnSetter)([]); + const GY = Y && (setGY as LazyColumnSetter)([]); + const GZ = Z && (setGZ as LazyColumnSetter)([]); + const GF = F && (setGF as LazyColumnSetter)([]); + const GS = S && (setGS as LazyColumnSetter)([]); let i = 0; for (const o of outputs) o.initialize(data); if (sort) sort.initialize(data); if (filter) filter.initialize(data); - for (const facet of facets) { + for (const facet of facets as FacetArray) { const groupFacet = []; for (const o of outputs) o.scope("facet", facet); if (sort) sort.scope("facet", facet); @@ -104,11 +107,11 @@ function groupn( if (filter && !filter.reduce(g)) continue; groupFacet.push(i++); groupData.push(reduceData.reduce(g, data)); - if (X) GX.push(x); - if (Y) GY.push(y); - if (Z) GZ.push(G === Z ? f : Z[g[0]]); - if (F) GF.push(G === F ? f : F[g[0]]); - if (S) GS.push(G === S ? f : S[g[0]]); + if (GX) GX.push(x); + if (GY) GY.push(y); + if (GZ) GZ.push(G === Z ? f : Z[g[0]]); + if (GF) GF.push(G === F ? f : F[g[0]]); + if (GS) GS.push(G === S ? f : S[g[0]]); for (const o of outputs) o.reduce(g); if (sort) sort.reduce(g); } @@ -125,65 +128,65 @@ function groupn( }; } -export function hasOutput(outputs, ...names) { +export function hasOutput(outputs: Reducer[], ...names: string[]) { for (const {name} of outputs) { - if (names.includes(name)) { + if (names.includes(name as string)) { return true; } } return false; } -export function maybeOutputs(outputs, inputs) { +export function maybeOutputs(outputs: OutputOptions, inputs: MarkOptionsDefined): Reducer[] { const entries = Object.entries(outputs); // Propagate standard mark channels by default. if (inputs.title != null && outputs.title === undefined) entries.push(["title", reduceTitle]); if (inputs.href != null && outputs.href === undefined) entries.push(["href", reduceFirst]); - return entries.map(([name, reduce]) => { + return entries.map(([name, reduce]: [string, AggregationMethod | undefined]) => { return reduce == null - ? {name, initialize() {}, scope() {}, reduce() {}} - : maybeOutput(name, reduce, inputs); + ? {name, initialize() {}, scope() {}, reduce() {}} as Reducer // type name to a FieldOptionsKey + : maybeOutput(name as FieldOptionsKey, reduce, inputs); }); } -export function maybeOutput(name, reduce, inputs) { +export function maybeOutput(name: FieldOptionsKey, reduce: any, inputs: any) { const evaluator = maybeEvaluator(name, reduce, inputs); const [output, setOutput] = column(evaluator.label); let O; return { name, output, - initialize(data) { + initialize(data: any) { evaluator.initialize(data); - O = setOutput([]); + O = (setOutput as LazyColumnSetter)([]); }, - scope(scope, I) { + scope(scope?: "data" | "facet", I?: IndexArray) { evaluator.scope(scope, I); }, - reduce(I, extent) { + reduce(I: IndexArray, extent?: string) { O.push(evaluator.reduce(I, extent)); } }; } -export function maybeEvaluator(name, reduce, inputs) { - const input = maybeInput(name, inputs); +export function maybeEvaluator(name: string, reduce: any, inputs: any): Reducer { + const input = maybeInput(name as FieldOptionsKey, inputs); const reducer = maybeReduce(reduce, input); - let V, context; + let V: ArrayLike | nullish, context: any; return { label: labelof(reducer === reduceCount ? null : input, reducer.label), - initialize(data) { + initialize(data: ArrayLike) { V = input === undefined ? data : valueof(data, input); if (reducer.scope === "data") { context = reducer.reduce(range(data), V); } }, - scope(scope, I) { + scope(scope?: "data" | "facet", I?: IndexArray) { if (reducer.scope === scope) { - context = reducer.reduce(I, V); + context = reducer.reduce(I as IndexArray, V); } }, - reduce(I, extent) { + reduce(I: IndexArray, extent?: string) { return reducer.scope == null ? reducer.reduce(I, V, extent) : reducer.reduce(I, V, context, extent); @@ -191,14 +194,14 @@ export function maybeEvaluator(name, reduce, inputs) { }; } -export function maybeGroup(I, X) { +export function maybeGroup(I: IndexArray, X: ArrayLike | nullish): [any, IndexArray][] { return X ? sort(grouper(I, i => X[i]), first) : [[, I]]; } -export function maybeReduce(reduce, value) { - if (reduce && typeof reduce.reduce === "function") return reduce; +export function maybeReduce(reduce: AggregationMethod, value: any): Reduce1 { + if (reduce && typeof (reduce as Reduce1).reduce === "function") return reduce as Reduce1; if (typeof reduce === "function") return reduceFunction(reduce); - if (/^p\d{2}$/i.test(reduce)) return reduceAccessor(percentile(reduce)); + if (/^p\d{2}$/i.test(reduce as string)) return reduceAccessor(percentile(reduce as PXX)); switch (`${reduce}`.toLowerCase()) { case "first": return reduceFirst; case "last": return reduceLast; @@ -226,19 +229,19 @@ export function maybeReduce(reduce, value) { throw new Error(`invalid reduce: ${reduce}`); } -export function maybeSubgroup(outputs, inputs) { +export function maybeSubgroup(outputs: Reducer[], inputs: {z?: any, stroke?: any, fill?: any}) { for (const name in inputs) { - const value = inputs[name]; + const value = inputs[name as "z" | "stroke" | "fill"]; if (value !== undefined && !outputs.some(o => o.name === name)) { return value; } } } -export function maybeSort(facets, sort, reverse) { +export function maybeSort(facets: FacetArray, sort: TodoChannel, reverse: booleanOption) { if (sort) { const S = sort.output.transform(); - const compare = (i, j) => ascendingDefined(S[i], S[j]); + const compare = (i: number, j: number) => ascendingDefined(S[i], S[j]); facets.forEach(f => f.sort(compare)); } if (reverse) { @@ -246,36 +249,36 @@ export function maybeSort(facets, sort, reverse) { } } -function reduceFunction(f) { +function reduceFunction(f: (X?: ArrayLike, extent?: any) => any) { return { - reduce(I, X, extent) { + reduce(I: IndexArray, X: any[], extent?: any) { return f(take(X, I), extent); } }; } -function reduceAccessor(f) { +function reduceAccessor(f: (I: IndexArray, a: (i: number) => any) => any) { return { - reduce(I, X) { - return f(I, i => X[i]); + reduce(I: IndexArray, X: ArrayLike) { + return f(I, (i: number) => X[i]); } }; } export const reduceIdentity = { - reduce(I, X) { + reduce(I: IndexArray, X: any) { return take(X, I); } }; export const reduceFirst = { - reduce(I, X) { + reduce(I: IndexArray, X: any) { return X[I[0]]; } }; const reduceTitle = { - reduce(I, X) { + reduce(I: IndexArray, X: any) { const n = 5; const groups = sort(rollup(I, V => V.length, i => X[i]), second); const top = groups.slice(-n).reverse(); @@ -288,21 +291,21 @@ const reduceTitle = { }; const reduceLast = { - reduce(I, X) { + reduce(I: IndexArray, X: any) { return X[I[I.length - 1]]; } }; export const reduceCount = { label: "Frequency", - reduce(I) { + reduce(I: IndexArray) { return I.length; } }; const reduceDistinct = { label: "Distinct", - reduce: (I, X) => { + reduce: (I: IndexArray, X: any) => { const s = new InternSet(); for (const i of I) s.add(X[i]); return s.size; @@ -311,49 +314,49 @@ const reduceDistinct = { const reduceSum = reduceAccessor(sum); -function reduceProportion(value, scope) { +function reduceProportion(value: any, scope: "data" | "facet") { return value == null - ? {scope, label: "Frequency", reduce: (I, V, basis = 1) => I.length / basis} - : {scope, reduce: (I, V, basis = 1) => sum(I, i => V[i]) / basis}; + ? {scope, label: "Frequency", reduce: (I: IndexArray, V: any, basis = 1) => I.length / basis} + : {scope, reduce: (I: IndexArray, V: any, basis = 1) => sum(I, i => V[i]) / basis}; } -function mid(x1, x2) { +function mid(x1: number | Date, x2: number | Date): number | Date { const m = (+x1 + +x2) / 2; return x1 instanceof Date ? new Date(m) : m; } const reduceX = { - reduce(I, X, {x1, x2}) { + reduce(I: IndexArray, X: any, {x1, x2}: any) { return mid(x1, x2); } }; const reduceY = { - reduce(I, X, {y1, y2}) { + reduce(I: IndexArray, X: any, {y1, y2}: any) { return mid(y1, y2); } }; const reduceX1 = { - reduce(I, X, {x1}) { + reduce(I: IndexArray, X: any, {x1}: any) { return x1; } }; const reduceX2 = { - reduce(I, X, {x2}) { + reduce(I: IndexArray, X: any, {x2}: any) { return x2; } }; const reduceY1 = { - reduce(I, X, {y1}) { + reduce(I: IndexArray, X: any, {y1}: any) { return y1; } }; const reduceY2 = { - reduce(I, X, {y2}) { + reduce(I: IndexArray, X: any, {y2}: any) { return y2; } }; From ed9dbb8cd0a236c9b5daf5af8f46895dd0580d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 07:34:10 +0200 Subject: [PATCH 14/23] transforms/group.ts --- src/common.ts | 68 +++++++++++++++++++++++++---------------- src/marks/marker.ts | 4 +-- src/style.ts | 6 ++-- src/transforms/basic.ts | 4 +-- src/transforms/group.ts | 26 +++++++++++----- 5 files changed, 67 insertions(+), 41 deletions(-) diff --git a/src/common.ts b/src/common.ts index 1da6b84f58..c8cabe7e6b 100644 --- a/src/common.ts +++ b/src/common.ts @@ -1,5 +1,45 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* + * API + */ + +/** + * Aggregation options for the group transform: + * * a string describing an aggregation (first, min, sum, count…) + * * a function - passed the array of values for each group + * * an object with a reduce method, an optionally a scope + * @link https://github.com/observablehq/plot/blob/main/README.md#group + */ +export type AggregationMethod = "first" | "last" | "count" | "sum" | "proportion" | "proportion-facet" | "min" | "min-index" | "max" | "max-index" | "mean" | "median" | "mode" | PXX | "deviation" | "variance" | ReduceFunction | Reduce1; +export type Reduce1 = { + label?: string; + reduce: (I: IndexArray, X: any, context?: any, extent?: any) => any; + scope?: "data" | "facet" +}; // TODO: rename to ReduceMethod + ReduceObject? +type ReduceFunction = ((data?: ArrayLike, extent?: any) => any); + +/** + * Facets expressed as an array of arrays of indices + */ +export type MaybeFacetArray = IndexArray[] | undefined; + +/** + * Array of indices into the data + */ + export type IndexArray = number[] | Uint32Array; + + + +/* + * COMMON + */ + + +/* + * UNSORTED + */ + export type nullish = null | undefined; export type DataSource = Iterable | ArrayLike; export type DataSourceOptional = DataSource | nullish; @@ -12,36 +52,12 @@ export type NumberChannel = number[] | Float32Array | Float64Array; export type Channel = TextChannel | NumberChannel | any[]; export type ConstantOrFieldOption = number | string | Channel | Date | ITransform | IAccessor | nullish; export type Comparator = (a: any, b: any) => number; -export type IndexArray = number[] | Uint32Array; /** * Definition for both transform and initializer functions. */ -export type FacetArray = number[][]; -export type MaybeFacetArray = FacetArray | undefined; -export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: number[][], channels?: any}; +export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: IndexArray[], channels?: any}; -/** - * Aggregation options for the group transform - * a string - * * a function - passed the array of values for each group - * * an object with a reduce method, an optionally a scope - */ - export type Reduce1 = { - label?: string; - reduce: (I: IndexArray, X: any, context?: any, extent?: any) => any; - scope?: "data" | "facet" -}; -export type AggregationMethod = Reduce1 | "first" | "last" | "count" | "sum" | "proportion" | "proportion-facet" | "min" | "min-index" | "max" | "max-index" | "mean" | "median" | "mode" | "deviation" | "variance" | ((data?: ArrayLike, extent?: any) => any) | PXX; - -export type Reducer = { - name?: FieldOptionsKey, - output?: (() => void) | LazyColumnOptions, - initialize: (data: any) => void, - scope: (scope?: any, I?: IndexArray) => void, - reduce: (I: IndexArray, data?: any) => any, - label?: string -}; export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> & { data?: any; @@ -55,7 +71,7 @@ export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> export interface LazyColumnOptions { transform: () => any[]; label?: string -} +} // TODO: API, Rename to ColumnOptions export type LazyColumnSetter = (v: Array) => Array; export type LazyColumn = [ LazyColumnOptions, LazyColumnSetter? ]; diff --git a/src/marks/marker.ts b/src/marks/marker.ts index d324787ce2..769c6a3acd 100644 --- a/src/marks/marker.ts +++ b/src/marks/marker.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction, nullish} from "../common.js"; +import {ISelection, IMark, IContext, MarkerOption, MarkerFunction, MaybeMarkerFunction, nullish, IndexArray} from "../common.js"; import {create} from "../context.js"; @@ -90,7 +90,7 @@ export function applyMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke } export function applyGroupedMarkers(path: ISelection, mark: IMark, {stroke: S}: {stroke?: string[]} = {}) { - return applyMarkersColor(path, mark, S && (([i]: number[]) => S[i])); + return applyMarkersColor(path, mark, S && (([i]: IndexArray) => S[i])); } function applyMarkersColor( diff --git a/src/style.ts b/src/style.ts index 393783e19f..41054f506b 100644 --- a/src/style.ts +++ b/src/style.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {IMark, IDimensions, Channel, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; +import type {IMark, IDimensions, IndexArray, Channel, ChannelObject, IScale, ISelection, IStyleObject, booleanOption, numberOption, stringOption, NumberChannel, TextChannel, UserOption, nullish} from "./common.js"; import {group, namespaces} from "d3"; import {defined, nonempty} from "./defined.js"; @@ -234,7 +234,7 @@ function groupAesthetics({ return [AL, T, F, FO, S, SO, SW, O, H].filter(c => c !== undefined) as (TextChannel | NumberChannel)[]; } -export function groupZ(I: number[], Z: Channel, z: UserOption) { +export function groupZ(I: IndexArray, Z: Channel, z: UserOption) { const G = group(I, i => Z[i]); if (z === undefined && G.size > I.length >> 1) { warn(`Warning: the implicit z channel has high cardinality. This may occur when the fill or stroke channel is associated with quantitative data rather than ordinal or categorical data. You can suppress this warning by setting the z option explicitly; if this data represents a single series, set z to null.`); @@ -242,7 +242,7 @@ export function groupZ(I: number[], Z: Channel, z: UserOption) { return G.values(); } -export function* groupIndex(I: number[], position: NumberChannel[], {z}: IMark, channels: ChannelObject) { +export function* groupIndex(I: IndexArray, position: NumberChannel[], {z}: IMark, channels: ChannelObject) { const {z: Z} = channels; // group channel const A = groupAesthetics(channels); // aesthetic channels const C = [...position, ...A]; // all channels diff --git a/src/transforms/basic.ts b/src/transforms/basic.ts index 5980834a5c..60f06ff47c 100644 --- a/src/transforms/basic.ts +++ b/src/transforms/basic.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable prefer-const */ -import type {IDimensions, IMark, Comparator, Channel, ConstantOrFieldOption, MarkOptions, MaybeFacetArray, TransformFunction, nullish} from "../common.js"; +import type {IDimensions, IMark, Comparator, Channel, ConstantOrFieldOption, MarkOptions, MaybeFacetArray, TransformFunction, nullish, IndexArray} from "../common.js"; import {randomLcg} from "d3"; import {ascendingDefined, descendingDefined} from "../defined.js"; @@ -131,6 +131,6 @@ function sortValue(value: any): TransformFunction { if (!V) return {}; // ignore missing channel } const compareValue = (i: number, j: number) => order((V as Channel)[i], (V as Channel)[j]); - return {data, facets: facets && facets.map((I: number[]) => I.slice().sort(compareValue))}; + return {data, facets: facets && facets.map((I: IndexArray) => I.slice().sort(compareValue))}; }; } diff --git a/src/transforms/group.ts b/src/transforms/group.ts index eda5f623b7..ebcde64f59 100644 --- a/src/transforms/group.ts +++ b/src/transforms/group.ts @@ -1,5 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, LazyColumnSetter, FacetArray, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, Reducer, OutputOptions} from "../common.js"; +import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, LazyColumnSetter, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, OutputOptions, LazyColumnOptions} from "../common.js"; + +type ComputedReducer = { + name?: FieldOptionsKey, + output?: LazyColumnOptions, + initialize: (data: any) => void, + scope: (scope?: any, I?: IndexArray) => void, + reduce: (I: IndexArray, data?: any) => any, + label?: string +}; + import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3"; import {ascendingDefined} from "../defined.js"; @@ -96,7 +106,7 @@ function groupn( for (const o of outputs) o.initialize(data); if (sort) sort.initialize(data); if (filter) filter.initialize(data); - for (const facet of facets as FacetArray) { + for (const facet of facets as IndexArray[]) { const groupFacet = []; for (const o of outputs) o.scope("facet", facet); if (sort) sort.scope("facet", facet); @@ -128,7 +138,7 @@ function groupn( }; } -export function hasOutput(outputs: Reducer[], ...names: string[]) { +export function hasOutput(outputs: ComputedReducer[], ...names: string[]) { for (const {name} of outputs) { if (names.includes(name as string)) { return true; @@ -137,14 +147,14 @@ export function hasOutput(outputs: Reducer[], ...names: string[]) { return false; } -export function maybeOutputs(outputs: OutputOptions, inputs: MarkOptionsDefined): Reducer[] { +export function maybeOutputs(outputs: OutputOptions, inputs: MarkOptionsDefined): ComputedReducer[] { const entries = Object.entries(outputs); // Propagate standard mark channels by default. if (inputs.title != null && outputs.title === undefined) entries.push(["title", reduceTitle]); if (inputs.href != null && outputs.href === undefined) entries.push(["href", reduceFirst]); return entries.map(([name, reduce]: [string, AggregationMethod | undefined]) => { return reduce == null - ? {name, initialize() {}, scope() {}, reduce() {}} as Reducer // type name to a FieldOptionsKey + ? {name, initialize() {}, scope() {}, reduce() {}} as ComputedReducer // type name to a FieldOptionsKey : maybeOutput(name as FieldOptionsKey, reduce, inputs); }); } @@ -169,7 +179,7 @@ export function maybeOutput(name: FieldOptionsKey, reduce: any, inputs: any) { }; } -export function maybeEvaluator(name: string, reduce: any, inputs: any): Reducer { +export function maybeEvaluator(name: string, reduce: any, inputs: any): ComputedReducer { const input = maybeInput(name as FieldOptionsKey, inputs); const reducer = maybeReduce(reduce, input); let V: ArrayLike | nullish, context: any; @@ -229,7 +239,7 @@ export function maybeReduce(reduce: AggregationMethod, value: any): Reduce1 { throw new Error(`invalid reduce: ${reduce}`); } -export function maybeSubgroup(outputs: Reducer[], inputs: {z?: any, stroke?: any, fill?: any}) { +export function maybeSubgroup(outputs: ComputedReducer[], inputs: {z?: any, stroke?: any, fill?: any}) { for (const name in inputs) { const value = inputs[name as "z" | "stroke" | "fill"]; if (value !== undefined && !outputs.some(o => o.name === name)) { @@ -238,7 +248,7 @@ export function maybeSubgroup(outputs: Reducer[], inputs: {z?: any, stroke?: any } } -export function maybeSort(facets: FacetArray, sort: TodoChannel, reverse: booleanOption) { +export function maybeSort(facets: IndexArray[], sort: ComputedReducer & {output: LazyColumnOptions} | nullish, reverse: booleanOption) { if (sort) { const S = sort.output.transform(); const compare = (i: number, j: number) => ascendingDefined(S[i], S[j]); From f8bcff0fe7c24bfa2e60e26416261ba688670104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 07:51:20 +0200 Subject: [PATCH 15/23] transforms/inset.ts --- src/common.ts | 6 +++++- src/transforms/{inset.js => inset.ts} | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) rename src/transforms/{inset.js => inset.ts} (62%) diff --git a/src/common.ts b/src/common.ts index c8cabe7e6b..ad31248f56 100644 --- a/src/common.ts +++ b/src/common.ts @@ -30,7 +30,11 @@ export type MaybeFacetArray = IndexArray[] | undefined; export type IndexArray = number[] | Uint32Array; - +/** + * The inset option is a number + */ +export type InsetOption = number | undefined; + /* * COMMON */ diff --git a/src/transforms/inset.js b/src/transforms/inset.ts similarity index 62% rename from src/transforms/inset.js rename to src/transforms/inset.ts index 46cd750d44..685b703fb2 100644 --- a/src/transforms/inset.js +++ b/src/transforms/inset.ts @@ -1,16 +1,19 @@ +import type {InsetOption} from "../common.js"; +type maybeInsetArgs = {inset?: InsetOption, insetLeft?: InsetOption, insetRight?: InsetOption, insetTop?: InsetOption, insetBottom?: InsetOption} + import {offset} from "../style.js"; -export function maybeInsetX({inset, insetLeft, insetRight, ...options} = {}) { +export function maybeInsetX({inset, insetLeft, insetRight, ...options}: maybeInsetArgs = {}) { ([insetLeft, insetRight] = maybeInset(inset, insetLeft, insetRight)); return {inset, insetLeft, insetRight, ...options}; } -export function maybeInsetY({inset, insetTop, insetBottom, ...options} = {}) { +export function maybeInsetY({inset, insetTop, insetBottom, ...options}: maybeInsetArgs = {}) { ([insetTop, insetBottom] = maybeInset(inset, insetTop, insetBottom)); return {inset, insetTop, insetBottom, ...options}; } -function maybeInset(inset, inset1, inset2) { +function maybeInset(inset: InsetOption, inset1: InsetOption, inset2: InsetOption) { return inset === undefined && inset1 === undefined && inset2 === undefined ? (offset ? [1, 0] : [0.5, 0.5]) : [inset1, inset2]; From a1eee286ab93a8aa71c4748dc2001e95aa139715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 08:03:36 +0200 Subject: [PATCH 16/23] clarify Plot.column API --- src/common.ts | 9 +++------ src/options.ts | 8 ++++---- src/transforms/group.ts | 18 +++++++++--------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/common.ts b/src/common.ts index ad31248f56..c9b5b06b8e 100644 --- a/src/common.ts +++ b/src/common.ts @@ -72,12 +72,9 @@ export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> /** * Plot.column() */ -export interface LazyColumnOptions { - transform: () => any[]; - label?: string -} // TODO: API, Rename to ColumnOptions -export type LazyColumnSetter = (v: Array) => Array; -export type LazyColumn = [ LazyColumnOptions, LazyColumnSetter? ]; +export type ColumnGetter = {transform: () => any[]; label?: string} +export type ColumnSetter = (v: Array) => Array; +export type Column = [ ColumnGetter, ColumnSetter ]; export interface FieldOptions { x?: ConstantOrFieldOption; diff --git a/src/options.ts b/src/options.ts index ef1bbb49df..42e5247eb9 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,8 +5,8 @@ import type { DataSourceOptional, UserOption, ConstantOrFieldOption, - LazyColumn, - LazyColumnOptions, + Column, + ColumnGetter, MarkOptionsDefined, FieldOptionsKey, MarkOptions, @@ -202,7 +202,7 @@ export function maybeInput(key: FieldOptionsKey, options: MarkOptionsDefined) { // 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. -export function column(source: UserOption): LazyColumn { +export function column(source: UserOption): Column { let value: any[]; return [ { @@ -228,7 +228,7 @@ export function labelof(value: any, defaultValue?: string) { // 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: LazyColumnOptions, x2: LazyColumnOptions) { +export function mid(x1: ColumnGetter, x2: ColumnGetter) { return { transform() { const X1 = x1.transform(); // there was a type error here!! diff --git a/src/transforms/group.ts b/src/transforms/group.ts index ebcde64f59..50100f2947 100644 --- a/src/transforms/group.ts +++ b/src/transforms/group.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, LazyColumnSetter, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, OutputOptions, LazyColumnOptions} from "../common.js"; +import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, ColumnSetter, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, OutputOptions, ColumnGetter} from "../common.js"; type ComputedReducer = { name?: FieldOptionsKey, - output?: LazyColumnOptions, + output?: ColumnGetter, initialize: (data: any) => void, scope: (scope?: any, I?: IndexArray) => void, reduce: (I: IndexArray, data?: any) => any, @@ -97,11 +97,11 @@ function groupn( const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S}); const groupFacets = []; const groupData: number[] = []; - const GX = X && (setGX as LazyColumnSetter)([]); - const GY = Y && (setGY as LazyColumnSetter)([]); - const GZ = Z && (setGZ as LazyColumnSetter)([]); - const GF = F && (setGF as LazyColumnSetter)([]); - const GS = S && (setGS as LazyColumnSetter)([]); + const GX = X && (setGX as ColumnSetter)([]); + const GY = Y && (setGY as ColumnSetter)([]); + const GZ = Z && (setGZ as ColumnSetter)([]); + const GF = F && (setGF as ColumnSetter)([]); + const GS = S && (setGS as ColumnSetter)([]); let i = 0; for (const o of outputs) o.initialize(data); if (sort) sort.initialize(data); @@ -168,7 +168,7 @@ export function maybeOutput(name: FieldOptionsKey, reduce: any, inputs: any) { output, initialize(data: any) { evaluator.initialize(data); - O = (setOutput as LazyColumnSetter)([]); + O = (setOutput as ColumnSetter)([]); }, scope(scope?: "data" | "facet", I?: IndexArray) { evaluator.scope(scope, I); @@ -248,7 +248,7 @@ export function maybeSubgroup(outputs: ComputedReducer[], inputs: {z?: any, stro } } -export function maybeSort(facets: IndexArray[], sort: ComputedReducer & {output: LazyColumnOptions} | nullish, reverse: booleanOption) { +export function maybeSort(facets: IndexArray[], sort: ComputedReducer & {output: ColumnGetter} | nullish, reverse: booleanOption) { if (sort) { const S = sort.output.transform(); const compare = (i: number, j: number) => ascendingDefined(S[i], S[j]); From 90c97386102e76cac62e47f53622b23f9be9c52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 08:05:34 +0200 Subject: [PATCH 17/23] Plot.column API+JSDocs --- src/common.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/common.ts b/src/common.ts index c9b5b06b8e..129a61036f 100644 --- a/src/common.ts +++ b/src/common.ts @@ -35,6 +35,15 @@ export type MaybeFacetArray = IndexArray[] | undefined; */ export type InsetOption = number | undefined; +/** + * Plot.column() + * @link https://github.com/observablehq/plot/blob/main/README.md#plotcolumnsource + */ + export type Column = [ ColumnGetter, ColumnSetter ]; + export type ColumnGetter = {transform: () => any[]; label?: string} + export type ColumnSetter = (v: Array) => Array; + + /* * COMMON */ @@ -69,13 +78,7 @@ export type OutputOptions = Partial<{[P in FieldOptionsKey]: AggregationMethod}> } -/** - * Plot.column() - */ -export type ColumnGetter = {transform: () => any[]; label?: string} -export type ColumnSetter = (v: Array) => Array; -export type Column = [ ColumnGetter, ColumnSetter ]; - + export interface FieldOptions { x?: ConstantOrFieldOption; x1?: ConstantOrFieldOption; From 35d35fee6eb2ba7d635add4a4d66c487e3ff1a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 12:51:02 +0200 Subject: [PATCH 18/23] cleaner arrayify/valueof --- src/common.ts | 10 ++++------ src/options.ts | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/common.ts b/src/common.ts index 129a61036f..781953be1b 100644 --- a/src/common.ts +++ b/src/common.ts @@ -54,8 +54,7 @@ export type InsetOption = number | undefined; */ export type nullish = null | undefined; -export type DataSource = Iterable | ArrayLike; -export type DataSourceOptional = DataSource | nullish; +export type DataSource = Iterable | ArrayLike; export type UserOption = unknown; // TODO: remove this type by checking which options are allowed in each case export type booleanOption = boolean | nullish; export type numberOption = number | nullish; @@ -63,11 +62,12 @@ export type stringOption = number | any[] | string | nullish; export type TextChannel = string[]; export type NumberChannel = number[] | Float32Array | Float64Array; export type Channel = TextChannel | NumberChannel | any[]; -export type ConstantOrFieldOption = number | string | Channel | Date | ITransform | IAccessor | nullish; +export type ConstantOrFieldOption = string | IAccessor | number | Channel | Date | ITransform | nullish; export type Comparator = (a: any, b: any) => number; /** * Definition for both transform and initializer functions. + * TODO: clarify the difference (when facets are returned or not, in the case of an initializer) */ export type TransformFunction = (this: IMark, data: any, facets: MaybeFacetArray, channels?: any, scales ?: any, dimensions?: IDimensions) => {data?: any, facets?: IndexArray[], channels?: any}; @@ -106,9 +106,7 @@ export type MarkOptions = MarkOptionsDefined | undefined; export type ArrayType = ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor; export type IAccessor = (d: any, i: number, data?: ArrayLike) => any; export type booleanish = boolean | undefined; -export interface ITransform { - transform: (data: DataSource) => DataSource; -} +export type ITransform = {transform: (data: DataSource) => DataSource}; /** * The document context, used to create new DOM elements. diff --git a/src/options.ts b/src/options.ts index 42e5247eb9..4563500122 100644 --- a/src/options.ts +++ b/src/options.ts @@ -2,7 +2,6 @@ import type { PXX, DataSource, - DataSourceOptional, UserOption, ConstantOrFieldOption, Column, @@ -28,13 +27,16 @@ const TypedArray = Object.getPrototypeOf(Uint8Array); const objectToString = Object.prototype.toString; // This allows transforms to behave equivalently to channels. -export function valueof(data: DataSource, value: ConstantOrFieldOption, arrayType?: ArrayType) { - const type = typeof value; - return type === "string" ? map(data, field(value as string), arrayType) - : type === "function" ? map(data, value as IAccessor, arrayType) - : type === "number" || value instanceof Date || type === "boolean" ? map(data, constant(value), arrayType) - : value && typeof (value as ITransform).transform === "function" ? arrayify((value as ITransform).transform(data), arrayType) - : arrayify(value as DataSource, arrayType); // preserve undefined type +/** + * @link https://github.com/observablehq/plot/blob/main/README.md#plotvalueofdata-value-type + */ +export function valueof(data: DataSource | nullish, value: ConstantOrFieldOption, arrayType?: ArrayType) { + return data == null ? data + : typeof value === "string" ? map(data, field(value as string), arrayType) + : typeof value === "function" ? map(data, value as IAccessor, arrayType) + : typeof value === "number" || value instanceof Date || typeof value === "boolean" ? map(data, constant(value), arrayType) + : value && typeof (value as ITransform).transform === "function" ? arrayify((value as ITransform).transform(data), arrayType) + : arrayify(value as ConstantOrFieldOption & Iterable, arrayType); // preserve undefined type } export const field = (name: string) => (d: any) => d[name]; @@ -94,7 +96,7 @@ export function keyword(input: string | null | undefined, name: string, allowed: // 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: DataSourceOptional, type?: ArrayType) { +export function arrayify(data: DataSource | nullish, type?: ArrayType) { return data == null ? data : (type === undefined ? (data instanceof Array || data instanceof TypedArray) ? data as any[] : Array.from(data) : (data instanceof type ? data : (type as ArrayConstructor).from(data))); From ed39702f66381e2791d3bca0f7cd1e5fd8ccde71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 13:18:42 +0200 Subject: [PATCH 19/23] transforms/map.ts --- src/common.ts | 16 ++++++++++--- src/transforms/{map.js => map.ts} | 37 ++++++++++++++++++------------- 2 files changed, 34 insertions(+), 19 deletions(-) rename src/transforms/{map.js => map.ts} (56%) diff --git a/src/common.ts b/src/common.ts index 781953be1b..3b9fb0407a 100644 --- a/src/common.ts +++ b/src/common.ts @@ -39,11 +39,21 @@ export type InsetOption = number | undefined; * Plot.column() * @link https://github.com/observablehq/plot/blob/main/README.md#plotcolumnsource */ - export type Column = [ ColumnGetter, ColumnSetter ]; - export type ColumnGetter = {transform: () => any[]; label?: string} - export type ColumnSetter = (v: Array) => Array; +export type Column = [ ColumnGetter, ColumnSetter ]; +export type ColumnGetter = {transform: () => any[]; label?: string} +export type ColumnSetter = (v: Array) => Array; +/** + * Map methods for Plot.map, Plot.mapX, Plot.mapY + * * cumsum - a cumulative sum + * * rank - the rank of each value in the sorted array + * * quantile - the rank, normalized between 0 and 1 + * * a function to be passed an array of values, returning new values + * * an object that implements the map method + */ +export type MapMethod = "cumsum" | "rank" | "quantile" | ((S: Channel) => Channel) | {map: (I: IndexArray, S: Channel, T: any[]) => any}; + /* * COMMON */ diff --git a/src/transforms/map.js b/src/transforms/map.ts similarity index 56% rename from src/transforms/map.js rename to src/transforms/map.ts index 2813d5cd04..58c963a5dd 100644 --- a/src/transforms/map.js +++ b/src/transforms/map.ts @@ -1,23 +1,28 @@ -import {count, group, rank} from "d3"; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type {MapMethod, IndexArray, Channel, FieldOptions, FieldOptionsKey, nullish} from "../common.js"; +type ComputedMapMethod = {map: (I: IndexArray, S: Channel, T: any[]) => any}; + + +import {count, group, Numeric, rank} from "d3"; import {maybeZ, take, valueof, maybeInput, column} from "../options.js"; import {basic} from "./basic.js"; -export function mapX(m, options = {}) { +export function mapX(m: MapMethod, options: FieldOptions = {}) { return map(Object.fromEntries(["x", "x1", "x2"] - .filter(key => options[key] != null) + .filter(key => options[key as "x" | "x1" | "x2"] != null) .map(key => [key, m])), options); } -export function mapY(m, options = {}) { +export function mapY(m: MapMethod, options: FieldOptions = {}) { return map(Object.fromEntries(["y", "y1", "y2"] - .filter(key => options[key] != null) + .filter(key => options[key as "y" | "y1" | "y2"] != null) .map(key => [key, m])), options); } -export function map(outputs = {}, options = {}) { +export function map(outputs: Record = {}, options = {}) { const z = maybeZ(options); const channels = Object.entries(outputs).map(([key, map]) => { - const input = maybeInput(key, options); + const input = maybeInput(key as FieldOptionsKey, options); if (input == null) throw new Error(`missing channel: ${key}`); const [output, setOutput] = column(input); return {key, input, output, setOutput, map: maybeMap(map)}; @@ -27,9 +32,9 @@ export function map(outputs = {}, options = {}) { const Z = valueof(data, z); const X = channels.map(({input}) => valueof(data, input)); const MX = channels.map(({setOutput}) => setOutput(new Array(data.length))); - for (const facet of facets) { + for (const facet of facets as IndexArray[]) { for (const I of Z ? group(facet, i => Z[i]).values() : [facet]) { - channels.forEach(({map}, i) => map.map(I, X[i], MX[i])); + channels.forEach(({map}, i) => map.map(I, X[i] as any[], MX[i])); } } return {data, facets}; @@ -38,9 +43,9 @@ export function map(outputs = {}, options = {}) { }; } -function maybeMap(map) { - if (map && typeof map.map === "function") return map; - if (typeof map === "function") return mapFunction(map); +function maybeMap(map: MapMethod): ComputedMapMethod { + if (map && typeof (map as ComputedMapMethod).map === "function") return map as ComputedMapMethod; + if (typeof map === "function") return mapFunction(map as () => any); switch (`${map}`.toLowerCase()) { case "cumsum": return mapCumsum; case "rank": return mapFunction(rank); @@ -49,14 +54,14 @@ function maybeMap(map) { throw new Error(`invalid map: ${map}`); } -function rankQuantile(V) { +function rankQuantile(V: Iterable) { const n = count(V) - 1; return rank(V).map(r => r / n); } -function mapFunction(f) { +function mapFunction(f: (S: Iterable) => any) { return { - map(I, S, T) { + map(I: IndexArray, S: Channel, T: any[]) { const M = f(take(S, I)); if (M.length !== I.length) throw new Error("map function returned a mismatched length"); for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i]; @@ -65,7 +70,7 @@ function mapFunction(f) { } const mapCumsum = { - map(I, S, T) { + map(I: IndexArray, S: Channel, T: any[]) { let sum = 0; for (const i of I) T[i] = sum += S[i]; } From 694b84cc80531a7d04a9d724eb3ec0118cc5c463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 14:16:45 +0200 Subject: [PATCH 20/23] column: stay close to the API names --- src/common.ts | 6 +++--- src/options.ts | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/common.ts b/src/common.ts index 3b9fb0407a..3b667ff734 100644 --- a/src/common.ts +++ b/src/common.ts @@ -39,9 +39,9 @@ export type InsetOption = number | undefined; * Plot.column() * @link https://github.com/observablehq/plot/blob/main/README.md#plotcolumnsource */ -export type Column = [ ColumnGetter, ColumnSetter ]; -export type ColumnGetter = {transform: () => any[]; label?: string} -export type ColumnSetter = (v: Array) => Array; +export type Column = [column, setColumn]; +export type column = {transform: () => any[]; label?: string} +export type setColumn = (v: Array) => Array; /** diff --git a/src/options.ts b/src/options.ts index 4563500122..3385479989 100644 --- a/src/options.ts +++ b/src/options.ts @@ -5,7 +5,6 @@ import type { UserOption, ConstantOrFieldOption, Column, - ColumnGetter, MarkOptionsDefined, FieldOptionsKey, MarkOptions, @@ -230,7 +229,7 @@ export function labelof(value: any, defaultValue?: string) { // 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: ColumnGetter, x2: ColumnGetter) { +export function mid(x1: Column[0], x2: Column[0]) { return { transform() { const X1 = x1.transform(); // there was a type error here!! From 47f4891b03a9edededf9f82315db49cee9a39be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 16:50:40 +0200 Subject: [PATCH 21/23] transforms/select.ts --- src/transforms/group.ts | 19 +++++---- src/transforms/{select.js => select.ts} | 52 +++++++++++++++---------- 2 files changed, 40 insertions(+), 31 deletions(-) rename src/transforms/{select.js => select.ts} (53%) diff --git a/src/transforms/group.ts b/src/transforms/group.ts index 50100f2947..c63beb0270 100644 --- a/src/transforms/group.ts +++ b/src/transforms/group.ts @@ -1,16 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, ColumnSetter, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, OutputOptions, ColumnGetter} from "../common.js"; +import type {MarkOptions, MarkOptionsDefined, ConstantOrFieldOption, PXX, FieldOptionsKey, nullish, IndexArray, booleanOption, Reduce1, AggregationMethod, OutputOptions, Column} from "../common.js"; type ComputedReducer = { name?: FieldOptionsKey, - output?: ColumnGetter, + output?: Column[0], initialize: (data: any) => void, scope: (scope?: any, I?: IndexArray) => void, reduce: (I: IndexArray, data?: any) => any, label?: string }; - import {group as grouper, sort, sum, deviation, min, max, mean, median, mode, variance, InternSet, minIndex, maxIndex, rollup} from "d3"; import {ascendingDefined} from "../defined.js"; import {valueof, maybeColorChannel, maybeInput, maybeTuple, maybeColumn, column, first, identity, take, labelof, range, second, percentile} from "../options.js"; @@ -97,11 +96,11 @@ function groupn( const G = maybeSubgroup(outputs, {z: Z, fill: F, stroke: S}); const groupFacets = []; const groupData: number[] = []; - const GX = X && (setGX as ColumnSetter)([]); - const GY = Y && (setGY as ColumnSetter)([]); - const GZ = Z && (setGZ as ColumnSetter)([]); - const GF = F && (setGF as ColumnSetter)([]); - const GS = S && (setGS as ColumnSetter)([]); + const GX = X && (setGX as Column[1])([]); + const GY = Y && (setGY as Column[1])([]); + const GZ = Z && (setGZ as Column[1])([]); + const GF = F && (setGF as Column[1])([]); + const GS = S && (setGS as Column[1])([]); let i = 0; for (const o of outputs) o.initialize(data); if (sort) sort.initialize(data); @@ -168,7 +167,7 @@ export function maybeOutput(name: FieldOptionsKey, reduce: any, inputs: any) { output, initialize(data: any) { evaluator.initialize(data); - O = (setOutput as ColumnSetter)([]); + O = (setOutput as Column[1])([]); }, scope(scope?: "data" | "facet", I?: IndexArray) { evaluator.scope(scope, I); @@ -248,7 +247,7 @@ export function maybeSubgroup(outputs: ComputedReducer[], inputs: {z?: any, stro } } -export function maybeSort(facets: IndexArray[], sort: ComputedReducer & {output: ColumnGetter} | nullish, reverse: booleanOption) { +export function maybeSort(facets: IndexArray[], sort: ComputedReducer & {output: Column[0]} | nullish, reverse: booleanOption) { if (sort) { const S = sort.output.transform(); const compare = (i: number, j: number) => ascendingDefined(S[i], S[j]); diff --git a/src/transforms/select.js b/src/transforms/select.ts similarity index 53% rename from src/transforms/select.js rename to src/transforms/select.ts index 8dbc090c3a..b907105b51 100644 --- a/src/transforms/select.js +++ b/src/transforms/select.ts @@ -1,8 +1,14 @@ +import type {IndexArray, Channel, MarkOptions, MarkOptionsDefined, FieldOptionsKey, FieldOptions, ConstantOrFieldOption, nullish} from "../common.js"; + import {greatest, group, least} from "d3"; import {maybeZ, valueof} from "../options.js"; import {basic} from "./basic.js"; -export function select(selector, options = {}) { +export type Selector = string | "first" | "last" | ComputedSelector; +export type MultiSelector = Record; +type ComputedSelector = (((I: IndexArray, X: Channel) => IterableIterator | Generator) | ((I: IndexArray, X?: Channel) => IterableIterator | Generator)); + +export function select(selector: Selector | MultiSelector, options: MarkOptions = {}): MarkOptions { // If specified selector is a string or function, it’s a selector without an // input channel such as first or last. if (typeof selector === "string") { @@ -10,6 +16,7 @@ export function select(selector, options = {}) { case "first": return selectFirst(options); case "last": return selectLast(options); } + throw new Error(`invalid selector: ${selector}`); } if (typeof selector === "function") { return selectChannel(null, selector, options); @@ -20,13 +27,13 @@ export function select(selector, options = {}) { let key, value; for (key in selector) { if (value !== undefined) throw new Error("ambiguous selector; multiple inputs"); - value = maybeSelector(selector[key]); + value = maybeSelector(selector[key as string]); } if (value === undefined) throw new Error(`invalid selector: ${selector}`); - return selectChannel(key, value, options); + return selectChannel(key as FieldOptionsKey, value, options); } -function maybeSelector(selector) { +function maybeSelector(selector: Selector): ComputedSelector { if (typeof selector === "function") return selector; switch (`${selector}`.toLowerCase()) { case "min": return selectorMin; @@ -35,61 +42,64 @@ function maybeSelector(selector) { throw new Error(`unknown selector: ${selector}`); } -export function selectFirst(options) { +export function selectFirst(options: MarkOptionsDefined) { return selectChannel(null, selectorFirst, options); } -export function selectLast(options) { +export function selectLast(options: MarkOptionsDefined) { return selectChannel(null, selectorLast, options); } -export function selectMinX(options) { +export function selectMinX(options: MarkOptionsDefined) { return selectChannel("x", selectorMin, options); } -export function selectMinY(options) { +export function selectMinY(options: MarkOptionsDefined) { return selectChannel("y", selectorMin, options); } -export function selectMaxX(options) { +export function selectMaxX(options: MarkOptionsDefined) { return selectChannel("x", selectorMax, options); } -export function selectMaxY(options) { +export function selectMaxY(options: MarkOptionsDefined) { return selectChannel("y", selectorMax, options); } -function* selectorFirst(I) { +function* selectorFirst(I: IndexArray) { yield I[0]; } -function* selectorLast(I) { +function* selectorLast(I: IndexArray) { yield I[I.length - 1]; } -function* selectorMin(I, X) { +function* selectorMin(I: IndexArray, X: Channel) { yield least(I, i => X[i]); } -function* selectorMax(I, X) { +function* selectorMax(I: IndexArray, X: Channel) { yield greatest(I, i => X[i]); } -function selectChannel(v, selector, options) { - if (v != null) { - if (options[v] == null) throw new Error(`missing channel: ${v}`); - v = options[v]; +function selectChannel(v1: FieldOptionsKey | nullish, selector: ComputedSelector, options: FieldOptions) { + let v: ConstantOrFieldOption; + if (v1 != null) { + if (options[v1] == null) throw new Error(`missing channel: ${v}`); + v = options[v1]; + } else { + v = v1; } const z = maybeZ(options); return basic(options, (data, facets) => { const Z = valueof(data, z); const V = valueof(data, v); const selectFacets = []; - for (const facet of facets) { + for (const facet of facets as IndexArray[]) { const selectFacet = []; for (const I of Z ? group(facet, i => Z[i]).values() : [facet]) { - for (const i of selector(I, V)) { - selectFacet.push(i); + for (const i of selector(I, V as Channel)) { + if (i !== undefined) selectFacet.push(i); } } selectFacets.push(selectFacet); From 811298ba39fcc193416526db2724ae8c35691255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 17:14:19 +0200 Subject: [PATCH 22/23] spurious type import --- src/transforms/map.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transforms/map.ts b/src/transforms/map.ts index 58c963a5dd..c7fd8501d5 100644 --- a/src/transforms/map.ts +++ b/src/transforms/map.ts @@ -3,7 +3,7 @@ import type {MapMethod, IndexArray, Channel, FieldOptions, FieldOptionsKey, null type ComputedMapMethod = {map: (I: IndexArray, S: Channel, T: any[]) => any}; -import {count, group, Numeric, rank} from "d3"; +import {count, group, rank} from "d3"; import {maybeZ, take, valueof, maybeInput, column} from "../options.js"; import {basic} from "./basic.js"; @@ -54,7 +54,7 @@ function maybeMap(map: MapMethod): ComputedMapMethod { throw new Error(`invalid map: ${map}`); } -function rankQuantile(V: Iterable) { +function rankQuantile(V: Iterable) { const n = count(V) - 1; return rank(V).map(r => r / n); } From 19834d6931fdfbb76261febe39357843b4304774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 21 Jul 2022 17:44:51 +0200 Subject: [PATCH 23/23] Named things --- src/options.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/options.ts b/src/options.ts index 3385479989..1e965694de 100644 --- a/src/options.ts +++ b/src/options.ts @@ -388,7 +388,7 @@ export function inherit(options: Record = {}, ...rest : Array): Record { console.warn("named iterables are deprecated; please use an object instead"); const names = new Set(); return Object.fromEntries(Array.from(things, thing => { @@ -402,6 +402,6 @@ export function Named(things) { })); } -export function maybeNamed(things) { +export function maybeNamed(things: any) { return isIterable(things) ? Named(things) : things; }