enarratio-astro 0.1.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -22
- package/LICENSE +21 -21
- package/README.md +155 -151
- package/dist/chart.d.ts +24 -2
- package/dist/chart.js +16 -12
- package/dist/index.d.ts +1 -1
- package/dist/markdown.js +8 -1
- package/package.json +66 -66
- package/src/Chart.astro +31 -31
- package/src/Enhance.astro +21 -21
- package/src/chart.ts +209 -177
- package/src/data.ts +156 -156
- package/src/env.d.ts +7 -7
- package/src/figure.ts +14 -14
- package/src/index.ts +18 -17
- package/src/markdown.ts +171 -164
- package/src/once.ts +11 -11
- package/src/styles.ts +36 -36
package/package.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "enarratio-astro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Accessible charts for Astro: a Chart component and charts in Markdown, drawn on the server by Enarratio",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"astro",
|
|
7
|
-
"astro-component",
|
|
8
|
-
"withastro",
|
|
9
|
-
"charts",
|
|
10
|
-
"accessibility",
|
|
11
|
-
"markdown",
|
|
12
|
-
"enarratio"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://github.com/DrDustinEdwards/astro/tree/main/packages/enarratio-astro#readme",
|
|
15
|
-
"bugs": {
|
|
16
|
-
"url": "https://github.com/DrDustinEdwards/astro/issues"
|
|
17
|
-
},
|
|
18
|
-
"repository": {
|
|
19
|
-
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/DrDustinEdwards/astro.git",
|
|
21
|
-
"directory": "packages/enarratio-astro"
|
|
22
|
-
},
|
|
23
|
-
"license": "MIT",
|
|
24
|
-
"author": "Dustin Edwards (https://dustinedwards.info)",
|
|
25
|
-
"type": "module",
|
|
26
|
-
"sideEffects": false,
|
|
27
|
-
"exports": {
|
|
28
|
-
".": {
|
|
29
|
-
"types": "./dist/index.d.ts",
|
|
30
|
-
"import": "./dist/index.js"
|
|
31
|
-
},
|
|
32
|
-
"./markdown": {
|
|
33
|
-
"types": "./dist/markdown.d.ts",
|
|
34
|
-
"import": "./dist/markdown.js"
|
|
35
|
-
},
|
|
36
|
-
"./Chart.astro": "./src/Chart.astro",
|
|
37
|
-
"./Enhance.astro": "./src/Enhance.astro",
|
|
38
|
-
"./package.json": "./package.json"
|
|
39
|
-
},
|
|
40
|
-
"files": [
|
|
41
|
-
"dist",
|
|
42
|
-
"src",
|
|
43
|
-
"CHANGELOG.md",
|
|
44
|
-
"README.md"
|
|
45
|
-
],
|
|
46
|
-
"engines": {
|
|
47
|
-
"node": ">=22.
|
|
48
|
-
},
|
|
49
|
-
"scripts": {
|
|
50
|
-
"build": "tsc -p tsconfig.build.json",
|
|
51
|
-
"test": "vitest run",
|
|
52
|
-
"prepare": "npm run build"
|
|
53
|
-
},
|
|
54
|
-
"peerDependencies": {
|
|
55
|
-
"astro": "
|
|
56
|
-
},
|
|
57
|
-
"dependencies": {
|
|
58
|
-
"@observablehq/plot": "0.6.17",
|
|
59
|
-
"enarratio": "0.3
|
|
60
|
-
"linkedom": "0.18.
|
|
61
|
-
},
|
|
62
|
-
"publishConfig": {
|
|
63
|
-
"access": "public",
|
|
64
|
-
"tag": "latest"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "enarratio-astro",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Accessible charts for Astro: a Chart component and charts in Markdown, drawn on the server by Enarratio",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"astro",
|
|
7
|
+
"astro-component",
|
|
8
|
+
"withastro",
|
|
9
|
+
"charts",
|
|
10
|
+
"accessibility",
|
|
11
|
+
"markdown",
|
|
12
|
+
"enarratio"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/DrDustinEdwards/astro/tree/main/packages/enarratio-astro#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/DrDustinEdwards/astro/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/DrDustinEdwards/astro.git",
|
|
21
|
+
"directory": "packages/enarratio-astro"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Dustin Edwards (https://dustinedwards.info)",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./markdown": {
|
|
33
|
+
"types": "./dist/markdown.d.ts",
|
|
34
|
+
"import": "./dist/markdown.js"
|
|
35
|
+
},
|
|
36
|
+
"./Chart.astro": "./src/Chart.astro",
|
|
37
|
+
"./Enhance.astro": "./src/Enhance.astro",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"src",
|
|
43
|
+
"CHANGELOG.md",
|
|
44
|
+
"README.md"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.22"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"prepare": "npm run build"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"astro": "7.4.4"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@observablehq/plot": "0.6.17",
|
|
59
|
+
"enarratio": "0.7.3",
|
|
60
|
+
"linkedom": "0.18.14"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public",
|
|
64
|
+
"tag": "latest"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/Chart.astro
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* A chart, drawn on the server when the page renders. The props are a ChartSpec (data, columns,
|
|
4
|
-
* text) plus `interactive` and the style options. The chart CSS is written once per page,
|
|
5
|
-
* whatever the number of charts.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* <Chart type="line" data={visits} x="month" y="visitors" series="site"
|
|
9
|
-
* alt="Monthly visitors to the site and the docs: both rise, the docs fastest." />
|
|
10
|
-
*/
|
|
11
|
-
import type { ChartSpec } from "./chart.ts";
|
|
12
|
-
import { renderChart } from "./chart.ts";
|
|
13
|
-
import Enhance from "./Enhance.astro";
|
|
14
|
-
import { chartMarkup } from "./figure.ts";
|
|
15
|
-
import { firstOnPage } from "./once.ts";
|
|
16
|
-
import { chartStyles, type StyleOptions } from "./styles.ts";
|
|
17
|
-
|
|
18
|
-
interface Props extends ChartSpec, StyleOptions {
|
|
19
|
-
/** Hover details, keyboard navigation and filtering, from Enarratio's small enhancement script. */
|
|
20
|
-
interactive?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const { interactive = false, styles, theme, stylesheetOptions, ...spec } = Astro.props;
|
|
24
|
-
const result = await renderChart(spec);
|
|
25
|
-
const css = firstOnPage(Astro.request) ? chartStyles({ styles, theme, stylesheetOptions }) : "";
|
|
26
|
-
if (!result.ok) console.warn(`[enarratio-astro] chart ${spec.id ?? ""} not drawn: ${result.error}`);
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
{css && <style is:inline set:html={css} />}
|
|
30
|
-
<Fragment set:html={chartMarkup(result, interactive)} />
|
|
31
|
-
{interactive && result.ok && <Enhance />}
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* A chart, drawn on the server when the page renders. The props are a ChartSpec (data, columns,
|
|
4
|
+
* text) plus `interactive` and the style options. The chart CSS is written once per page,
|
|
5
|
+
* whatever the number of charts.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <Chart type="line" data={visits} x="month" y="visitors" series="site"
|
|
9
|
+
* alt="Monthly visitors to the site and the docs: both rise, the docs fastest." />
|
|
10
|
+
*/
|
|
11
|
+
import type { ChartSpec } from "./chart.ts";
|
|
12
|
+
import { renderChart } from "./chart.ts";
|
|
13
|
+
import Enhance from "./Enhance.astro";
|
|
14
|
+
import { chartMarkup } from "./figure.ts";
|
|
15
|
+
import { firstOnPage } from "./once.ts";
|
|
16
|
+
import { chartStyles, type StyleOptions } from "./styles.ts";
|
|
17
|
+
|
|
18
|
+
interface Props extends ChartSpec, StyleOptions {
|
|
19
|
+
/** Hover details, keyboard navigation and filtering, from Enarratio's small enhancement script. */
|
|
20
|
+
interactive?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { interactive = false, styles, theme, stylesheetOptions, ...spec } = Astro.props;
|
|
24
|
+
const result = await renderChart(spec, { interactive });
|
|
25
|
+
const css = firstOnPage(Astro.request) ? chartStyles({ styles, theme, stylesheetOptions }) : "";
|
|
26
|
+
if (!result.ok) console.warn(`[enarratio-astro] chart ${spec.id ?? ""} not drawn: ${result.error}`);
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
{css && <style is:inline set:html={css} />}
|
|
30
|
+
<Fragment set:html={chartMarkup(result, interactive)} />
|
|
31
|
+
{interactive && result.ok && <Enhance />}
|
package/src/Enhance.astro
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Loads Enarratio's enhancement layer for the charts marked interactive. `<Chart interactive>`
|
|
4
|
-
* renders it, and a layout whose Markdown has interactive charts includes it once. Astro bundles
|
|
5
|
-
* the script and adds it only to pages that render this component, so other pages stay
|
|
6
|
-
* script-free.
|
|
7
|
-
*/
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
<script>
|
|
11
|
-
import { enhance } from "enarratio/enhance";
|
|
12
|
-
|
|
13
|
-
function run() {
|
|
14
|
-
for (const chart of document.querySelectorAll<HTMLElement>("[data-enarratio-interactive-chart]")) {
|
|
15
|
-
enhance(chart);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
run();
|
|
19
|
-
// Sites using Astro's client router swap pages without reloading; enhance() skips charts it has seen.
|
|
20
|
-
document.addEventListener("astro:page-load", run);
|
|
21
|
-
</script>
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Loads Enarratio's enhancement layer for the charts marked interactive. `<Chart interactive>`
|
|
4
|
+
* renders it, and a layout whose Markdown has interactive charts includes it once. Astro bundles
|
|
5
|
+
* the script and adds it only to pages that render this component, so other pages stay
|
|
6
|
+
* script-free.
|
|
7
|
+
*/
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
import { enhance } from "enarratio/enhance";
|
|
12
|
+
|
|
13
|
+
function run() {
|
|
14
|
+
for (const chart of document.querySelectorAll<HTMLElement>("[data-enarratio-interactive-chart]")) {
|
|
15
|
+
enhance(chart);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
run();
|
|
19
|
+
// Sites using Astro's client router swap pages without reloading; enhance() skips charts it has seen.
|
|
20
|
+
document.addEventListener("astro:page-load", run);
|
|
21
|
+
</script>
|
package/src/chart.ts
CHANGED
|
@@ -1,177 +1,209 @@
|
|
|
1
|
-
import { areaChart, barChart, lineChart } from "enarratio/lite";
|
|
2
|
-
import { type Cell, column, parseTable, type Table, tableFromRows } from "./data.js";
|
|
3
|
-
|
|
4
|
-
/** The chart types a {@link ChartSpec} can draw. */
|
|
5
|
-
export const CHART_TYPES = ["bar", "line", "area", "scatter", "heatmap"] as const;
|
|
6
|
-
export type ChartType = (typeof CHART_TYPES)[number];
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* One chart, as an author writes it: in a component's props, a Markdown `chart` block, or an
|
|
10
|
-
* EmDash Chart block. Every field is checked when the chart is drawn, so a spec can come from
|
|
11
|
-
* content as easily as from code.
|
|
12
|
-
*/
|
|
13
|
-
export interface ChartSpec {
|
|
14
|
-
/** Bar (default), line, area (stacked), scatter or heatmap. */
|
|
15
|
-
readonly type?: string | undefined;
|
|
16
|
-
/**
|
|
17
|
-
* The data: CSV, semicolon- or tab-separated text whose first row names the columns, or rows
|
|
18
|
-
* as objects (from JSON or a content collection) whose values are numbers, text, dates or empty.
|
|
19
|
-
*/
|
|
20
|
-
readonly data: string | readonly Readonly<Record<string, unknown>>[];
|
|
21
|
-
/** Required: what the chart shows and finds, in a sentence. Names the chart for screen readers. */
|
|
22
|
-
readonly alt?: string | undefined;
|
|
23
|
-
readonly title?: string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
readonly
|
|
26
|
-
/**
|
|
27
|
-
readonly
|
|
28
|
-
/**
|
|
29
|
-
readonly
|
|
30
|
-
/**
|
|
31
|
-
readonly
|
|
32
|
-
/**
|
|
33
|
-
readonly
|
|
34
|
-
|
|
35
|
-
readonly
|
|
36
|
-
/**
|
|
37
|
-
readonly
|
|
38
|
-
/** Bar
|
|
39
|
-
readonly
|
|
40
|
-
/**
|
|
41
|
-
readonly
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
-
|
|
45
|
-
readonly
|
|
46
|
-
/**
|
|
47
|
-
readonly
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
import { areaChart, barChart, lineChart } from "enarratio/lite";
|
|
2
|
+
import { type Cell, column, parseTable, type Table, tableFromRows } from "./data.js";
|
|
3
|
+
|
|
4
|
+
/** The chart types a {@link ChartSpec} can draw. */
|
|
5
|
+
export const CHART_TYPES = ["bar", "line", "area", "scatter", "heatmap"] as const;
|
|
6
|
+
export type ChartType = (typeof CHART_TYPES)[number];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One chart, as an author writes it: in a component's props, a Markdown `chart` block, or an
|
|
10
|
+
* EmDash Chart block. Every field is checked when the chart is drawn, so a spec can come from
|
|
11
|
+
* content as easily as from code.
|
|
12
|
+
*/
|
|
13
|
+
export interface ChartSpec {
|
|
14
|
+
/** Bar (default), line, area (stacked), scatter or heatmap. */
|
|
15
|
+
readonly type?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The data: CSV, semicolon- or tab-separated text whose first row names the columns, or rows
|
|
18
|
+
* as objects (from JSON or a content collection) whose values are numbers, text, dates or empty.
|
|
19
|
+
*/
|
|
20
|
+
readonly data: string | readonly Readonly<Record<string, unknown>>[];
|
|
21
|
+
/** Required: what the chart shows and finds, in a sentence. Names the chart for screen readers. */
|
|
22
|
+
readonly alt?: string | undefined;
|
|
23
|
+
readonly title?: string | undefined;
|
|
24
|
+
/** A line under the title saying what the chart shows or how to read it. */
|
|
25
|
+
readonly subtitle?: string | undefined;
|
|
26
|
+
/** Method or caveats, below the chart. */
|
|
27
|
+
readonly caption?: string | undefined;
|
|
28
|
+
/** Notes under the chart: what a measure means, what was left out. */
|
|
29
|
+
readonly notes?: string | undefined;
|
|
30
|
+
/** Where the data come from, in the source line beside the CSV download. */
|
|
31
|
+
readonly source?: string | undefined;
|
|
32
|
+
/** A link for the source (https). */
|
|
33
|
+
readonly sourceHref?: string | undefined;
|
|
34
|
+
/** Who made the chart, in the source line. */
|
|
35
|
+
readonly byline?: string | undefined;
|
|
36
|
+
/** The column along the x axis. Column names match without regard to case. */
|
|
37
|
+
readonly x?: string | undefined;
|
|
38
|
+
/** The value column. Bar charts count rows without one; for a heatmap, the row column. */
|
|
39
|
+
readonly y?: string | undefined;
|
|
40
|
+
/** A column that splits rows into series. */
|
|
41
|
+
readonly series?: string | undefined;
|
|
42
|
+
/** Heatmap: the cell value column. */
|
|
43
|
+
readonly value?: string | undefined;
|
|
44
|
+
readonly xLabel?: string | undefined;
|
|
45
|
+
readonly yLabel?: string | undefined;
|
|
46
|
+
/** Units added to the axis labels and table headers: "Weight (kg)". */
|
|
47
|
+
readonly xUnit?: string | undefined;
|
|
48
|
+
readonly yUnit?: string | undefined;
|
|
49
|
+
/** Bar: series stacked (default) or side by side ("grouped"). */
|
|
50
|
+
readonly layout?: string | undefined;
|
|
51
|
+
/** Bar: "vertical" (default) or "horizontal". */
|
|
52
|
+
readonly orientation?: string | undefined;
|
|
53
|
+
/** Line and scatter: "linear" (default) or "log" y axis. */
|
|
54
|
+
readonly scale?: string | undefined;
|
|
55
|
+
/** Scatter: a least-squares line with its 95% band. */
|
|
56
|
+
readonly regression?: boolean | undefined;
|
|
57
|
+
/** "details" (default): the data table behind a toggle; "visually-hidden": for screen readers only. */
|
|
58
|
+
readonly dataTable?: string | undefined;
|
|
59
|
+
/** The figure's id, reported as `chartId` in the enhancement layer's events. */
|
|
60
|
+
readonly id?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A drawn chart's HTML, or why it could not be drawn, in words an author can act on. */
|
|
64
|
+
export type ChartResult =
|
|
65
|
+
| { readonly ok: true; readonly html: string }
|
|
66
|
+
| { readonly ok: false; readonly error: string };
|
|
67
|
+
|
|
68
|
+
type Row = Readonly<Record<string, Cell>>;
|
|
69
|
+
|
|
70
|
+
function text(value: string | undefined): string | undefined {
|
|
71
|
+
const trimmed = value?.trim();
|
|
72
|
+
return trimmed ? trimmed : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Copies the rows with one column's numbers turned into text, for fields Enarratio reads as names. */
|
|
76
|
+
function asNames(table: Table, name: string): Row[] {
|
|
77
|
+
return table.rows.map((row) => {
|
|
78
|
+
const copy: Record<string, Cell> = Object.assign(Object.create(null), row);
|
|
79
|
+
const v = copy[name];
|
|
80
|
+
if (typeof v === "number") copy[name] = String(v);
|
|
81
|
+
return copy;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Checks that a column holds only numbers and empty cells. */
|
|
86
|
+
function numeric(table: Table, name: string): void {
|
|
87
|
+
const bad = table.rows.find((row) => typeof row[name] === "string");
|
|
88
|
+
if (bad) throw new Error(`The column "${name}" should hold numbers, but has "${bad[name]}".`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** How a chart is drawn, beside what it shows. */
|
|
92
|
+
export interface RenderOptions {
|
|
93
|
+
/**
|
|
94
|
+
* Whether the page enhances the chart (`<Chart interactive>`). An enhanced chart's figure holds
|
|
95
|
+
* the controls the enhancement layer adds, inert until it arrives, so the page does not move;
|
|
96
|
+
* a chart that is never enhanced leaves them out. Defaults to true, as Enarratio does.
|
|
97
|
+
*/
|
|
98
|
+
readonly interactive?: boolean | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The options every chart type shares: text, the data table, the id and the pending controls. */
|
|
102
|
+
function figure(spec: ChartSpec, options: RenderOptions) {
|
|
103
|
+
const alt = text(spec.alt);
|
|
104
|
+
if (!alt) throw new Error("Add a description (alt): one sentence on what the chart shows.");
|
|
105
|
+
const title = text(spec.title);
|
|
106
|
+
const source = text(spec.source);
|
|
107
|
+
const sourceHref = text(spec.sourceHref);
|
|
108
|
+
if (sourceHref && !source)
|
|
109
|
+
throw new Error("Name the source (source) that the link (sourceHref) goes to.");
|
|
110
|
+
const id = text(spec.id)?.replace(/[^A-Za-z0-9_-]/g, "");
|
|
111
|
+
const optional = Object.fromEntries(
|
|
112
|
+
(["subtitle", "caption", "notes", "byline", "xLabel", "yLabel", "xUnit", "yUnit"] as const)
|
|
113
|
+
.map((key) => [key, text(spec[key])])
|
|
114
|
+
.filter(([, value]) => value !== undefined),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
alt,
|
|
118
|
+
...(title ? { title } : {}),
|
|
119
|
+
...optional,
|
|
120
|
+
...(source ? { source: sourceHref ? { name: source, href: sourceHref } : source } : {}),
|
|
121
|
+
...(id ? { id } : {}),
|
|
122
|
+
dataTable:
|
|
123
|
+
spec.dataTable === "visually-hidden" ? ("visually-hidden" as const) : ("details" as const),
|
|
124
|
+
...(options.interactive === false ? { controls: false } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function draw(spec: ChartSpec, options: RenderOptions): Promise<string> {
|
|
129
|
+
const type = (text(spec.type) ?? "bar").toLowerCase();
|
|
130
|
+
if (!CHART_TYPES.includes(type as ChartType)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`There is no chart type "${spec.type}". Use one of: ${CHART_TYPES.join(", ")}.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const common = figure(spec, options);
|
|
136
|
+
const table = typeof spec.data === "string" ? parseTable(spec.data) : tableFromRows(spec.data);
|
|
137
|
+
const x = column(table, spec.x, "x axis");
|
|
138
|
+
const seriesName = text(spec.series) ? column(table, spec.series, "series") : undefined;
|
|
139
|
+
const data = seriesName ? asNames(table, seriesName) : [...table.rows];
|
|
140
|
+
const series = seriesName ? { series: seriesName } : {};
|
|
141
|
+
const log = spec.scale === "log" ? ({ yType: "log" } as const) : {};
|
|
142
|
+
|
|
143
|
+
// Enarratio types its field options by the row type; parsed rows are only known at run time,
|
|
144
|
+
// so the options are checked here and passed on as plain objects.
|
|
145
|
+
// biome-ignore lint/suspicious/noExplicitAny: see above
|
|
146
|
+
type Loose = any;
|
|
147
|
+
|
|
148
|
+
switch (type as ChartType) {
|
|
149
|
+
case "bar": {
|
|
150
|
+
const y = text(spec.y) ? column(table, spec.y, "values") : undefined;
|
|
151
|
+
if (y) numeric(table, y);
|
|
152
|
+
return barChart<Loose>({
|
|
153
|
+
...common,
|
|
154
|
+
data,
|
|
155
|
+
x,
|
|
156
|
+
...(y ? { y } : {}),
|
|
157
|
+
...series,
|
|
158
|
+
layout: spec.layout === "grouped" ? "grouped" : "stacked",
|
|
159
|
+
orientation: spec.orientation === "horizontal" ? "horizontal" : "vertical",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
case "line":
|
|
163
|
+
case "area": {
|
|
164
|
+
const y = column(table, spec.y, "values");
|
|
165
|
+
numeric(table, y);
|
|
166
|
+
const options = { ...common, data, x, y, ...series };
|
|
167
|
+
return type === "line" ? lineChart<Loose>({ ...options, ...log }) : areaChart<Loose>(options);
|
|
168
|
+
}
|
|
169
|
+
case "scatter": {
|
|
170
|
+
const y = column(table, spec.y, "values");
|
|
171
|
+
numeric(table, x);
|
|
172
|
+
numeric(table, y);
|
|
173
|
+
const { scatterPlot } = await import("enarratio/plot");
|
|
174
|
+
return scatterPlot<Loose>({
|
|
175
|
+
...common,
|
|
176
|
+
data,
|
|
177
|
+
x,
|
|
178
|
+
y,
|
|
179
|
+
...series,
|
|
180
|
+
...log,
|
|
181
|
+
regression: spec.regression === true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
case "heatmap": {
|
|
185
|
+
const y = column(table, spec.y, "rows");
|
|
186
|
+
const value = column(table, spec.value, "cell values");
|
|
187
|
+
numeric(table, value);
|
|
188
|
+
const { heatmap } = await import("enarratio/plot");
|
|
189
|
+
return heatmap<Loose>({ ...common, data, x, y, value });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Draws a chart. Bar, line and area charts come from `enarratio/lite`, which builds the SVG as a
|
|
196
|
+
* string with no DOM, the cheapest render on a Worker; scatter plots and heatmaps load
|
|
197
|
+
* `enarratio/plot` (Observable Plot and linkedom) only when a page has one. Never throws: a chart
|
|
198
|
+
* that cannot be drawn returns the reason instead, so one mistake cannot break a page.
|
|
199
|
+
*/
|
|
200
|
+
export async function renderChart(
|
|
201
|
+
spec: ChartSpec,
|
|
202
|
+
options: RenderOptions = {},
|
|
203
|
+
): Promise<ChartResult> {
|
|
204
|
+
try {
|
|
205
|
+
return { ok: true, html: await draw(spec, options) };
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
208
|
+
}
|
|
209
|
+
}
|