Skip to content

Script to keep README in sync with JSDoc #1034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 60 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
5dc70d6
Add script to inject JSDoc into README.md
Jul 28, 2022
1d34ce2
yarn readme:update
Jul 28, 2022
d98fab4
emit declaration files to types/
Aug 4, 2022
119e0ef
jsdoc-to-readme - allow specifiying the title prefix
Aug 23, 2022
cd18fa3
use full urls for readme links
Aug 23, 2022
9668a22
JSDoc for scales
Aug 23, 2022
8a60c10
jsdoc legend
Aug 23, 2022
1a8b1b3
fix link
Aug 23, 2022
38dd8ec
jsdoc-to-readme - indicate in title when a param uses dot-dot-dot not…
Aug 23, 2022
8ac36fd
jsdoc marks
Aug 23, 2022
658871a
fix link
Aug 23, 2022
42f2e61
jsdoc area
Aug 23, 2022
9a43100
jsdoc areaX
Aug 23, 2022
0cc56ff
jsdoc areaY
Aug 23, 2022
021d1f8
jsdoc arrow
Aug 23, 2022
1c3137e
jsdoc barX
Aug 23, 2022
6a8032e
jsdoc barY
Aug 23, 2022
2814bb0
jsdoc boxX, boxY
Aug 23, 2022
57d18b9
jsdoc cell, cellX, cellY
Aug 23, 2022
4904ab3
jsdoc delaunayLink, delaunayMesh
Aug 23, 2022
2d47b19
jsdoc hull, voronoi, voronoiMesh
Aug 23, 2022
efa31a7
jsdoc density
Aug 23, 2022
f1c9468
jsdoc dot, dotX, dotY
Aug 23, 2022
1b2bae0
jsdoc circle, hexagon
Aug 23, 2022
dab682a
jsdoc hexgrid
Aug 23, 2022
bd10157
jsdoc image
Aug 23, 2022
4574b7b
jsdoc linearRegressionX, linearRegressionY
Aug 23, 2022
9c96d02
jsdoc line, lineX, lineY
Aug 23, 2022
2a075b1
jsdoc link
Aug 23, 2022
9124408
jsdoc rect, rectX, rectY
Aug 23, 2022
9ce7efa
jsdoc ruleX, ruleY
Aug 23, 2022
585f854
jsdoc text, textX, textY
Aug 23, 2022
235da27
jsdoc tickX, tickY
Aug 23, 2022
3ca8142
jsdoc vector, vectorX, vectorY
Aug 23, 2022
3eb3c6f
jsdoc frame
Aug 23, 2022
ccf4cd5
jsdoc sort, shuffle, reverse, filter
Aug 23, 2022
2712ebb
jsdoc bin, binX, binY
Aug 23, 2022
400f3f1
jsdoc group, groupX, groupY, groupZ
Aug 23, 2022
b162e60
jsdoc map, mapX, mapY
Aug 23, 2022
3a75fca
jsdoc normalize, normalizeX, normalizeY
Aug 23, 2022
d8f3b91
jsdoc select*
Aug 23, 2022
1bf6098
jsdoc stack*
Aug 23, 2022
3d17a80
jsdoc tree, cluster, treeNode, treeLink
Aug 23, 2022
28aaa70
jsdoc dodgeX, dodgeY
Aug 23, 2022
9addc76
jsdoc hexbin
Aug 23, 2022
e1177b2
jsdoc initializer
Aug 23, 2022
e79a04d
jsdoc format*
Aug 23, 2022
5b3f935
fix mapX, mapY
Aug 23, 2022
dac50b5
revert d.ts related changes for another PR
Aug 24, 2022
f5204ea
Hard-wrap JSDoc comments
Aug 25, 2022
924729e
run readme:check as part of yarn test
Aug 25, 2022
ac5bf04
jsdoc scale - move content from JSDoc to the README
Aug 25, 2022
02c5825
valueof arrayType->type
Aug 25, 2022
21476de
use map argument name in map functions
Aug 26, 2022
32373b8
use original README as source of truth for argument names throughout
Aug 26, 2022
7c520d3
jsdoc-to-readme - make links to other parts of README relative
Aug 26, 2022
839f33c
fix README
Aug 26, 2022
9899f68
fix console.log
Aug 26, 2022
e899666
resolve conflicts
Aug 26, 2022
b1fb27b
yarn readme:update
Aug 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ jobs:
echo ::add-matcher::.github/eslint.json
yarn run eslint . --format=compact
- run: yarn test
- run: yarn readme:check
1,275 changes: 1,076 additions & 199 deletions README.md

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
"src/**/*.js"
],
"scripts": {
"test": "yarn test:typecheck && yarn test:lint && yarn test:mocha",
"test": "yarn test:typecheck && yarn test:lint && yarn readme:check && yarn test:mocha",
"test:mocha": "mkdir -p test/output && mocha --conditions=mocha 'test/**/*-test.*' 'test/plot.js'",
"test:lint": "eslint src test",
"test:typecheck": "tsc --noEmit",
"readme:check": "tsx scripts/jsdoc-to-readme.ts --check",
"readme:update": "tsx scripts/jsdoc-to-readme.ts",
"prepublishOnly": "rm -rf dist && rollup -c && tsc",
"postpublish": "git push && git push --tags",
"dev": "vite"
Expand Down Expand Up @@ -63,7 +65,9 @@
"prettier": "^2.7.1",
"rollup": "2",
"rollup-plugin-terser": "7",
"ts-morph": "^15.1.0",
"tslib": "^2.4.0",
"tsx": "^3.8.0",
"typescript": "^4.6.4",
"typescript-module-alias": "^1.0.2",
"vite": "3"
Expand Down
119 changes: 119 additions & 0 deletions scripts/jsdoc-to-readme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {readFileSync, writeFileSync} from "fs";
import type {ExportedDeclarations, FunctionDeclaration} from "ts-morph";
import {Project} from "ts-morph";

/**
* This script will find html comments in the README of the below shape and
* inject the corresponding JSDoc from that exported symbol.
*
* <!-- jsdoc column -->
* <!-- jsdocEnd -->
*/

const readmePath = "README.md";
let indexPath = "src/index.js";
const project = new Project({tsConfigFilePath: "tsconfig.json"});

let index = project.getSourceFile(indexPath);
if (!index) {
indexPath = "src/index.ts";
index = project.getSourceFile(indexPath);
if (!index) throw new Error(`index file not found in src/`);
}

const exported = index.getExportedDeclarations();
function getByApiName(name: string) {
for (const [exportedName, declarations] of exported) {
if (name === exportedName) {
return declarations[0];
}
}
}

function injectJsDoc(readme: string) {
const lines = readme.split("\n");
const output: string[] = [];
let insideReplacement = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let replacement = "";
let isReplacementDelimiter = false;
if (line.startsWith("<!-- jsdocEnd")) {
if (!insideReplacement) throw new Error(`Unexpected jsdocEnd on line ${i}.`);
isReplacementDelimiter = true;
insideReplacement = false;
} else if (line.startsWith("<!-- jsdoc ")) {
isReplacementDelimiter = true;
insideReplacement = true;
const parts = [""];
const match = line.match(/jsdoc\s+(#+)?\s?(.+)\s/);
if (!match || match.length < 2) throw new Error(`Malformed jsdoc comment in README.md on line ${i}.`);
const [, prefix, name] = match;
const declaration = getByApiName(name);
if (!declaration) throw new Error(`${name} is not exported by src/index`);
parts.push(getJsDocs(name, declaration, prefix));
parts.push("");
replacement = parts.join("\n");
}
if (!insideReplacement || isReplacementDelimiter) output.push(line);
if (replacement) output.push(replacement);
}
return output.join("\n");
}

function getJsDocs(name: string, declaration: ExportedDeclarations, prefix = "####") {
if ("getParameters" in declaration) {
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 `JSDoc extraction for ${declaration.getKindName()} not yet implemented.`;
}

function getJsDocsForFunction(name: string, declaration: FunctionDeclaration, prefix = "####") {
const parameters = declaration.getParameters();
const title = `${prefix} Plot.${name}(${parameters
.map((param) => `${param.getDotDotDotToken() ? "..." : ""}*${param.getName()}*`)
.join(", ")})`;
const parts = [title];
const docs = declaration.getJsDocs();
if (docs.length) {
parts.push(docs.map((doc) => makeRelativeUrls(doc.getDescription())).join("\n\n"));
return parts.join("\n");
}
// If we didn't find docs on the implementation, it's probably on one of the
// overloads.
const overloads = declaration.getOverloads();
for (const overload of overloads) {
const docs = overload.getJsDocs();
if (!docs.length) continue;
parts.push(docs.map((doc) => makeRelativeUrls(doc.getDescription())).join("\n\n"));
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);

if (original !== output) {
if (check) {
console.log("README.md is out of sync. Please run `yarn readme:update`");
process.exit(1);
} else {
writeFileSync(readmePath, output);
console.log("README.md has been updated based on the JSDoc annotations.");
}
} else {
console.log("README.md requires no changes.");
}
45 changes: 39 additions & 6 deletions src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,54 @@ export function formatNumber(locale = "en-US"): (value: any) => string | undefin
return (i: any) => (i != null && !isNaN(i) ? format.format(i) : undefined);
}

/**
* ```js
* Plot.formatMonth("es-MX", "long")(0) // "enero"
* ```
*
* Returns a function that formats a given month number (from 0 = January to 11
* = December) according to the specified *locale* and *format*. The *locale* is
* a [BCP 47 language tag](https://tools.ietf.org/html/bcp47) and defaults to
* U.S. English. The *format* is a [month
* format](https://tc39.es/ecma402/#datetimeformat-objects): either *2-digit*,
* *numeric*, *narrow*, *short*, *long*; if not specified, it defaults to
* *short*.
*/
export function formatMonth(
locale = "en-US",
month: "numeric" | "2-digit" | "long" | "short" | "narrow" | undefined = "short"
format: "numeric" | "2-digit" | "long" | "short" | "narrow" | undefined = "short"
) {
const format = monthFormat(locale, month);
const fmt = monthFormat(locale, format);
return (i: Date | number | null | undefined) =>
i != null && !isNaN((i = +new Date(Date.UTC(2000, +i)))) ? format.format(i) : undefined;
i != null && !isNaN((i = +new Date(Date.UTC(2000, +i)))) ? fmt.format(i) : undefined;
}

export function formatWeekday(locale = "en-US", weekday: "long" | "short" | "narrow" | undefined = "short") {
const format = weekdayFormat(locale, weekday);
/**
* ```js
* Plot.formatWeekday("es-MX", "long")(0) // "domingo"
* ```
*
* Returns a function that formats a given week day number (from 0 = Sunday to 6
* = Saturday) according to the specified *locale* and *format*. The *locale* is
* a [BCP 47 language tag](https://tools.ietf.org/html/bcp47) and defaults to
* U.S. English. The *format* is a [weekday
* format](https://tc39.es/ecma402/#datetimeformat-objects): either *narrow*,
* *short*, or *long*; if not specified, it defaults to *short*.
*/
export function formatWeekday(locale = "en-US", format: "long" | "short" | "narrow" | undefined = "short") {
const fmt = weekdayFormat(locale, format);
return (i: Date | number | null | undefined) =>
i != null && !isNaN((i = +new Date(Date.UTC(2001, 0, +i)))) ? format.format(i) : undefined;
i != null && !isNaN((i = +new Date(Date.UTC(2001, 0, +i)))) ? fmt.format(i) : undefined;
}

/**
* ```js
* Plot.formatIsoDate(new Date("2020-01-01T00:00.000Z")) // "2020-01-01"
* ```
*
* Given a *date*, returns the shortest equivalent ISO 8601 UTC string. If the
* given *date* is not valid, returns `"Invalid Date"`.
*/
export function formatIsoDate(date: Date): string {
return isoFormat(date, "Invalid Date");
}
Expand Down
24 changes: 24 additions & 0 deletions src/legends.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ const legendRegistry = new Map([
["opacity", legendOpacity]
]);

/**
* Returns a standalone legend for the scale defined by the given *options*
* object. The *options* object must define at least one scale; see [Scale
* options](https://github.com/observablehq/plot/blob/main/README.md#scale-options)
* for how to define a scale. For example, here is a ramp legend of a linear
* color scale with the default domain of [0, 1] and default scheme *turbo*:
*
* ```js
* Plot.legend({color: {type: "linear"}})
* ```
*
* The *options* object may also include any additional legend options described
* in the previous section. For example, to make the above legend slightly
* wider:
*
* ```js
* Plot.legend({
* width: 320,
* color: {
* type: "linear"
* }
* })
* ```
*/
export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
Expand Down
77 changes: 77 additions & 0 deletions src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,93 @@ export class Area extends Mark {
}
}

/**
* ```js
* Plot.area(aapl, {x1: "Date", y1: 0, y2: "Close"})
* ```
*
* Returns a new area with the given *data* and *options*. Plot.area is rarely
* used directly; it is only needed when the baseline and topline have neither
* common *x* nor *y* values.
* [Plot.areaY](https://github.com/observablehq/plot/blob/main/README.md#plotareaydata-options)
* is used in the common horizontal orientation where the baseline and topline
* share *x* values, while
* [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.
*/
export function area(data, options) {
if (options === undefined) return areaY(data, {x: first, y: second});
return new Area(data, options);
}

/**
* ```js
* Plot.areaX(aapl, {y: "Date", x: "Close"})
* ```
*
* Returns a new area with the given *data* and *options*. This constructor is
* used when the baseline and topline share *y* values, as in a time-series area
* chart where time goes up↑. If neither the **x1** nor **x2** option is
* specified, the **x** option may be specified as shorthand to apply an
* implicit [stackX
* transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackxstack-options);
* this is the typical configuration for an area chart with a baseline at *x* =
* 0. If the **x** option is not specified, it defaults to the identity
* function. The **y** option specifies the **y1** channel; and the **y1** and
* **y2** options are ignored.
*
* If the **interval** option is specified, the [binY
* transform](https://github.com/observablehq/plot/blob/main/README.md#bin) is
* implicitly applied to the specified *options*. The reducer of the output *x*
* 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.
*
* ```js
* Plot.areaX(observations, {y: "date", x: "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.
*/
export function areaX(data, options) {
const {y = indexOf, ...rest} = maybeDenseIntervalY(options);
return new Area(data, maybeStackX(maybeIdentityX({...rest, y1: y, y2: undefined})));
}

/**
* ```js
* Plot.areaY(aapl, {x: "Date", y: "Close"})
* ```
*
* Returns a new area with the given *data* and *options*. This constructor is
* used when the baseline and topline share *x* values, as in a time-series area
* chart where time goes right→. If neither the **y1** nor **y2** option is
* specified, the **y** option may be specified as shorthand to apply an
* implicit [stackY
* transform](https://github.com/observablehq/plot/blob/main/README.md#plotstackystack-options);
* this is the typical configuration for an area chart with a baseline at *y* =
* 0. If the **y** option is not specified, it defaults to the identity
* function. The **x** option specifies the **x1** channel; and the **x1** and
* **x2** options are ignored.
*
* If the **interval** option is specified, the [binX
* transform](https://github.com/observablehq/plot/blob/main/README.md#bin) 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.
*
* ```js
* Plot.areaY(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.
*/
export function areaY(data, options) {
const {x = indexOf, ...rest} = maybeDenseIntervalX(options);
return new Area(data, maybeStackY(maybeIdentityY({...rest, x1: x, x2: undefined})));
Expand Down
12 changes: 10 additions & 2 deletions src/marks/arrow.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,16 @@ function circleCircleIntersect([ax, ay, ar], [bx, by, br], sign) {
return [ax + (dx * x + dy * y) / d, ay + (dy * x - dx * y) / d];
}

export function arrow(data, {x, x1, x2, y, y1, y2, ...options} = {}) {
/**
* ```js
* Plot.arrow(inequality, {x1: "POP_1980", y1: "R90_10_1980", x2: "POP_2015", y2: "R90_10_2015", bend: true})
* ```
*
* Returns a new arrow with the given *data* and *options*.
*/
export function arrow(data, options = {}) {
let {x, x1, x2, y, y1, y2, ...remainingOptions} = options;
[x1, x2] = maybeSameValue(x, x1, x2);
[y1, y2] = maybeSameValue(y, y1, y2);
return new Arrow(data, {...options, x1, x2, y1, y2});
return new Arrow(data, {...remainingOptions, x1, x2, y1, y2});
}
Loading