diff --git a/README.md b/README.md index 16c0e4b641..40824eadff 100644 --- a/README.md +++ b/README.md @@ -2346,7 +2346,6 @@ Plot.rect(athletes, Plot.bin({fillOpacity: "count"}, {x: "weight", y: "height"}) Bins on *x* and *y*. Also groups on the first channel of *z*, *fill*, or *stroke*, if any. - #### Plot.binX(*outputs*, *options*) @@ -2358,7 +2357,6 @@ Plot.rectY(athletes, Plot.binX({y: "count"}, {x: "weight"})) Bins on *x*. Also groups on *y* and the first channel of *z*, *fill*, or *stroke*, if any. - #### Plot.binY(*outputs*, *options*) @@ -2456,7 +2454,6 @@ Plot.group({fill: "count"}, {x: "island", y: "species"}) Groups on *x*, *y*, and the first channel of *z*, *fill*, or *stroke*, if any. - #### Plot.groupX(*outputs*, *options*) @@ -2467,7 +2464,6 @@ Plot.groupX({y: "sum"}, {x: "species", y: "body_mass_g"}) Groups on *x* and the first channel of *z*, *fill*, or *stroke*, if any. - #### Plot.groupY(*outputs*, *options*) @@ -2478,7 +2474,6 @@ Plot.groupY({x: "sum"}, {y: "species", x: "body_mass_g"}) Groups on *y* and the first channel of *z*, *fill*, or *stroke*, if any. - #### Plot.groupZ(*outputs*, *options*) @@ -2491,7 +2486,6 @@ Groups on the first channel of *z*, *fill*, or *stroke*, if any. If none of *z*, *fill*, or *stroke* are channels, then all data (within each facet) is placed into a single group. - ### Map @@ -2569,7 +2563,6 @@ each channel declared in the specified *outputs* object, applies the corresponding map method. Each channel in *outputs* must have a corresponding input channel in *options*. - #### Plot.mapX(*map*, *options*) @@ -2581,7 +2574,6 @@ Plot.mapX("cumsum", {x: d3.randomNormal()}) Equivalent to Plot.map({x: *map*, x1: *map*, x2: *map*}, *options*), but ignores any of **x**, **x1**, and **x2** not present in *options*. - #### Plot.mapY(*map*, *options*) @@ -2593,7 +2585,6 @@ Plot.mapY("cumsum", {y: d3.randomNormal()}) Equivalent to Plot.map({y: *map*, y1: *map*, y2: *map*}, *options*), but ignores any of **y**, **y1**, and **y2** not present in *options*. - #### Plot.normalize(*basis*) @@ -2803,7 +2794,6 @@ resulting in a count of the data points. The stack options (*offset*, *order*, and *reverse*) may be specified as part of the *options* object, if the only argument, or as a separate *stack* options argument. - #### Plot.stackY1(*stack*, *options*) @@ -2817,7 +2807,6 @@ Equivalent to except that the **y1** channel is returned as the **y** channel. This can be used, for example, to draw a line at the bottom of each stacked area. - #### Plot.stackX(*stack*, *options*) @@ -2843,7 +2831,6 @@ Plot.stackX({y: "year", x: "revenue", z: "format", fill: "group"}) See Plot.stackY, but with *x* as the input value channel, *y* as the stack index, *x1*, *x2* and *x* as the output channels. - #### Plot.stackX1(*stack*, *options*) @@ -2857,7 +2844,6 @@ Equivalent to except that the **x1** channel is returned as the **x** channel. This can be used, for example, to draw a line at the left edge of each stacked area. - #### Plot.stackX2(*stack*, *options*) @@ -2871,7 +2857,6 @@ Equivalent to except that the **x2** channel is returned as the **x** channel. This can be used, for example, to draw a line at the right edge of each stacked area. - ### Tree @@ -3140,7 +3125,6 @@ Given marks arranged along the *x* axis, the dodgeY transform piles them vertically by defining a *y* position channel that avoids overlapping. The *x* position channel is unchanged. - #### Plot.dodgeX(*dodgeOptions*, *options*) @@ -3153,7 +3137,6 @@ Equivalent to Plot.dodgeY, but piling horizontally, creating a new *x* position channel that avoids overlapping. The *y* position channel is unchanged. - ### Hexbin diff --git a/scripts/jsdoc-to-readme.ts b/scripts/jsdoc-to-readme.ts index 52953bf1a7..2b1cd789ce 100644 --- a/scripts/jsdoc-to-readme.ts +++ b/scripts/jsdoc-to-readme.ts @@ -1,5 +1,5 @@ import {readFileSync, writeFileSync} from "fs"; -import type {ExportedDeclarations, FunctionDeclaration} from "ts-morph"; +import type {ExportedDeclarations, FunctionDeclaration, JSDoc} from "ts-morph"; import {Project} from "ts-morph"; /** @@ -53,7 +53,7 @@ function injectJsDoc(readme: string) { if (!declaration) throw new Error(`${name} is not exported by src/index`); parts.push(getJsDocs(name, declaration, prefix)); parts.push(""); - replacement = parts.join("\n"); + replacement = pad(parts.join("\n")); } if (!insideReplacement || isReplacementDelimiter) output.push(line); if (replacement) output.push(replacement); @@ -66,14 +66,24 @@ function getJsDocs(name: string, declaration: ExportedDeclarations, prefix = "## return getJsDocsForFunction(name, declaration, prefix); } if ("getJsDocs" in declaration) { - return `${prefix} Plot.${name}\n${declaration - .getJsDocs() - .map((doc) => makeRelativeUrls(doc.getDescription())) - .join("\n\n")}`; + return `${prefix} Plot.${name}\n${transformDocs(declaration.getJsDocs())}`; } return `JSDoc extraction for ${declaration.getKindName()} not yet implemented.`; } +function transformDocs(docs: JSDoc[]): string { + return makeRelativeUrls(pad(docs.map((doc) => doc.getDescription()).join("\n\n"))); +} + +function makeRelativeUrls(description: string) { + return description.replace(new RegExp("https://github.com/observablehq/plot/blob/main/README.md#", "g"), "#"); +} + +// Standardize on one leading and trailing new line for each replacement. +function pad(s: string) { + return `\n${s.trim()}\n`; +} + function getJsDocsForFunction(name: string, declaration: FunctionDeclaration, prefix = "####") { const parameters = declaration.getParameters(); const title = `${prefix} Plot.${name}(${parameters @@ -82,7 +92,7 @@ function getJsDocsForFunction(name: string, declaration: FunctionDeclaration, pr const parts = [title]; const docs = declaration.getJsDocs(); if (docs.length) { - parts.push(docs.map((doc) => makeRelativeUrls(doc.getDescription())).join("\n\n")); + parts.push(transformDocs(docs)); return parts.join("\n"); } // If we didn't find docs on the implementation, it's probably on one of the @@ -91,17 +101,13 @@ function getJsDocsForFunction(name: string, declaration: FunctionDeclaration, pr for (const overload of overloads) { const docs = overload.getJsDocs(); if (!docs.length) continue; - parts.push(docs.map((doc) => makeRelativeUrls(doc.getDescription())).join("\n\n")); + parts.push(transformDocs(docs)); return parts.join("\n"); } return "No JSDocs found."; } -function makeRelativeUrls(description: string) { - return description.replace(new RegExp("https://github.com/observablehq/plot/blob/main/README.md#", "g"), "#"); -} - const check = process.argv[process.argv.length - 1] === "--check"; const original = readFileSync(readmePath, {encoding: "utf-8"}); const output = injectJsDoc(original); diff --git a/src/marks/area.js b/src/marks/area.js index 1234b16e2c..463144d6b6 100644 --- a/src/marks/area.js +++ b/src/marks/area.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").StackOptions} StackOptions + * @typedef {MarkOptions & StackOptions | undefined} Options + */ + import {area as shapeArea} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; @@ -85,6 +92,8 @@ export class Area extends Mark { * [Plot.areaX](https://github.com/observablehq/plot/blob/main/README.md#plotareaxdata-options) * is used in the vertical orientation where the baseline and topline share *y* * values. + * @param {Data} data + * @param {Options=} options */ export function area(data, options) { if (options === undefined) return areaY(data, {x: first, y: second}); @@ -121,6 +130,8 @@ export function area(data, options) { * The **interval** option is recommended to “regularize” sampled data; for * example, if your data represents timestamped temperature measurements and you * expect one sample per day, use d3.utcDay as the interval. + * @param {Data} data + * @param {Options=} options */ export function areaX(data, options) { const {y = indexOf, ...rest} = maybeDenseIntervalY(options); @@ -157,6 +168,8 @@ export function areaX(data, options) { * The **interval** option is recommended to “regularize” sampled data; for * example, if your data represents timestamped temperature measurements and you * expect one sample per day, use d3.utcDay as the interval. + * @param {Data} data + * @param {Options=} options */ export function areaY(data, options) { const {x = indexOf, ...rest} = maybeDenseIntervalX(options); diff --git a/src/marks/arrow.js b/src/marks/arrow.js index 79fbefa392..555130bd15 100644 --- a/src/marks/arrow.js +++ b/src/marks/arrow.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").ArrowOptions} ArrowOptions + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {ArrowOptions & MarkOptions | undefined} Options + */ + import {create} from "../context.js"; import {radians} from "../math.js"; import {constant} from "../options.js"; @@ -182,6 +189,8 @@ function circleCircleIntersect([ax, ay, ar], [bx, by, br], sign) { * ``` * * Returns a new arrow with the given *data* and *options*. + * @param {Data} data + * @param {Options} options */ export function arrow(data, options = {}) { let {x, x1, x2, y, y1, y2, ...remainingOptions} = options; diff --git a/src/marks/bar.js b/src/marks/bar.js index 0a3874f981..f7463e346f 100644 --- a/src/marks/bar.js +++ b/src/marks/bar.js @@ -1,3 +1,11 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").RectOptions} RectOptions + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").StackOptions} StackOptions + * @typedef {MarkOptions & StackOptions & RectOptions | undefined} Options + */ + import {create} from "../context.js"; import {identity, indexOf, number} from "../options.js"; import {Mark} from "../plot.js"; @@ -163,6 +171,8 @@ export class BarY extends AbstractBar { * * If the **y** channel is not specified, the bar will span the full vertical * extent of the plot (or facet). + * @param {Data} data + * @param {Options} options */ export function barX(data, options = {y: indexOf, x2: identity}) { return new BarX(data, maybeStackX(maybeIntervalX(maybeIdentityX(options)))); @@ -203,6 +213,8 @@ export function barX(data, options = {y: indexOf, x2: identity}) { * * If the **x** channel is not specified, the bar will span the full horizontal * extent of the plot (or facet). + * @param {Data} data + * @param {Options} options */ export function barY(data, options = {x: indexOf, y2: identity}) { return new BarY(data, maybeStackY(maybeIntervalY(maybeIdentityY(options)))); diff --git a/src/marks/box.js b/src/marks/box.js index 6f18204d57..a8f571264a 100644 --- a/src/marks/box.js +++ b/src/marks/box.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {min, max, quantile} from "d3"; import {marks} from "../plot.js"; import {groupX, groupY, groupZ} from "../transforms/group.js"; @@ -16,6 +21,8 @@ import {tickX, tickY} from "./tick.js"; * defaults to the identity function, as when *data* is an array of numbers. If * the **y** option is not specified, it defaults to null; if the **y** option * is specified, it should represent an ordinal (discrete) value. + * @param {Data} data + * @param {Options} options */ export function boxX(data, options = {}) { // Returns a composite mark for producing a horizontal box plot, applying the @@ -49,6 +56,8 @@ export function boxX(data, options = {}) { * defaults to the identity function, as when *data* is an array of numbers. If * the **x** option is not specified, it defaults to null; if the **x** option * is specified, it should represent an ordinal (discrete) value. + * @param {Data} data + * @param {Options} options */ export function boxY(data, options = {}) { // Returns a composite mark for producing a vertical box plot, applying the diff --git a/src/marks/cell.js b/src/marks/cell.js index 6df7557432..b9fee48d8f 100644 --- a/src/marks/cell.js +++ b/src/marks/cell.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").RectOptions} RectOptions + * @typedef {RectOptions & MarkOptions | undefined} Options + */ + import {identity, indexOf, maybeColorChannel, maybeTuple} from "../options.js"; import {applyTransform} from "../style.js"; import {AbstractBar} from "./bar.js"; @@ -33,6 +40,8 @@ export class Cell extends AbstractBar { * nor **y** options are specified, *data* is assumed to be an array of pairs * [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, * *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function cell(data, options = {}) { let {x, y, ...remainingOptions} = options; @@ -51,6 +60,8 @@ export function cell(data, options = {}) { * …], and if the **fill** option is not specified and **stroke** is not a * channel, the fill defaults to the identity function and assumes that *data* = * [*x₀*, *x₁*, *x₂*, …]. + * @param {Data} data + * @param {Options} options */ export function cellX(data, options = {}) { let {x = indexOf, fill, stroke, ...remainingOptions} = options; @@ -69,6 +80,8 @@ export function cellX(data, options = {}) { * …], and if the **fill** option is not specified and **stroke** is not a * channel, the fill defaults to the identity function and assumes that *data* = * [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function cellY(data, options = {}) { let {y = indexOf, fill, stroke, ...remainingOptions} = options; diff --git a/src/marks/delaunay.js b/src/marks/delaunay.js index e80603db4f..3707853a45 100644 --- a/src/marks/delaunay.js +++ b/src/marks/delaunay.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").MarkerOptions} MarkerOptions + * @typedef {MarkOptions & MarkerOptions | undefined} Options + */ + import {group, path, select, Delaunay} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; @@ -289,6 +296,9 @@ function delaunayMark(DelaunayMark, data, {x, y, ...options} = {}) { * * If a **z** channel is specified, the input points are grouped by *z*, and * separate Delaunay triangulations are constructed for each group. + * @param {Data} data + * @param {Options=} options + * @returns {DelaunayLink} */ export function delaunayLink(data, options) { return delaunayMark(DelaunayLink, data, options); @@ -304,6 +314,9 @@ export function delaunayLink(data, options) { * * If a **z** channel is specified, the input points are grouped by *z*, and * separate Delaunay triangulations are constructed for each group. + * @param {Data} data + * @param {Options=} options + * @returns {DelaunayMesh} */ export function delaunayMesh(data, options) { return delaunayMark(DelaunayMesh, data, options); @@ -320,6 +333,9 @@ export function delaunayMesh(data, options) { * separate convex hulls are constructed for each group. If the **z** channel is * not specified, it defaults to either the **fill** channel, if any, or the * **stroke** channel, if any. + * @param {Data} data + * @param {Options=} options + * @returns {Hull} */ export function hull(data, options) { return delaunayMark(Hull, data, options); @@ -331,6 +347,9 @@ export function hull(data, options) { * * If a **z** channel is specified, the input points are grouped by *z*, and * separate Voronoi tesselations are constructed for each group. + * @param {Data} data + * @param {Options=} options + * @returns {Voronoi} */ export function voronoi(data, options) { return delaunayMark(Voronoi, data, options); @@ -346,6 +365,9 @@ export function voronoi(data, options) { * * If a **z** channel is specified, the input points are grouped by *z*, and * separate Voronoi tesselations are constructed for each group. + * @param {Data} data + * @param {Options=} options + * @returns {VoronoiMesh} */ export function voronoiMesh(data, options) { return delaunayMark(VoronoiMesh, data, options); diff --git a/src/marks/density.js b/src/marks/density.js index a8d9cf2d93..8d3d17e78a 100644 --- a/src/marks/density.js +++ b/src/marks/density.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {contourDensity, create, geoPath} from "d3"; import {identity, isTypedArray, maybeTuple, maybeZ, valueof} from "../options.js"; import {Mark} from "../plot.js"; @@ -84,6 +89,8 @@ export class Density extends Mark { * series. If the **stroke** or **fill** is specified as *density*, a color * channel is constructed with values representing the density threshold value * of each contour. + * @param {Data} data + * @param {Options} options */ export function density(data, options = {}) { let {x, y, ...remainingOptions} = options; diff --git a/src/marks/dot.js b/src/marks/dot.js index 6b3e380cc5..18dff82164 100644 --- a/src/marks/dot.js +++ b/src/marks/dot.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").DotOptions} DotOptions + * @typedef {MarkOptions & DotOptions | undefined} Options + */ + import {path, symbolCircle} from "d3"; import {create} from "../context.js"; import {positive} from "../defined.js"; @@ -122,6 +129,8 @@ export class Dot extends Mark { * nor **y** nor **frameAnchor** options are specified, *data* is assumed to be * an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that * **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function dot(data, options = {}) { let {x, y, ...remainingOptions} = options; @@ -143,6 +152,8 @@ export function dot(data, options = {}) { * (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If * the interval is specified as a number *n*, *y* will be the midpoint of two * consecutive multiples of *n* that bracket *y*. + * @param {Data} data + * @param {Options} options */ export function dotX(data, options = {}) { const {x = identity, ...remainingOptions} = options; @@ -163,6 +174,8 @@ export function dotX(data, options = {}) { * (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If * the interval is specified as a number *n*, *x* will be the midpoint of two * consecutive multiples of *n* that bracket *x*. + * @param {Data} data + * @param {Options} options */ export function dotY(data, options = {}) { const {y = identity, ...remainingOptions} = options; @@ -173,6 +186,8 @@ export function dotY(data, options = {}) { * Equivalent to * [Plot.dot](https://github.com/observablehq/plot/blob/main/README.md#plotdotdata-options) * except that the **symbol** option is set to *circle*. + * @param {Data} data + * @param {Options=} options */ export function circle(data, options) { return dot(data, {...options, symbol: "circle"}); @@ -182,6 +197,8 @@ export function circle(data, options) { * Equivalent to * [Plot.dot](https://github.com/observablehq/plot/blob/main/README.md#plotdotdata-options) * except that the **symbol** option is set to *hexagon*. + * @param {Data} data + * @param {Options=} options */ export function hexagon(data, options) { return dot(data, {...options, symbol: "hexagon"}); diff --git a/src/marks/frame.js b/src/marks/frame.js index d97e671f66..f0ba58e48a 100644 --- a/src/marks/frame.js +++ b/src/marks/frame.js @@ -1,3 +1,9 @@ +/** + * @typedef {import("../types.js").StyleOptions} StyleOptions + * @typedef {import("../types.js").RectOptions} RectOptions + * @typedef {StyleOptions & RectOptions | undefined} Options + */ + import {create} from "../context.js"; import {number} from "../options.js"; import {Mark} from "../plot.js"; @@ -43,6 +49,8 @@ export class Frame extends Mark { * ``` * * Returns a new frame with the specified *options*. + * + * @param {Options=} options */ export function frame(options) { return new Frame(options); diff --git a/src/marks/hexgrid.js b/src/marks/hexgrid.js index 337a67c786..bd559400b1 100644 --- a/src/marks/hexgrid.js +++ b/src/marks/hexgrid.js @@ -1,3 +1,9 @@ +/** + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {{binWidth?: number}} HexOptions + * @typedef {MarkOptions & HexOptions | undefined} Options + */ + import {create} from "../context.js"; import {number} from "../options.js"; import {Mark} from "../plot.js"; @@ -16,6 +22,7 @@ const defaults = { * The **binWidth** option specifies the distance between the centers of * neighboring hexagons, in pixels (defaults to 20). The **clip** option * defaults to true, clipping the mark to the frame’s dimensions. + * @param {Options=} options */ export function hexgrid(options) { return new Hexgrid(options); diff --git a/src/marks/image.js b/src/marks/image.js index efe203a40f..d6e0ac7478 100644 --- a/src/marks/image.js +++ b/src/marks/image.js @@ -1,3 +1,7 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ import {create} from "../context.js"; import {positive} from "../defined.js"; import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, string} from "../options.js"; @@ -118,6 +122,8 @@ export class Image extends Mark { * nor **y** nor **frameAnchor** options are specified, *data* is assumed to be * an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that * **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function image(data, options = {}) { let {x, y, ...remainingOptions} = options; diff --git a/src/marks/line.js b/src/marks/line.js index a4db931266..d89ae68661 100644 --- a/src/marks/line.js +++ b/src/marks/line.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").MarkerOptions} MarkerOptions + * @typedef {MarkOptions & MarkerOptions | undefined} Options + */ + import {line as shapeLine} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; @@ -79,6 +86,8 @@ export class Line extends Mark { * nor **y** options are specified, *data* is assumed to be an array of pairs * [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, * *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function line(data, options = {}) { let {x, y, ...remainingOptions} = options; @@ -111,6 +120,8 @@ export function line(data, options = {}) { * The **interval** option is recommended to “regularize” sampled data; for * example, if your data represents timestamped temperature measurements and you * expect one sample per day, use d3.utcDay as the interval. + * @param {Data} data + * @param {Options} options */ export function lineX(data, options = {}) { const {x = identity, y = indexOf, ...remainingOptions} = options; @@ -142,6 +153,8 @@ export function lineX(data, options = {}) { * The **interval** option is recommended to “regularize” sampled data; for * example, if your data represents timestamped temperature measurements and you * expect one sample per day, use d3.utcDay as the interval. + * @param {Data} data + * @param {Options} options */ export function lineY(data, options = {}) { const {x = indexOf, y = identity, ...remainingOptions} = options; diff --git a/src/marks/linearRegression.js b/src/marks/linearRegression.js index 7cdd44902d..48bf86fa3d 100644 --- a/src/marks/linearRegression.js +++ b/src/marks/linearRegression.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {extent, range, sum, area as shapeArea, namespaces} from "d3"; import {create} from "../context.js"; import {identity, indexOf, isNone, isNoneish, maybeZ} from "../options.js"; @@ -129,6 +134,8 @@ class LinearRegressionY extends LinearRegression { * * Returns a linear regression mark where *x* is the dependent variable and *y* * is the independent variable. + * @param {Data} data + * @param {Options} options */ export function linearRegressionX(data, options = {}) { const { @@ -148,6 +155,8 @@ export function linearRegressionX(data, options = {}) { * * Returns a linear regression mark where *y* is the dependent variable and *x* * is the independent variable. + * @param {Data} data + * @param {Options} options */ export function linearRegressionY(data, options = {}) { const { diff --git a/src/marks/link.js b/src/marks/link.js index b4d9529c7d..3701fb09d7 100644 --- a/src/marks/link.js +++ b/src/marks/link.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").MarkerOptions} MarkerOptions + * @typedef {MarkOptions & MarkerOptions | undefined} Options + */ + import {path} from "d3"; import {create} from "../context.js"; import {Curve} from "../curve.js"; @@ -64,6 +71,8 @@ export class Link extends Mark { * ``` * * Returns a new link with the given *data* and *options*. + * @param {Data} data + * @param {Options} options */ export function link(data, options = {}) { let {x, x1, x2, y, y1, y2, ...remainingOptions} = options; diff --git a/src/marks/rect.js b/src/marks/rect.js index b7a18b0192..2e33f01ab5 100644 --- a/src/marks/rect.js +++ b/src/marks/rect.js @@ -1,3 +1,10 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").RectOptions} RectOptions + * @typedef {MarkOptions & RectOptions | undefined} Options + */ + import {create} from "../context.js"; import {identity, indexOf, number} from "../options.js"; import {Mark} from "../plot.js"; @@ -94,6 +101,8 @@ export class Rect extends Mark { * ``` * * Returns a new rect with the given *data* and *options*. + * @param {Data} data + * @param {Options=} options */ export function rect(data, options) { return new Rect(data, maybeTrivialIntervalX(maybeTrivialIntervalY(options))); @@ -112,6 +121,8 @@ export function rect(data, options) { * this is the typical configuration for a histogram with rects aligned at *x* = * 0. If the **x** option is not specified, it defaults to the identity * function. + * @param {Data} data + * @param {Options} options */ export function rectX(data, options = {y: indexOf, interval: 1, x2: identity}) { return new Rect(data, maybeStackX(maybeTrivialIntervalY(maybeIdentityX(options)))); @@ -130,6 +141,8 @@ export function rectX(data, options = {y: indexOf, interval: 1, x2: identity}) { * this is the typical configuration for a histogram with rects aligned at *y* = * 0. If the **y** option is not specified, it defaults to the identity * function. + * @param {Data} data + * @param {Options} options */ export function rectY(data, options = {x: indexOf, interval: 1, y2: identity}) { return new Rect(data, maybeStackY(maybeTrivialIntervalX(maybeIdentityY(options)))); diff --git a/src/marks/rule.js b/src/marks/rule.js index caedd29d89..c056141876 100644 --- a/src/marks/rule.js +++ b/src/marks/rule.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {create} from "../context.js"; import {identity, number} from "../options.js"; import {Mark} from "../plot.js"; @@ -137,6 +142,8 @@ export class RuleY extends Mark { * *y1*, and *interval*.offset(*y1*) is invoked for each *y1* to produce *y2*. * If the interval is specified as a number *n*, *y1* and *y2* are taken as the * two consecutive multiples of *n* that bracket *y*. + * @param {Data} data + * @param {Options=} options */ export function ruleX(data, options) { let {x = identity, y, y1, y2, ...rest} = maybeIntervalY(options); @@ -175,6 +182,8 @@ export function ruleX(data, options) { * *x1*, and *interval*.offset(*x1*) is invoked for each *x1* to produce *x2*. * If the interval is specified as a number *n*, *x1* and *x2* are taken as the * two consecutive multiples of *n* that bracket *x*. + * @param {Data} data + * @param {Options=} options */ export function ruleY(data, options) { let {y = identity, x, x1, x2, ...rest} = maybeIntervalX(options); diff --git a/src/marks/text.js b/src/marks/text.js index 8295272930..02c6516781 100644 --- a/src/marks/text.js +++ b/src/marks/text.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {namespaces} from "d3"; import {create} from "../context.js"; import {nonempty} from "../defined.js"; @@ -162,6 +167,8 @@ function applyMultilineText(selection, {monospace, lineAnchor, lineHeight, lineW * **x** nor **y** nor **frameAnchor** options are specified, *data* is assumed * to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such * that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function text(data, options = {}) { let {x, y, ...remainingOptions} = options; @@ -179,6 +186,8 @@ export function text(data, options = {}) { * (*interval*.floor(*y*) + *interval*.offset(*interval*.floor(*y*))) / 2. If * the interval is specified as a number *n*, *y* will be the midpoint of two * consecutive multiples of *n* that bracket *y*. + * @param {Data} data + * @param {Options} options */ export function textX(data, options = {}) { const {x = identity, ...remainingOptions} = options; @@ -195,6 +204,8 @@ export function textX(data, options = {}) { * (*interval*.floor(*x*) + *interval*.offset(*interval*.floor(*x*))) / 2. If * the interval is specified as a number *n*, *x* will be the midpoint of two * consecutive multiples of *n* that bracket *x*. + * @param {Data} data + * @param {Options} options */ export function textY(data, options = {}) { const {y = identity, ...remainingOptions} = options; diff --git a/src/marks/tick.js b/src/marks/tick.js index 9303b7c46d..0c033db813 100644 --- a/src/marks/tick.js +++ b/src/marks/tick.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {create} from "../context.js"; import {identity, number} from "../options.js"; import {Mark} from "../plot.js"; @@ -116,6 +121,8 @@ export class TickY extends AbstractTick { * * If the **y** channel is not specified, the tick will span the full vertical * extent of the plot (or facet). + * @param {Data} data + * @param {Options} options */ export function tickX(data, options = {}) { const {x = identity, ...remainingOptions} = options; @@ -139,6 +146,8 @@ export function tickX(data, options = {}) { * * If the **x** channel is not specified, the tick will span the full vertical * extent of the plot (or facet). + * @param {Data} data + * @param {Options} options */ export function tickY(data, options = {}) { const {y = identity, ...remainingOptions} = options; diff --git a/src/marks/tree.js b/src/marks/tree.js index 736c5d2338..77b07aee90 100644 --- a/src/marks/tree.js +++ b/src/marks/tree.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {cluster as Cluster} from "d3"; import {isNoneish} from "../options.js"; import {marks} from "../plot.js"; @@ -42,6 +47,8 @@ import {text} from "./text.js"; * * Any additional *options* are passed through to the constituent link, dot, and * text marks and their corresponding treeLink or treeNode transform. + * @param {Data} data + * @param {Options} options */ export function tree(data, options = {}) { let { @@ -108,6 +115,8 @@ export function tree(data, options = {}) { * [Plot.tree](https://github.com/observablehq/plot/blob/main/README.md#plottreedata-options), * except sets the **treeLayout** option to D3’s cluster (dendrogram) algorithm, * which aligns leaf nodes. + * @param {Data} data + * @param {Options=} options */ export function cluster(data, options) { return tree(data, {...options, treeLayout: Cluster}); diff --git a/src/marks/vector.js b/src/marks/vector.js index 7dc00e5700..22f1ca7f05 100644 --- a/src/marks/vector.js +++ b/src/marks/vector.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").Data} Data + * @typedef {import("../types.js").MarkOptions | undefined} Options + */ + import {create} from "../context.js"; import {radians} from "../math.js"; import {maybeFrameAnchor, maybeNumberChannel, maybeTuple, keyword, identity} from "../options.js"; @@ -83,6 +88,8 @@ export class Vector extends Mark { * **x** nor **y** options are specified, *data* is assumed to be an array of * pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, * *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function vector(data, options = {}) { let {x, y, ...remainingOptions} = options; @@ -95,6 +102,8 @@ export function vector(data, options = {}) { * [Plot.vector](https://github.com/observablehq/plot/blob/main/README.md#plotvectordata-options) * except that if the **x** option is not specified, it defaults to the identity * function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …]. + * @param {Data} data + * @param {Options} options */ export function vectorX(data, options = {}) { const {x = identity, ...remainingOptions} = options; @@ -106,6 +115,8 @@ export function vectorX(data, options = {}) { * [Plot.vector](https://github.com/observablehq/plot/blob/main/README.md#plotvectordata-options) * except that if the **y** option is not specified, it defaults to the identity * function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …]. + * @param {Data} data + * @param {Options} options */ export function vectorY(data, options = {}) { const {y = identity, ...remainingOptions} = options; diff --git a/src/plot.js b/src/plot.js index 0e53efdce0..12cc42d04c 100644 --- a/src/plot.js +++ b/src/plot.js @@ -1,3 +1,7 @@ +/** + * @typedef {import("./types.js").PlotOptions | undefined} Options + * @typedef {import("./types.js").Chart | undefined} Chart + */ import {cross, difference, groups, InternMap, select} from "d3"; import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js"; import {Channel, Channels, channelDomain, valueObject} from "./channel.js"; @@ -368,6 +372,8 @@ import {consumeWarnings, warn} from "./warnings.js"; * const color = plot.scale("color"); // retrieve the color scale object * console.log(color.range); // inspect the color scale’s range, ["red", "blue"] * ``` + * @param {Options} options + * @returns {Chart} */ export function plot(options = {}) { const {facet, style, caption, ariaLabel, ariaDescription} = options; @@ -713,6 +719,8 @@ export class Mark { * [box mark * implementation](https://github.com/observablehq/plot/blob/main/src/marks/box.js) * for an example. + * @param {Mark[]} marks + * @returns {Mark} */ export function marks(...marks) { marks.plot = Mark.prototype.plot; diff --git a/src/transforms/stack.js b/src/transforms/stack.js index 7018ff5ce9..7a971cc707 100644 --- a/src/transforms/stack.js +++ b/src/transforms/stack.js @@ -1,3 +1,8 @@ +/** + * @typedef {import("../types.js").MarkOptions} MarkOptions + * @typedef {import("../types.js").StackOptions} StackOptions + */ + import {InternMap, cumsum, group, groupSort, greatest, max, min, rollup, sum} from "d3"; import {ascendingDefined} from "../defined.js"; import {field, column, maybeColumn, maybeZ, mid, range, valueof, maybeZero, one} from "../options.js"; @@ -12,6 +17,8 @@ import {basic} from "./basic.js"; * index, *x1*, *x2* and *x* as the output channels. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackX(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); @@ -31,6 +38,8 @@ export function stackX(stack = {}, options = {}) { * used, for example, to draw a line at the left edge of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackX1(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); @@ -50,6 +59,8 @@ export function stackX1(stack = {}, options = {}) { * used, for example, to draw a line at the right edge of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackX2(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); @@ -72,6 +83,8 @@ export function stackX2(stack = {}, options = {}) { * the only argument, or as a separate *stack* options argument. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackY(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); @@ -91,6 +104,8 @@ export function stackY(stack = {}, options = {}) { * used, for example, to draw a line at the bottom of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackY1(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); @@ -110,6 +125,8 @@ export function stackY1(stack = {}, options = {}) { * used, for example, to draw a line at the top of each stacked area. * * @link https://github.com/observablehq/plot/blob/main/README.md#stack + * @param {StackOptions & MarkOptions} stack + * @param {MarkOptions} options */ export function stackY2(stack = {}, options = {}) { if (arguments.length === 1) [stack, options] = mergeOptions(stack); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000000..11556c49dd --- /dev/null +++ b/src/types.ts @@ -0,0 +1,860 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Options + */ + +// eslint-disable-next-line @typescript-eslint/ban-types +type pXX = string & {}; // p00 to p99 + +// eslint-disable-next-line @typescript-eslint/ban-types +type Channel = AccessorFunction | ValueArray | TransformMethod | (string & {}) | number | null | undefined; + +type AccessorFunction = (d: any, i: number) => any; + +type TransformMethod = { + transform: (data: Data | null | undefined) => ValueArray | Iterable | null | undefined; + label?: string; +}; + +/** + * Data + */ + +/** + * Values are associated to screen encodings (positions, colors…) via scales. + */ +type Value = number | string | Date | boolean | null | undefined; + +/** + * A Row represents a data point with values attached to field names; typically, + * a row from a tabular dataset. + */ +type Row = Record; + +/** + * A single Datum is often a Value, a Row, or an array of values; if a Row, possible field names + * can be inferred from its keys to define accessors; if an array, typical accessors are indices, + * and length, expressed as strings + */ +type Datum = Row | Value | Value[]; + +/** + * The marks data; typically an array of Datum, but can also + * be defined as an iterable compatible with Array.from. + */ +export type Data = ArrayLike | Iterable | TypedArray; + +/** + * The data is then arrayified, and a range of indices is computed, serving as pointers + * into the columnar representation of each channel + */ +type DataArray = T[] | TypedArray; + +/** + * A series is an array of indices, used to group data into classes (e.g., groups and facets) + */ +type index = number; // integer +type Series = index[] | Uint32Array; +// type Facets = Series[]; + +type NumericArray = number[] | TypedArray; +type ValueArray = NumericArray | Value[]; + +/** + * Typed arrays are preserved through arrayify + */ +type TypedArray = + | Int8Array + | Uint8Array + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Uint8ClampedArray + | Float32Array + | Float64Array; + +/** + * Plot API + * @link https://github.com/observablehq/plot/blob/main/README.md + */ + +/** + * API Types + */ +type pixels = number; + +/** + * The data is then arrayified, and a range of indices is computed, serving as pointers + * into a the column representation of Plot.valueof + */ + +/** + * Layout options (in pixels) + */ +type LayoutOptions = { + marginTop?: pixels; // the top margin + marginRight?: pixels; // the right margin + marginBottom?: pixels; // the bottom margin + marginLeft?: pixels; // the left margin + margin?: pixels; // shorthand for the four margins + width?: pixels; // the outer width of the plot (including margins) + height?: pixels; // the outer height of the plot (including margins) +}; + +/** + * Facet options + */ +type FacetOptions = { + facet?: { + data: Data; + x?: Channel; + y?: Channel; + marginTop?: pixels; // the top margin + marginRight?: pixels; // the right margin + marginBottom?: pixels; // the bottom margin + marginLeft?: pixels; // the left margin + margin?: pixels; // shorthand for the four margins + grid?: boolean; // if true, draw grid lines for each facet + label?: null; // if null, disable default facet axis labels + }; +}; + +/** + * Scale options + */ +type ScalesOptions = { + x?: ScaleOptions; + y?: ScaleOptions; + r?: ScaleOptions; + color?: ScaleOptions & ColorScaleOptions; + opacity?: ScaleOptions; + length?: ScaleOptions; + symbol?: ScaleOptions; + fx?: ScaleOptions; + fy?: ScaleOptions; + inset?: pixels; + grid?: boolean; // shorthand for x.grid and y.grid +}; + +export interface ScaleOptions { + domain?: any[]; + range?: any[]; + type?: + | "time" + | "utc" + | "diverging" + | "diverging-sqrt" + | "diverging-pow" + | "diverging-log" + | "diverging-symlog" + | "categorical" + | "ordinal" + | "cyclical" + | "sequential" + | "linear" + | "sqrt" + | "threshold" + | "quantile" + | "quantize" + | "pow" + | "log" + | "symlog" + | "utc" + | "time" + | "point" + | "band" + | "identity"; + unknown?: any; + reverse?: boolean; + interval?: any; + tickFormat?: string | ((d: any) => any); + base?: number; + exponent?: number; + clamp?: boolean; + nice?: boolean; + zero?: boolean; + percent?: boolean; + transform?: (t: any) => any; + inset?: number; + round?: boolean; + insetLeft?: number; + insetRight?: number; + insetTop?: number; + insetBottom?: number; + padding?: number; + align?: number; + paddingInner?: number; + paddingOuter?: number; + axis?: "top" | "bottom" | "left" | "right" | null; + ticks?: number; + tickSize?: number; + tickPadding?: number; + tickRotate?: number; + line?: boolean; + label?: string; + labelAnchor?: "top" | "right" | "bottom" | "left" | "center"; + labelOffset?: number; + legend?: boolean; + fontVariant?: string; + ariaLabel?: string; + ariaDescription?: string; +} + +export interface ColorScaleOptions { + scheme?: OrdinalSchemes | QuantitativeSchemes; + interpolate?: "number" | "rgb" | "hsl" | "hcl" | "lab" | ((t: number) => string); + pivot?: number; +} + +/** + * An instantiated mark + */ +type InstantiatedMark = { + initialize: (data: Data) => void; + z?: Channel; // copy the user option for error messages + clip?: "frame"; + dx: number; + dy: number; + marker?: MaybeMarkerFunction; + markerStart?: MaybeMarkerFunction; + markerMid?: MaybeMarkerFunction; + markerEnd?: MaybeMarkerFunction; + + // common styles, as constants + stroke?: string | null; + fill?: string | null; + fillOpacity?: number | null; + strokeWidth?: number | null; + strokeOpacity?: number | null; + strokeLinejoin?: string | null; + strokeLinecap?: string | null; + strokeMiterlimit?: number | null; + strokeDasharray?: string | null; + strokeDashoffset?: string | null; + target?: string | null; + ariaLabel?: string | null; + ariaDescription?: string | null; + ariaHidden?: "true" | "false"; + opacity?: number | null; + mixBlendMode?: string | null; + paintOrder?: string | null; + pointerEvents?: string | null; + shapeRendering?: string | null; + + // other styles, some of which are not supported by all marks + frameAnchor?: string; + + // other properties + sort?: SortOption | null; +}; + +/** + * A mark + */ +type Mark = InstantiatedMark | (() => SVGElement | null | undefined) | Mark[]; +type MarksOptions = {marks?: Mark[]}; + +/** + * Other top-level options + */ +type TopLevelOptions = { + ariaLabel?: string | null; + ariaDescription?: string | null; + caption?: string | HTMLElement; // a caption + className?: string; + document?: Document; // the document used to create plot elements +}; + +/** + * Plot options + */ +export type PlotOptions = LayoutOptions & + FacetOptions & + MarksOptions & {style?: string | Partial} & ScalesOptions & + TopLevelOptions; + +/** + * Legend options + */ +type LegendOptions = any; // TODO + +/** + * Plot returns a SVG or a FIGURE element, with additional properties + */ +export interface Chart extends HTMLElement { + scale: ( + scaleName: "x" | "y" | "r" | "color" | "opacity" | "length" | "symbol" | "fx" | "fy" + ) => ScaleOptions | undefined; + legend: (scaleName: "color" | "opacity" | "symbol", legendOptions: LegendOptions) => HTMLElement | undefined; +} + +type MaybeSymbol = Channel | SymbolName | SymbolObject; + +/** + * Mark channel options + */ +type ChannelOptions = { + x?: Channel; // TODO: OptionsX + x1?: Channel; + x2?: Channel; + y?: Channel; // TODO: OptionsY + y1?: Channel; + y2?: Channel; + z?: Channel; + fill?: Channel; + fillOpacity?: Channel; + stroke?: Channel; + strokeOpacity?: Channel; + strokeWidth?: Channel; + title?: Channel; + opacity?: Channel; +}; + +export type DotOptions = { + r?: Channel; + symbol?: MaybeSymbol; +}; + +/** + * Mark constant style options + */ +export type StyleOptions = { + ariaDescription?: string; + ariaHidden?: boolean; + target?: string; + strokeLinecap?: string; + strokeLinejoin?: string; + strokeMiterlimit?: pixels; + strokeDasharray?: pixels | string | pixels[]; + strokeDashoffset?: string; + mixBlendMode?: string; + paintOrder?: string; + pointerEvents?: string; + shapeRendering?: string; + clip?: boolean | null | undefined; +}; + +/** + * Rect options for marks + */ +export type RectOptions = RadiusOptions & InsetOptionsX & InsetOptionsY; + +export type RadiusOptions = {rx?: number; ry?: number}; + +export type InsetOptionsX = { + inset?: pixels; + insetTop?: pixels; + insetBottom?: pixels; +}; + +export type InsetOptionsY = { + inset?: pixels; + insetLeft?: pixels; + insetRight?: pixels; +}; + +/** + * Interval options + * +If the interval option is specified, the binX transform is implicitly applied to the specified options. The reducer of the output y channel may be specified via the reduce option, which defaults to first. To default to zero instead of showing gaps in data, as when the observed value represents a quantity, use the sum reducer. + +Plot.lineY(observations, {x: "date", y: "temperature", interval: d3.utcDay}) +The interval option is recommended to “regularize” sampled data; for example, if your data represents timestamped temperature measurements and you expect one sample per day, use d3.utcDay as the interval. + * @link TODO: unclear where to link + * TODO: accept number or Date (for e.g., d3.utcYear) + */ +type Interval = number | IntervalObject; +type IntervalObject = { + floor: (v: number) => number; + offset: (v: number) => number; + range: (lo: number, hi: number) => number[]; +}; + +/** + * The Plot.normalizeX and Plot.normalizeY transforms normalize series values relative to the given basis. For example, if the series values are [y₀, y₁, y₂, …] and the first basis is used, the mapped series values would be [y₀ / y₀, y₁ / y₀, y₂ / y₀, …] as in an index chart. The basis option specifies how to normalize the series values. The following basis methods are supported: + * + * * first - the first value, as in an index chart; the default + * * last - the last value + * * min - the minimum value + * * max - the maximum value + * * mean - the mean value (average) + * * median - the median value + * * pXX - the percentile value, where XX is a number in [00,99] + * * sum - the sum of values + * * extent - the minimum is mapped to zero, and the maximum to one + * * deviation - each value is transformed by subtracting the mean and then dividing by the standard deviation + * * a function to be passed an array of values, returning the desired basis + */ +type Basis = "first" | "last" | "min" | "max" | "median" | "p95" | pXX | "sum" | "extent" | "deviation" | BasisFunction; +type BasisFunction = (V: ValueArray) => Value; + +/** + * Other Mark options + */ +type OtherMarkOptions = { + // the following are necessarily channels + title?: Channel; + href?: Channel; + ariaLabel?: Channel; + // filter & sort + filter?: Channel; + sort?: SortOption | null; + reverse?: boolean; + // include in facet + facet?: "auto" | "include" | "exclude" | boolean | null; + // interval + interval?: number | Interval; + // basis for the normalize transform + basis?: Basis; + // transform + transform?: TransformOption; + initializer?: InitializerOption; +}; + +/** + * Aggregation options for Series transforms (group, bin, sort…): + * * a string describing an aggregation (first, min, sum, count…) + * * a function - passed the array of values for each series + * * an object with a reduce method, an optionally a scope + * @link https://github.com/observablehq/plot/blob/main/README.md#group + */ +type AggregationMethod = + | (( + | "first" + | "last" + | "count" + | "sum" + | "proportion" + | "proportion-facet" + | "min" + | "min-index" + | "max" + | "max-index" + | "mean" + | "median" + | "mode" + | "p25" + | "p95" + | pXX + | "deviation" + | "variance" + | AggregationFunction + ) & {reduce?: never}) // duck-typing in maybeReduce + | Aggregate; + +/** + * An object with a reduce method, and optionally a scope, for the group transform or the bin transform + * + * the reduce method is repeatedly passed three arguments: the index for each bin (an array + * of integers), the input channel’s array of values, and the extent of the bin (an object + * {x1, x2, y1, y2}); it must then return the corresponding aggregate value for the bin. + * If the reducer object’s scope is “data”, then the reduce method is first invoked for the + * full data; the return value of the reduce method is then made available as a third argument + * (making the extent the fourth argument). Similarly if the scope is “facet”, then the reduce + * method is invoked for each facet, and the resulting reduce value is made available while + * reducing the facet’s bins. (This optional scope is used by the proportion and proportion-facet + * reducers.) + * @link https://github.com/observablehq/plot/blob/main/README.md#bin + * @link https://github.com/observablehq/plot/blob/main/README.md#group + */ + +type Aggregate = { + label?: string; + reduce: ( + I: Series, + X: ValueArray, + contextOrExtent?: Value | BinExtent | null, + extent?: BinExtent + ) => Value | null | undefined; + scope?: "data" | "facet"; +}; + +/** + * The extent of a bin, for extent-based reducers + */ +type BinExtent = { + x1?: number | Date; + x2?: number | Date; + y1?: number | Date; + y2?: number | Date; +}; + +type AggregationFunction = (values?: ValueArray, extent?: BinExtent) => Value; + +/** + * The sort option, inside a mark, might sort the mark's data if specified as a string or a function. + * If specified as an object, it will sort the domain of an associated scale + * @link https://github.com/observablehq/plot/blob/main/README.md#transforms + * @link https://github.com/observablehq/plot/blob/main/README.md#sort-options + */ +type SortOption = ( + | // Field, accessor or comparator + ((string | ((d: Datum) => Datum) | Comparator) & {value?: never; channel?: never}) // duck-typing in isDomainSort + | ChannelSortOption + | DomainSortOption +) & {transform?: never}; // duck-typing in isOptions + +/** + * A comparator function returns a number to sort two values + */ +type Comparator = (a: Datum, b: Datum) => number; + +type SortChannel = "x" | "y" | "r" | "data"; + +type ChannelSortOption = { + channel: SortChannel; + order?: "descending" | "ascending" | "none"; // TODO + value?: never; // duck-typing in isDomainSort +}; + +type DomainSortOption = { + x?: SortChannel | SortValue; + y?: SortChannel | SortValue; + fx?: SortChannel | SortValue; + fy?: SortChannel | SortValue; + color?: SortChannel | SortValue; + reduce?: AggregationMethod; + reverse?: boolean; + limit?: number | [number, number]; + channel?: never; // duck-typing in isDomainSort + value?: never; // duck-typing in isDomainSort +}; +type SortValue = { + value: SortChannel; + reduce?: AggregationMethod; + reverse?: boolean; + limit?: number | [number, number]; +}; + +/** + * Definition for the transform and initializer functions. + */ +type TransformFunction = ( + this: InstantiatedMark, + data: DataArray, + facets: Series[] +) => {data: DataArray; facets: Series[]; channels?: never}; + +/** + * Plot’s transforms provide a convenient mechanism for transforming data as part of a plot specification. + * @link https://github.com/observablehq/plot/blob/main/README.md#transforms + */ +type TransformOption = TransformFunction | null | undefined; + +/** + * Initializers can be used to transform and derive new channels prior to rendering. Unlike transforms which + * operate in abstract data space, initializers can operate in screen space such as pixel coordinates and colors. + * For example, initializers can modify a marks’ positions to avoid occlusion. Initializers are invoked *after* + * the initial scales are constructed and can modify the channels or derive new channels; these in turn may (or + * may not, as desired) be passed to scales. + * @link https://github.com/observablehq/plot/blob/main/README.md#initializers + */ +type InitializerFunction = ( + this: InstantiatedMark, + data: DataArray, + facets: Series[], + channels?: any, + scales?: any, + dimensions?: Dimensions +) => {data: DataArray; facets: Series[]; channels?: any}; +type InitializerOption = InitializerFunction | TransformOption; + +/** + * Stack options + * @link https://github.com/observablehq/plot/blob/main/README.md#stack + */ +export type StackOptions = {offset?: Offset; order?: StackOrder; reverse?: boolean}; + +/** + * Arrow options + */ +export type ArrowOptions = { + bend?: number | boolean; + headAngle?: number; + inset?: number; + insetStart?: number; + insetEnd?: number; + length?: number; +}; + +/** + * Stack order options: + * The following order methods are supported: + * + * * null - input order (default) + * * value - ascending value order (or descending with reverse) + * * sum - order series by their total value + * * appearance - order series by the position of their maximum value + * * inside-out - order the earliest-appearing series on the inside + * * a named field or function of data - order data by priority + * * an array of z values + * @link https://github.com/observablehq/plot/blob/main/README.md#stack + */ +type StackOrder = null | "value" | "sum" | "appearance" | "inside-out" | string | ((d: Datum) => Value) | ValueArray; + +/** + * Stack offset options + * + * After all values have been stacked from zero, an optional offset can be applied to translate or scale the stacks. The following offset methods are supported: + * + * * null - a zero baseline (default) + * * expand (or normalize) - rescale each stack to fill [0, 1] + * * center (or silhouette) - align the centers of all stacks + * * wiggle - translate stacks to minimize apparent movement + * * a function to be passed a nested index, and start, end, and z values + * + * If a given stack has zero total value, the expand offset will not adjust the stack’s position. Both the center and wiggle offsets ensure that the lowest element across stacks starts at zero for better default axes. The wiggle offset is recommended for streamgraphs, and if used, changes the default order to inside-out; see Byron & Wattenberg. + * + * If the offset is specified as a function, it will receive four arguments: an index of stacks nested by facet and then stack, an array of start values, an array of end values, and an array of z values. For stackX, the start and end values correspond to x1 and x2, while for stackY, the start and end values correspond to y1 and y2. The offset function is then responsible for mutating the arrays of start and end values, such as by subtracting a common offset for each of the indices that pertain to the same stack. + */ +type Offset = null | "expand" | "center" | "wiggle" | OffsetFunction; +type OffsetFunction = (stacks: Series[][], Y1: Float64Array, Y2: Float64Array, Z?: ValueArray | null) => void; + +/** + * Window options + * + * The Plot.windowX and Plot.windowY transforms compute a moving window around each data point and then derive a summary statistic from values in the current window, say to compute rolling averages, rolling minimums, or rolling maximums. These transforms also take additional options: + * + * * k - the window size (the number of elements in the window) + * * anchor - how to align the window: start, middle, or end + * * reduce - the aggregation method (window reducer) + * * strict - if true, output undefined if any window value is undefined; defaults to false + * + * If the strict option is true, the output start values or end values or both (depending on the anchor) of each series may be undefined since there are not enough elements to create a window of size k; output values may also be undefined if some of the input values in the corresponding window are undefined. If the strict option is false (the default), the window will be automatically truncated as needed, and undefined input values are ignored. For example, if k is 24 and anchor is middle, then the initial 11 values have effective window sizes of 13, 14, 15, … 23, and likewise the last 12 values have effective window sizes of 23, 22, 21, … 12. Values computed with a truncated window may be noisy; if you would prefer to not show this data, set the strict option to true. + * + * The following window reducers are supported: + * + * * min - the minimum + * * max - the maximum + * * mean - the mean (average) + * * median - the median + * * mode - the mode (most common occurrence) + * * pXX - the percentile value, where XX is a number in [00,99] + * * sum - the sum of values + * * deviation - the standard deviation + * * variance - the variance per Welford’s algorithm + * * difference - the difference between the last and first window value + * * ratio - the ratio of the last and first window value + * * first - the first value + * * last - the last value + * * a function to be passed an array of k values + * + * @link https://github.com/observablehq/plot/blob/main/README.md#plotwindowk + */ +export type WindowOptions = { + k?: number; + anchor?: "start" | "middle" | "end"; + reduce?: + | "min" + | "max" + | "mean" + | "median" + | "mode" + | "p25" + | "p95" + | pXX + | "sum" + | "deviation" + | "variance" + | "difference" + | "ratio" + | "first" + | "last" + | ((values: ValueArray) => Value); + strict?: boolean; + shift?: "deprecated"; +}; + +/** + * Mark options (as passed by the user or returned by a transform) + */ +export type MarkOptions = ChannelOptions & StyleOptions & OtherMarkOptions; + +/** + * The dimensions passed to a mark's render function + */ +type Dimensions = { + width: pixels; + height: pixels; + marginLeft: pixels; + marginRight: pixels; + marginTop: pixels; + marginBottom: pixels; +}; + +/** + * A marker defines a graphic drawn on vertices of a delaunay, line or a link mark + * @link https://github.com/observablehq/plot/blob/main/README.md#markers + */ +type MarkerOption = "none" | "arrow" | "dot" | "circle" | "circle-stroke" | MarkerFunction | boolean | null | undefined; +export type MarkerOptions = { + marker?: MarkerOption; + markerStart?: MarkerOption; + markerMid?: MarkerOption; + markerEnd?: MarkerOption; +}; +type MarkerFunction = (color: string, document: Context["document"]) => SVGElement; +type MaybeMarkerFunction = MarkerFunction | null; + +/** + * Ordinal color schemes + */ +type OrdinalSchemes = + | "accent" + | "category10" + | "dark2" + | "paired" + | "pastel1" + | "pastel2" + | "set1" + | "set2" + | "set3" + | "tableau10" + | "brbg" + | "prgn" + | "piyg" + | "puor" + | "rdbu" + | "rdgy" + | "rdylbu" + | "rdylgn" + | "spectral" + | "burd" + | "buylrd" + | "blues" + | "greens" + | "greys" + | "oranges" + | "purples" + | "reds" + | "turbo" + | "viridis" + | "magma" + | "inferno" + | "plasma" + | "cividis" + | "cubehelix" + | "warm" + | "cool" + | "bugn" + | "bupu" + | "gnbu" + | "orrd" + | "pubu" + | "pubugn" + | "purd" + | "rdpu" + | "ylgn" + | "ylgnbu" + | "ylorbr" + | "ylorrd" + | "rainbow" + | "sinebow"; + +/** + * Quantitative color schemes + */ +type QuantitativeSchemes = DivergingSchemes | SequentialSchemes | CyclicalSchemes; + +/** + * Diverging color schemes + */ +type DivergingSchemes = + | "brbg" + | "prgn" + | "piyg" + | "puor" + | "rdbu" + | "rdgy" + | "rdylbu" + | "rdylgn" + | "spectral" + | "burd" + | "buylrd"; + +/** + * Sequential color schemes + */ +type SequentialSchemes = + | "blues" + | "greens" + | "greys" + | "purples" + | "reds" + | "oranges" + | "turbo" + | "viridis" + | "magma" + | "inferno" + | "plasma" + | "cividis" + | "cubehelix" + | "warm" + | "cool" + | "bugn" + | "bupu" + | "gnbu" + | "orrd" + | "pubugn" + | "pubu" + | "purd" + | "rdpu" + | "ylgnbu" + | "ylgn" + | "ylorbr" + | "ylorrd"; + +/** + * Cyclical color schemes + */ +type CyclicalSchemes = "rainbow" | "sinebow"; + +/** + * The context in which Plot operates + */ +interface Context { + document: Document; +} + +/** + * Know symbols for dots + * @link https://github.com/observablehq/plot/blob/main/README.md#dot + */ +type SymbolName = + | "asterisk" + | "circle" + | "cross" + | "diamond" + | "diamond2" + | "hexagon" + | "plus" + | "square" + | "square2" + | "star" + | "times" + | "triangle" + | "triangle2" + | "wye"; + +/** + * A symbol object with a draw function + * @link https://github.com/d3/d3-shape/blob/main/README.md#custom-symbol-types + */ +type SymbolObject = {draw: (context: CanvasPath, size: number) => void}; + +/** + * A restrictive definition of D3 selections + */ +type Selection = { + append: (name: string) => Selection; + attr: (name: string, value: any) => Selection; + call: (callback: (selection: Selection, ...args: any[]) => void, ...args: any[]) => Selection; + each: (callback: (d: any) => void) => Selection; + filter: (filter: (d: any, i: number) => boolean) => Selection; + property: (name: string, value: any) => Selection; + style: (name: string, value: any) => Selection; + text: (value: any) => Selection; + [Symbol.iterator]: () => IterableIterator; +}; diff --git a/test/plots/index.html b/test/plots/index.html index 9f94ddb72b..f51ed23e0f 100644 --- a/test/plots/index.html +++ b/test/plots/index.html @@ -1,6 +1,6 @@ - +