@misoto22/design 0.5.0 → 0.6.0
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/README.md +124 -0
- package/bin/misoto22-design.mjs +205 -0
- package/dist/agent/Accordion.md +41 -0
- package/dist/agent/Alert.md +42 -0
- package/dist/agent/AppShell.md +40 -0
- package/dist/agent/AreaChart.md +68 -0
- package/dist/agent/Article.md +41 -0
- package/dist/agent/Avatar.md +32 -0
- package/dist/agent/Badge.md +40 -0
- package/dist/agent/BarChart.md +65 -0
- package/dist/agent/BarList.md +40 -0
- package/dist/agent/BigNumber.md +45 -0
- package/dist/agent/BoxPlot.md +58 -0
- package/dist/agent/Breadcrumb.md +32 -0
- package/dist/agent/BulletChart.md +48 -0
- package/dist/agent/Button.md +56 -0
- package/dist/agent/Calendar.md +42 -0
- package/dist/agent/Card.md +49 -0
- package/dist/agent/Checkbox.md +35 -0
- package/dist/agent/Collapsible.md +46 -0
- package/dist/agent/Combobox.md +50 -0
- package/dist/agent/Command.md +93 -0
- package/dist/agent/ComposedChart.md +57 -0
- package/dist/agent/ContextMenu.md +59 -0
- package/dist/agent/DatePicker.md +75 -0
- package/dist/agent/Diagram.md +46 -0
- package/dist/agent/Dialog.md +52 -0
- package/dist/agent/DropdownMenu.md +70 -0
- package/dist/agent/EmptyState.md +37 -0
- package/dist/agent/ErrorState.md +41 -0
- package/dist/agent/Facet.md +82 -0
- package/dist/agent/Field.md +37 -0
- package/dist/agent/FigureBand.md +37 -0
- package/dist/agent/FloatingIconButton.md +38 -0
- package/dist/agent/FunnelChart.md +52 -0
- package/dist/agent/Heatmap.md +53 -0
- package/dist/agent/Histogram.md +63 -0
- package/dist/agent/Input.md +30 -0
- package/dist/agent/Kbd.md +29 -0
- package/dist/agent/LineChart.md +60 -0
- package/dist/agent/LinkArrow.md +29 -0
- package/dist/agent/NativeSelect.md +40 -0
- package/dist/agent/NavItem.md +35 -0
- package/dist/agent/Pagination.md +43 -0
- package/dist/agent/PieChart.md +55 -0
- package/dist/agent/Popover.md +57 -0
- package/dist/agent/Progress.md +33 -0
- package/dist/agent/RadarChart.md +54 -0
- package/dist/agent/RadialChart.md +57 -0
- package/dist/agent/RadioGroup.md +44 -0
- package/dist/agent/SankeyChart.md +61 -0
- package/dist/agent/ScatterChart.md +54 -0
- package/dist/agent/ScrollArea.md +40 -0
- package/dist/agent/SearchableMenu.md +46 -0
- package/dist/agent/Select.md +75 -0
- package/dist/agent/Separator.md +35 -0
- package/dist/agent/Sheet.md +57 -0
- package/dist/agent/Skeleton.md +73 -0
- package/dist/agent/Slider.md +39 -0
- package/dist/agent/Sparkline.md +45 -0
- package/dist/agent/Spinner.md +39 -0
- package/dist/agent/StatusDot.md +36 -0
- package/dist/agent/StatusPill.md +28 -0
- package/dist/agent/Steps.md +44 -0
- package/dist/agent/Switch.md +29 -0
- package/dist/agent/Table.md +85 -0
- package/dist/agent/Tabs.md +48 -0
- package/dist/agent/Tag.md +35 -0
- package/dist/agent/Textarea.md +26 -0
- package/dist/agent/Toast.md +26 -0
- package/dist/agent/ToggleGroup.md +44 -0
- package/dist/agent/Tooltip.md +51 -0
- package/dist/agent/TreemapChart.md +52 -0
- package/dist/agent/WaterfallChart.md +62 -0
- package/dist/agent/catalog.json +1791 -0
- package/dist/agent/index.md +131 -0
- package/dist/charts/AreaChart/AreaChart.d.ts +191 -0
- package/dist/charts/AreaChart/AreaChart.js +480 -0
- package/dist/charts/AreaChart/AreaChart.js.map +1 -0
- package/dist/charts/BarChart/BarChart.d.ts +179 -0
- package/dist/charts/BarChart/BarChart.js +591 -0
- package/dist/charts/BarChart/BarChart.js.map +1 -0
- package/dist/charts/BarList/BarList.d.ts +57 -0
- package/dist/charts/BarList/BarList.js +71 -0
- package/dist/charts/BarList/BarList.js.map +1 -0
- package/dist/charts/BigNumber/BigNumber.d.ts +56 -0
- package/dist/charts/BigNumber/BigNumber.js +31 -0
- package/dist/charts/BigNumber/BigNumber.js.map +1 -0
- package/dist/charts/BoxPlot/BoxPlot.d.ts +177 -0
- package/dist/charts/BoxPlot/BoxPlot.js +495 -0
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -0
- package/dist/charts/BulletChart/BulletChart.d.ts +100 -0
- package/dist/charts/BulletChart/BulletChart.js +162 -0
- package/dist/charts/BulletChart/BulletChart.js.map +1 -0
- package/dist/charts/ComposedChart/ComposedChart.d.ts +171 -0
- package/dist/charts/ComposedChart/ComposedChart.js +667 -0
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -0
- package/dist/charts/Facet/Facet.d.ts +240 -0
- package/dist/charts/Facet/Facet.js +133 -0
- package/dist/charts/Facet/Facet.js.map +1 -0
- package/dist/charts/Facet/panels.d.ts +187 -0
- package/dist/charts/Facet/panels.js +203 -0
- package/dist/charts/Facet/panels.js.map +1 -0
- package/dist/charts/FunnelChart/FunnelChart.d.ts +101 -0
- package/dist/charts/FunnelChart/FunnelChart.js +147 -0
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -0
- package/dist/charts/Heatmap/Heatmap.d.ts +78 -0
- package/dist/charts/Heatmap/Heatmap.js +108 -0
- package/dist/charts/Heatmap/Heatmap.js.map +1 -0
- package/dist/charts/Histogram/Histogram.d.ts +165 -0
- package/dist/charts/Histogram/Histogram.js +350 -0
- package/dist/charts/Histogram/Histogram.js.map +1 -0
- package/dist/charts/LineChart/LineChart.d.ts +167 -0
- package/dist/charts/LineChart/LineChart.js +486 -0
- package/dist/charts/LineChart/LineChart.js.map +1 -0
- package/dist/charts/PieChart/PieChart.d.ts +128 -0
- package/dist/charts/PieChart/PieChart.js +309 -0
- package/dist/charts/PieChart/PieChart.js.map +1 -0
- package/dist/charts/RadarChart/RadarChart.d.ts +123 -0
- package/dist/charts/RadarChart/RadarChart.js +299 -0
- package/dist/charts/RadarChart/RadarChart.js.map +1 -0
- package/dist/charts/RadialChart/RadialChart.d.ts +124 -0
- package/dist/charts/RadialChart/RadialChart.js +257 -0
- package/dist/charts/RadialChart/RadialChart.js.map +1 -0
- package/dist/charts/SankeyChart/SankeyChart.d.ts +137 -0
- package/dist/charts/SankeyChart/SankeyChart.js +501 -0
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -0
- package/dist/charts/ScatterChart/ScatterChart.d.ts +128 -0
- package/dist/charts/ScatterChart/ScatterChart.js +269 -0
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -0
- package/dist/charts/Sparkline/Sparkline.d.ts +53 -0
- package/dist/charts/Sparkline/Sparkline.js +93 -0
- package/dist/charts/Sparkline/Sparkline.js.map +1 -0
- package/dist/charts/TreemapChart/TreemapChart.d.ts +93 -0
- package/dist/charts/TreemapChart/TreemapChart.js +118 -0
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -0
- package/dist/charts/WaterfallChart/WaterfallChart.d.ts +164 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js +319 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -0
- package/dist/charts/index.d.ts +41 -0
- package/dist/charts/index.js +128 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/lib/annotations.d.ts +103 -0
- package/dist/charts/lib/annotations.js +99 -0
- package/dist/charts/lib/annotations.js.map +1 -0
- package/dist/charts/lib/axis.d.ts +18 -0
- package/dist/charts/lib/axis.js +21 -0
- package/dist/charts/lib/axis.js.map +1 -0
- package/dist/charts/lib/background.d.ts +33 -0
- package/dist/charts/lib/background.js +173 -0
- package/dist/charts/lib/background.js.map +1 -0
- package/dist/charts/lib/brush.d.ts +94 -0
- package/dist/charts/lib/brush.js +535 -0
- package/dist/charts/lib/brush.js.map +1 -0
- package/dist/charts/lib/chart.d.ts +94 -0
- package/dist/charts/lib/chart.js +134 -0
- package/dist/charts/lib/chart.js.map +1 -0
- package/dist/charts/lib/dot.d.ts +29 -0
- package/dist/charts/lib/dot.js +78 -0
- package/dist/charts/lib/dot.js.map +1 -0
- package/dist/charts/lib/empty.d.ts +31 -0
- package/dist/charts/lib/empty.js +13 -0
- package/dist/charts/lib/empty.js.map +1 -0
- package/dist/charts/lib/export.d.ts +103 -0
- package/dist/charts/lib/export.js +204 -0
- package/dist/charts/lib/export.js.map +1 -0
- package/dist/charts/lib/figure.d.ts +61 -0
- package/dist/charts/lib/figure.js +59 -0
- package/dist/charts/lib/figure.js.map +1 -0
- package/dist/charts/lib/format.d.ts +46 -0
- package/dist/charts/lib/format.js +56 -0
- package/dist/charts/lib/format.js.map +1 -0
- package/dist/charts/lib/legend.d.ts +45 -0
- package/dist/charts/lib/legend.js +117 -0
- package/dist/charts/lib/legend.js.map +1 -0
- package/dist/charts/lib/loading.d.ts +48 -0
- package/dist/charts/lib/loading.js +105 -0
- package/dist/charts/lib/loading.js.map +1 -0
- package/dist/charts/lib/paint.d.ts +154 -0
- package/dist/charts/lib/paint.js +551 -0
- package/dist/charts/lib/paint.js.map +1 -0
- package/dist/charts/lib/sonify-control.d.ts +163 -0
- package/dist/charts/lib/sonify-control.js +172 -0
- package/dist/charts/lib/sonify-control.js.map +1 -0
- package/dist/charts/lib/sonify.d.ts +248 -0
- package/dist/charts/lib/sonify.js +262 -0
- package/dist/charts/lib/sonify.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +143 -0
- package/dist/charts/lib/toolbar.js +156 -0
- package/dist/charts/lib/toolbar.js.map +1 -0
- package/dist/charts/lib/tooltip.d.ts +59 -0
- package/dist/charts/lib/tooltip.js +135 -0
- package/dist/charts/lib/tooltip.js.map +1 -0
- package/dist/charts/lib/values.d.ts +56 -0
- package/dist/charts/lib/values.js +71 -0
- package/dist/charts/lib/values.js.map +1 -0
- package/dist/charts/lib/zoom.d.ts +139 -0
- package/dist/charts/lib/zoom.js +314 -0
- package/dist/charts/lib/zoom.js.map +1 -0
- package/dist/semantic.css +19 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +50 -0
- package/dist/tokens.css +118 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.json +101 -0
- package/package.json +26 -4
- package/skills/misoto22-design/SKILL.md +171 -0
- package/skills/misoto22-design/evals/claims.json +95 -0
- package/skills/misoto22-design/rules/a11y.md +115 -0
- package/skills/misoto22-design/rules/composition.md +108 -0
- package/skills/misoto22-design/rules/forms.md +128 -0
- package/skills/misoto22-design/rules/naming.md +155 -0
- package/skills/misoto22-design/rules/tokens.md +131 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
2
|
+
const PAINTED_PROPERTIES = [
|
|
3
|
+
"display",
|
|
4
|
+
"visibility",
|
|
5
|
+
"opacity",
|
|
6
|
+
"color",
|
|
7
|
+
"fill",
|
|
8
|
+
"fill-opacity",
|
|
9
|
+
"fill-rule",
|
|
10
|
+
"stroke",
|
|
11
|
+
"stroke-opacity",
|
|
12
|
+
"stroke-width",
|
|
13
|
+
"stroke-linecap",
|
|
14
|
+
"stroke-linejoin",
|
|
15
|
+
"stroke-dasharray",
|
|
16
|
+
"stroke-dashoffset",
|
|
17
|
+
"stop-color",
|
|
18
|
+
"stop-opacity",
|
|
19
|
+
"flood-color",
|
|
20
|
+
"flood-opacity",
|
|
21
|
+
"mask",
|
|
22
|
+
"filter",
|
|
23
|
+
"clip-path",
|
|
24
|
+
"clip-rule",
|
|
25
|
+
"paint-order",
|
|
26
|
+
"mix-blend-mode",
|
|
27
|
+
"shape-rendering",
|
|
28
|
+
"text-anchor",
|
|
29
|
+
"dominant-baseline",
|
|
30
|
+
"font-family",
|
|
31
|
+
"font-size",
|
|
32
|
+
"font-style",
|
|
33
|
+
"font-weight",
|
|
34
|
+
"letter-spacing",
|
|
35
|
+
"text-transform"
|
|
36
|
+
];
|
|
37
|
+
async function chartToPng(element, options = {}) {
|
|
38
|
+
const { title, scale = 2, background, padding = 16 } = options;
|
|
39
|
+
const svg = findPlotSvg(element);
|
|
40
|
+
if (!svg) throw new Error("chartToPng: found no <svg> to export inside the element it was given");
|
|
41
|
+
const box = svg.getBoundingClientRect();
|
|
42
|
+
const width = Math.ceil(box.width);
|
|
43
|
+
const height = Math.ceil(box.height);
|
|
44
|
+
if (width === 0 || height === 0) {
|
|
45
|
+
throw new Error("chartToPng: the chart has no measured size yet \u2014 export it after it renders");
|
|
46
|
+
}
|
|
47
|
+
const clone = svg.cloneNode(true);
|
|
48
|
+
inlinePaint(svg, clone);
|
|
49
|
+
clone.setAttribute("width", String(width));
|
|
50
|
+
clone.setAttribute("height", String(height));
|
|
51
|
+
const titleBand = title ? 30 : 0;
|
|
52
|
+
const totalWidth = width + padding * 2;
|
|
53
|
+
const totalHeight = height + padding * 2 + titleBand;
|
|
54
|
+
const page = document.createElementNS(SVG_NS, "svg");
|
|
55
|
+
page.setAttribute("xmlns", SVG_NS);
|
|
56
|
+
page.setAttribute("width", String(totalWidth));
|
|
57
|
+
page.setAttribute("height", String(totalHeight));
|
|
58
|
+
page.setAttribute("viewBox", `0 0 ${totalWidth} ${totalHeight}`);
|
|
59
|
+
const plate = document.createElementNS(SVG_NS, "rect");
|
|
60
|
+
plate.setAttribute("width", String(totalWidth));
|
|
61
|
+
plate.setAttribute("height", String(totalHeight));
|
|
62
|
+
plate.setAttribute("fill", background ?? readToken(element, "--chart-surface", "#ffffff"));
|
|
63
|
+
page.append(plate);
|
|
64
|
+
if (title) {
|
|
65
|
+
const caption = document.createElementNS(SVG_NS, "text");
|
|
66
|
+
caption.setAttribute("x", String(padding));
|
|
67
|
+
caption.setAttribute("y", String(padding + 14));
|
|
68
|
+
caption.setAttribute("fill", readToken(element, "--ink", "#101010"));
|
|
69
|
+
caption.setAttribute("font-size", "13");
|
|
70
|
+
caption.setAttribute("font-weight", "500");
|
|
71
|
+
caption.setAttribute("font-family", getComputedStyle(element).fontFamily || "sans-serif");
|
|
72
|
+
caption.textContent = title;
|
|
73
|
+
page.append(caption);
|
|
74
|
+
}
|
|
75
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
76
|
+
group.setAttribute("transform", `translate(${padding}, ${padding + titleBand})`);
|
|
77
|
+
group.append(clone);
|
|
78
|
+
page.append(group);
|
|
79
|
+
const markup = new XMLSerializer().serializeToString(page);
|
|
80
|
+
const url = URL.createObjectURL(
|
|
81
|
+
new Blob([`<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
${markup}`], {
|
|
83
|
+
type: "image/svg+xml;charset=utf-8"
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
try {
|
|
87
|
+
const image = await loadImage(url);
|
|
88
|
+
const canvas = document.createElement("canvas");
|
|
89
|
+
canvas.width = Math.ceil(totalWidth * scale);
|
|
90
|
+
canvas.height = Math.ceil(totalHeight * scale);
|
|
91
|
+
const context = canvas.getContext("2d");
|
|
92
|
+
if (!context) throw new Error("chartToPng: this browser gave back no 2D canvas context");
|
|
93
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
94
|
+
return await canvasToBlob(canvas);
|
|
95
|
+
} finally {
|
|
96
|
+
URL.revokeObjectURL(url);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function findPlotSvg(element) {
|
|
100
|
+
if (element.tagName.toLowerCase() === "svg") return element;
|
|
101
|
+
const scope = element.querySelector('[data-slot="chart"]') ?? element;
|
|
102
|
+
return scope.querySelector("svg.recharts-surface") ?? scope.querySelector("svg");
|
|
103
|
+
}
|
|
104
|
+
function inlinePaint(live, clone) {
|
|
105
|
+
const computed = getComputedStyle(live);
|
|
106
|
+
let css = "";
|
|
107
|
+
for (const property of PAINTED_PROPERTIES) {
|
|
108
|
+
const value = computed.getPropertyValue(property);
|
|
109
|
+
if (value) css += `${property}:${value};`;
|
|
110
|
+
}
|
|
111
|
+
if (css) clone.setAttribute("style", css);
|
|
112
|
+
const children = live.children;
|
|
113
|
+
const clones = clone.children;
|
|
114
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
115
|
+
const liveChild = children[index];
|
|
116
|
+
const cloneChild = clones[index];
|
|
117
|
+
if (liveChild && cloneChild) inlinePaint(liveChild, cloneChild);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function readToken(element, name, fallback) {
|
|
121
|
+
const value = getComputedStyle(element).getPropertyValue(name).trim();
|
|
122
|
+
return value && !value.includes("var(") ? value : fallback;
|
|
123
|
+
}
|
|
124
|
+
function loadImage(url) {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const image = new Image();
|
|
127
|
+
image.addEventListener("load", () => resolve(image));
|
|
128
|
+
image.addEventListener(
|
|
129
|
+
"error",
|
|
130
|
+
() => reject(new Error("chartToPng: the browser refused to decode the serialised chart"))
|
|
131
|
+
);
|
|
132
|
+
image.src = url;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function canvasToBlob(canvas) {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
try {
|
|
138
|
+
canvas.toBlob((blob) => {
|
|
139
|
+
if (blob) resolve(blob);
|
|
140
|
+
else reject(new Error("chartToPng: the canvas produced no PNG data"));
|
|
141
|
+
}, "image/png");
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
reject(
|
|
144
|
+
new Error(
|
|
145
|
+
"chartToPng: the canvas is tainted, which means the chart contains a cross-origin image",
|
|
146
|
+
{ cause }
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const UTF8_BOM = "\uFEFF";
|
|
153
|
+
function chartToCsv(rows, columns) {
|
|
154
|
+
if (columns.length === 0) return UTF8_BOM;
|
|
155
|
+
const lines = [columns.map((column) => csvField(headerText(column))).join(",")];
|
|
156
|
+
for (const row of rows) {
|
|
157
|
+
lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(","));
|
|
158
|
+
}
|
|
159
|
+
return `${UTF8_BOM}${lines.join("\r\n")}\r
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
function csvField(value) {
|
|
163
|
+
if (!/[",\r\n]/.test(value)) return value;
|
|
164
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
165
|
+
}
|
|
166
|
+
function headerText(column) {
|
|
167
|
+
const { label } = column;
|
|
168
|
+
if (typeof label === "string") return label;
|
|
169
|
+
if (typeof label === "number") return String(label);
|
|
170
|
+
return column.key;
|
|
171
|
+
}
|
|
172
|
+
function cellText(value) {
|
|
173
|
+
if (value === null || value === void 0) return "";
|
|
174
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "";
|
|
175
|
+
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
|
176
|
+
if (value instanceof Date) return value.toISOString();
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
function downloadBlob(blob, filename) {
|
|
180
|
+
if (typeof document === "undefined") {
|
|
181
|
+
throw new Error("downloadBlob: needs a browser document \u2014 call it from an event handler");
|
|
182
|
+
}
|
|
183
|
+
const url = URL.createObjectURL(blob);
|
|
184
|
+
const anchor = document.createElement("a");
|
|
185
|
+
anchor.href = url;
|
|
186
|
+
anchor.download = filename;
|
|
187
|
+
anchor.rel = "noopener";
|
|
188
|
+
anchor.style.display = "none";
|
|
189
|
+
document.body.append(anchor);
|
|
190
|
+
anchor.click();
|
|
191
|
+
anchor.remove();
|
|
192
|
+
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
193
|
+
}
|
|
194
|
+
function exportFilename(title, extension) {
|
|
195
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
196
|
+
return `${slug || "chart"}.${extension}`;
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
chartToCsv,
|
|
200
|
+
chartToPng,
|
|
201
|
+
downloadBlob,
|
|
202
|
+
exportFilename
|
|
203
|
+
};
|
|
204
|
+
//# sourceMappingURL=export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/export.ts"],"sourcesContent":["import type { ChartColumn } from './figure'\n\n/**\n * Taking a chart off the page: a PNG of the plot, or a CSV of its rows.\n *\n * Nothing here touches the DOM at module scope. Every browser API — `document`,\n * `canvas`, `URL.createObjectURL` — is reached for inside a function body, so\n * this module is safe to import from a server component and only fails when a\n * consumer actually asks for a file on a server, which is a call that cannot\n * succeed anywhere.\n */\n\nconst SVG_NS = 'http://www.w3.org/2000/svg'\n\n/**\n * The properties copied from the live DOM onto the serialised clone.\n *\n * Paint, geometry-adjacent paint, and type. Layout is deliberately absent: an\n * SVG's geometry is in its attributes, which the clone already carries, and\n * copying computed `width`/`height`/`transform` onto every node would fight\n * them.\n */\nconst PAINTED_PROPERTIES = [\n 'display',\n 'visibility',\n 'opacity',\n 'color',\n 'fill',\n 'fill-opacity',\n 'fill-rule',\n 'stroke',\n 'stroke-opacity',\n 'stroke-width',\n 'stroke-linecap',\n 'stroke-linejoin',\n 'stroke-dasharray',\n 'stroke-dashoffset',\n 'stop-color',\n 'stop-opacity',\n 'flood-color',\n 'flood-opacity',\n 'mask',\n 'filter',\n 'clip-path',\n 'clip-rule',\n 'paint-order',\n 'mix-blend-mode',\n 'shape-rendering',\n 'text-anchor',\n 'dominant-baseline',\n 'font-family',\n 'font-size',\n 'font-style',\n 'font-weight',\n 'letter-spacing',\n 'text-transform',\n] as const\n\nexport interface ChartPngOptions {\n /**\n * Printed above the plot in the exported image. Pass the figure's title: a\n * PNG travels into a slide deck with none of the page's context around it,\n * and an unnamed plot in a deck is a plot nobody can cite.\n */\n title?: string\n /** Multiplier on the on-screen size. 2 is a retina-sharp export. */\n scale?: number\n /**\n * Painted behind the plot. Defaults to the resolved `--chart-surface`, which\n * is what makes a dark-mode export come out dark rather than transparent.\n */\n background?: string\n /** Space around the plot, in CSS pixels. */\n padding?: number\n}\n\n/**\n * The chart's `<svg>`, rasterised to a PNG blob.\n *\n * ## The problem this function exists to solve\n *\n * Serialising the `<svg>` and handing it to an `<img>` puts it in a document\n * of its own. That document has none of the page's stylesheets, so every\n * `var(--color-desktop-0)`, `var(--chart-fill)` and `var(--ink)` in the markup\n * resolves to nothing — and a paint that resolves to nothing is not a fallback\n * colour, it is an invisible mark. Serialise a chart naively and you get an\n * empty rectangle with axes on it.\n *\n * ## The solution\n *\n * Walk the live tree and the clone in step, and on every node write\n * `getComputedStyle`'s answer for the painted properties into an inline\n * `style`. Computed values have already had `var()` substituted by the engine,\n * so `fill: var(--color-desktop-0)` arrives as `fill: rgb(16, 16, 16)` — and,\n * because the walk reads the LIVE element, it reads it under whichever theme\n * is currently applied. Light and dark come out right for free, with no theme\n * argument anywhere in this file.\n *\n * `<stop>` elements are walked too, which is the half that is easy to miss: a\n * series is painted `fill=\"url(#…-colors-desktop)\"`, the url survives\n * serialisation because the gradient is inside the same `<svg>`, and it is the\n * gradient's STOPS that carry the custom properties.\n *\n * ## What this cannot capture, honestly\n *\n * - **Anything drawn in HTML.** The legend, the tooltip, the brush strip and\n * the hidden data table are DOM, not SVG. The PNG is the plot.\n * - **Web fonts.** The isolated document cannot fetch the page's `@font-face`\n * sources, so text is rasterised in whatever the resolved family stack finds\n * locally. Type metrics will differ from the screen.\n * - **Cross-origin images.** A chart with an external `<image>` in it taints\n * the canvas and `toBlob` throws; there is no way around it from script.\n * - **The exact rendering.** This is the browser's SVG rasteriser re-drawing\n * the markup, not a screenshot. Antialiasing, `backdrop-filter` and any\n * effect a page stylesheet applied from outside the `<svg>` are not in it.\n *\n * @example\n * const blob = await chartToPng(plotRef.current, { title: 'Visitors per month' })\n * downloadBlob(blob, 'visitors-per-month.png')\n */\nexport async function chartToPng(\n element: HTMLElement | SVGSVGElement,\n options: ChartPngOptions = {},\n): Promise<Blob> {\n const { title, scale = 2, background, padding = 16 } = options\n\n const svg = findPlotSvg(element)\n if (!svg) throw new Error('chartToPng: found no <svg> to export inside the element it was given')\n\n const box = svg.getBoundingClientRect()\n const width = Math.ceil(box.width)\n const height = Math.ceil(box.height)\n if (width === 0 || height === 0) {\n // A chart that has not been measured yet has no size to export. Silently\n // producing a 0×0 PNG would look like a broken download rather than a\n // too-early call.\n throw new Error('chartToPng: the chart has no measured size yet — export it after it renders')\n }\n\n const clone = svg.cloneNode(true) as SVGSVGElement\n inlinePaint(svg, clone)\n clone.setAttribute('width', String(width))\n clone.setAttribute('height', String(height))\n\n const titleBand = title ? 30 : 0\n const totalWidth = width + padding * 2\n const totalHeight = height + padding * 2 + titleBand\n\n const page = document.createElementNS(SVG_NS, 'svg')\n page.setAttribute('xmlns', SVG_NS)\n page.setAttribute('width', String(totalWidth))\n page.setAttribute('height', String(totalHeight))\n page.setAttribute('viewBox', `0 0 ${totalWidth} ${totalHeight}`)\n\n const plate = document.createElementNS(SVG_NS, 'rect')\n plate.setAttribute('width', String(totalWidth))\n plate.setAttribute('height', String(totalHeight))\n plate.setAttribute('fill', background ?? readToken(element, '--chart-surface', '#ffffff'))\n page.append(plate)\n\n if (title) {\n const caption = document.createElementNS(SVG_NS, 'text')\n caption.setAttribute('x', String(padding))\n caption.setAttribute('y', String(padding + 14))\n caption.setAttribute('fill', readToken(element, '--ink', '#101010'))\n caption.setAttribute('font-size', '13')\n caption.setAttribute('font-weight', '500')\n caption.setAttribute('font-family', getComputedStyle(element).fontFamily || 'sans-serif')\n caption.textContent = title\n page.append(caption)\n }\n\n const group = document.createElementNS(SVG_NS, 'g')\n group.setAttribute('transform', `translate(${padding}, ${padding + titleBand})`)\n group.append(clone)\n page.append(group)\n\n const markup = new XMLSerializer().serializeToString(page)\n const url = URL.createObjectURL(\n new Blob([`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n${markup}`], {\n type: 'image/svg+xml;charset=utf-8',\n }),\n )\n\n try {\n const image = await loadImage(url)\n const canvas = document.createElement('canvas')\n canvas.width = Math.ceil(totalWidth * scale)\n canvas.height = Math.ceil(totalHeight * scale)\n\n const context = canvas.getContext('2d')\n if (!context) throw new Error('chartToPng: this browser gave back no 2D canvas context')\n context.drawImage(image, 0, 0, canvas.width, canvas.height)\n\n return await canvasToBlob(canvas)\n } finally {\n URL.revokeObjectURL(url)\n }\n}\n\n/**\n * The plot's `<svg>`, not the first `<svg>` in the subtree.\n *\n * The distinction matters because the toolbar sits inside the same wrapper and\n * every icon in it is an `<svg>`. Narrowing to the measured chart box first is\n * what stops an export of a chart from being a picture of a magnifying glass.\n */\nfunction findPlotSvg(element: HTMLElement | SVGSVGElement): SVGSVGElement | null {\n if (element.tagName.toLowerCase() === 'svg') return element as SVGSVGElement\n const scope = element.querySelector('[data-slot=\"chart\"]') ?? element\n return scope.querySelector<SVGSVGElement>('svg.recharts-surface') ?? scope.querySelector('svg')\n}\n\n/** Copies the resolved paint of every node in `live` onto the matching node in `clone`. */\nfunction inlinePaint(live: Element, clone: Element): void {\n const computed = getComputedStyle(live)\n let css = ''\n for (const property of PAINTED_PROPERTIES) {\n const value = computed.getPropertyValue(property)\n if (value) css += `${property}:${value};`\n }\n if (css) clone.setAttribute('style', css)\n\n // The clone is a faithful deep copy, so child lists line up by index.\n const children = live.children\n const clones = clone.children\n for (let index = 0; index < children.length; index += 1) {\n const liveChild = children[index]\n const cloneChild = clones[index]\n if (liveChild && cloneChild) inlinePaint(liveChild, cloneChild)\n }\n}\n\n/**\n * A custom property, resolved against the live element.\n *\n * `var()` inside a custom property is substituted at computed-value time, so\n * `--chart-surface: var(--paper)` reads back as a colour. The guard is for the\n * case where it did not — an unregistered name, or a value the engine left as\n * a token stream — because writing `var(--paper)` into a serialised SVG paints\n * nothing at all.\n */\nfunction readToken(element: Element, name: string, fallback: string): string {\n const value = getComputedStyle(element).getPropertyValue(name).trim()\n return value && !value.includes('var(') ? value : fallback\n}\n\nfunction loadImage(url: string): Promise<HTMLImageElement> {\n return new Promise((resolve, reject) => {\n const image = new Image()\n image.addEventListener('load', () => resolve(image))\n image.addEventListener('error', () =>\n reject(new Error('chartToPng: the browser refused to decode the serialised chart')),\n )\n image.src = url\n })\n}\n\nfunction canvasToBlob(canvas: HTMLCanvasElement): Promise<Blob> {\n return new Promise((resolve, reject) => {\n try {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob)\n else reject(new Error('chartToPng: the canvas produced no PNG data'))\n }, 'image/png')\n } catch (cause) {\n // A cross-origin image anywhere in the plot taints the canvas and this\n // throws SecurityError. Nothing in script can untaint it, so the honest\n // move is to say which failure it was.\n reject(\n new Error(\n 'chartToPng: the canvas is tainted, which means the chart contains a cross-origin image',\n { cause },\n ),\n )\n }\n })\n}\n\n/**\n * A UTF-8 byte order mark.\n *\n * Excel does not sniff encodings. Handed a CSV with no BOM it decodes the file\n * with the machine's legacy code page, so \"Müller\", \"東京\" and \"€\" arrive as\n * mojibake on most Windows installs — and the reader's conclusion is that the\n * export is broken, not that the encoding was guessed. The BOM is the only\n * in-band signal Excel honours, and every other tool skips it silently.\n */\nconst UTF8_BOM = '\\uFEFF'\n\n/**\n * The chart's rows as RFC 4180 CSV.\n *\n * Quotes a field only when it needs quoting — a comma, a double quote, or a\n * line break in it — doubles any quote inside, and separates records with CRLF\n * as the RFC requires rather than with the `\\n` a Unix habit would reach for.\n *\n * Numbers are written unformatted on purpose. `1,234` would need quoting and\n * would then land in the spreadsheet as text, which is the one thing a numeric\n * export must not do; formatting belongs to the chart, not to the file.\n *\n * A column's `label` is a `ReactNode`, so only a string or a number can be a\n * header. Anything else falls back to the column's key, which is a worse\n * header than the label but a better one than an empty cell.\n *\n * @example\n * const csv = chartToCsv(data, [{ key: 'desktop', label: 'Desktop' }])\n * downloadBlob(new Blob([csv], { type: 'text/csv;charset=utf-8' }), 'visitors.csv')\n */\nexport function chartToCsv(rows: Record<string, unknown>[], columns: ChartColumn[]): string {\n if (columns.length === 0) return UTF8_BOM\n\n const lines = [columns.map((column) => csvField(headerText(column))).join(',')]\n\n for (const row of rows) {\n lines.push(columns.map((column) => csvField(cellText(row[column.key]))).join(','))\n }\n\n // The RFC makes the last CRLF optional; writing it matches what a\n // spreadsheet emits and keeps the file from ending mid-record in a terminal.\n return `${UTF8_BOM}${lines.join('\\r\\n')}\\r\\n`\n}\n\n/** One field, quoted only when the RFC says it has to be. */\nfunction csvField(value: string): string {\n if (!/[\",\\r\\n]/.test(value)) return value\n return `\"${value.replace(/\"/g, '\"\"')}\"`\n}\n\n/** A column's header text, falling back to its key when the label is markup. */\nfunction headerText(column: ChartColumn): string {\n const { label } = column\n if (typeof label === 'string') return label\n if (typeof label === 'number') return String(label)\n return column.key\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n return ''\n}\n\n/**\n * Hands a blob to the browser as a download.\n *\n * Throws rather than no-oping without a `document`: a download is something a\n * person just asked for, and a silent return would look to them like a click\n * that did nothing.\n */\nexport function downloadBlob(blob: Blob, filename: string): void {\n if (typeof document === 'undefined') {\n throw new Error('downloadBlob: needs a browser document — call it from an event handler')\n }\n\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = filename\n anchor.rel = 'noopener'\n anchor.style.display = 'none'\n document.body.append(anchor)\n anchor.click()\n anchor.remove()\n\n // Revoking in the same tick cancels the download in Safari; one turn of the\n // event loop is enough for the navigation to have started.\n setTimeout(() => URL.revokeObjectURL(url), 0)\n}\n\n/**\n * A figure's title as a filename.\n *\n * Lowercase, ASCII-safe and hyphenated, because a downloaded file crosses into\n * shells, zip archives and Windows paths where a title's spaces and slashes\n * are someone else's problem.\n */\nexport function exportFilename(title: string, extension: string): string {\n const slug = title\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return `${slug || 'chart'}.${extension}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface ChartColumn {
|
|
5
|
+
/** The row field this column reads. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** What the column is called in the table header. */
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
interface ChartDataTableProps {
|
|
11
|
+
caption: string;
|
|
12
|
+
rows: Record<string, unknown>[];
|
|
13
|
+
columns: ChartColumn[];
|
|
14
|
+
/** The field naming each row — the category, the date, the bucket. */
|
|
15
|
+
rowKey?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The chart's data, as a table, for a reader who cannot see the chart.
|
|
19
|
+
*
|
|
20
|
+
* An SVG plot is a picture: `accessibilityLayer` gives Recharts' own keyboard
|
|
21
|
+
* cursor and announces one point at a time, which is navigation rather than
|
|
22
|
+
* access to the figures. This is the table view — visually hidden, in the
|
|
23
|
+
* accessibility tree, and generated from the same rows the marks are drawn
|
|
24
|
+
* from, so the two cannot disagree.
|
|
25
|
+
*
|
|
26
|
+
* `sr-only` rather than `hidden`: `display: none` removes it from the
|
|
27
|
+
* accessibility tree as well as from the page, which would defeat the purpose.
|
|
28
|
+
*/
|
|
29
|
+
declare function ChartDataTable({ caption, rows, columns, rowKey }: ChartDataTableProps): react.JSX.Element | null;
|
|
30
|
+
interface ChartFigureProps {
|
|
31
|
+
/**
|
|
32
|
+
* What the chart shows, in a sentence a reader could act on — "Visitors per
|
|
33
|
+
* month, 2026" rather than "Chart".
|
|
34
|
+
*
|
|
35
|
+
* Required, and required for the same reason `Table.caption` and
|
|
36
|
+
* `Progress.label` are: a plot with no name announces nothing, and it is not
|
|
37
|
+
* a thing a call site can be trusted to remember. Hidden by default, because
|
|
38
|
+
* most call sites already print a heading above the chart; `showTitle` puts
|
|
39
|
+
* it back when they do not.
|
|
40
|
+
*/
|
|
41
|
+
title: string;
|
|
42
|
+
/** Prints the title above the plot instead of hiding it from sight. */
|
|
43
|
+
showTitle?: boolean;
|
|
44
|
+
/** A line under the title — the unit, the window, the caveat. */
|
|
45
|
+
description?: ReactNode;
|
|
46
|
+
className?: string;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
/** The table view. Omit and it is generated; `false` opts out. */
|
|
49
|
+
table?: Omit<ChartDataTableProps, 'caption'> | false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The figure a chart is rendered inside: its name, its plot, and its data as a
|
|
53
|
+
* table.
|
|
54
|
+
*
|
|
55
|
+
* Every chart root in the package wraps itself in this, so "the chart is
|
|
56
|
+
* announced and its numbers are reachable" is a property of the system rather
|
|
57
|
+
* than something each call site has to remember to add.
|
|
58
|
+
*/
|
|
59
|
+
declare function ChartFigure({ title, showTitle, description, className, children, table, }: ChartFigureProps): react.JSX.Element;
|
|
60
|
+
|
|
61
|
+
export { type ChartColumn, ChartDataTable, type ChartDataTableProps, ChartFigure, type ChartFigureProps };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { cn } from "../../lib/cn.js";
|
|
4
|
+
function ChartDataTable({ caption, rows, columns, rowKey }) {
|
|
5
|
+
if (rows.length === 0 || columns.length === 0) return null;
|
|
6
|
+
return /* @__PURE__ */ jsxs("table", { className: "sr-only", children: [
|
|
7
|
+
/* @__PURE__ */ jsx("caption", { children: caption }),
|
|
8
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
|
|
9
|
+
rowKey && /* @__PURE__ */ jsx("th", { scope: "col", children: rowKey }),
|
|
10
|
+
columns.map((column) => /* @__PURE__ */ jsx("th", { scope: "col", children: column.label }, column.key))
|
|
11
|
+
] }) }),
|
|
12
|
+
/* @__PURE__ */ jsx("tbody", { children: rows.map((row, index) => /* @__PURE__ */ jsxs("tr", { children: [
|
|
13
|
+
rowKey && /* @__PURE__ */ jsx("th", { scope: "row", children: cellText(row[rowKey]) || String(index + 1) }),
|
|
14
|
+
columns.map((column) => /* @__PURE__ */ jsx("td", { children: cellText(row[column.key]) }, column.key))
|
|
15
|
+
] }, index)) })
|
|
16
|
+
] });
|
|
17
|
+
}
|
|
18
|
+
function cellText(value) {
|
|
19
|
+
if (value === null || value === void 0) return "";
|
|
20
|
+
if (typeof value === "number") return value.toLocaleString();
|
|
21
|
+
if (typeof value === "string" || typeof value === "boolean") return String(value);
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
function ChartFigure({
|
|
25
|
+
title,
|
|
26
|
+
showTitle = false,
|
|
27
|
+
description,
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
table
|
|
31
|
+
}) {
|
|
32
|
+
const captionId = `chart-title-${useId().replace(/:/g, "")}`;
|
|
33
|
+
return (
|
|
34
|
+
// Named explicitly rather than relying on the caption. A `<figcaption>` IS
|
|
35
|
+
// the figure's accessible name per the spec, but support for deriving it is
|
|
36
|
+
// uneven across assistive technology — and a chart whose name resolves in
|
|
37
|
+
// only some screen readers is a chart with no name.
|
|
38
|
+
/* @__PURE__ */ jsxs("figure", { "aria-labelledby": captionId, className: cn("flex min-h-0 w-full flex-col", className), children: [
|
|
39
|
+
/* @__PURE__ */ jsxs(
|
|
40
|
+
"figcaption",
|
|
41
|
+
{
|
|
42
|
+
id: captionId,
|
|
43
|
+
className: cn(!showTitle && "sr-only", showTitle && "mb-3 flex flex-col gap-1"),
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: cn(showTitle && "font-heading text-[length:var(--fs-item)] text-(--ink)"), children: title }),
|
|
46
|
+
description && /* @__PURE__ */ jsx("span", { className: cn(showTitle && "mono-meta text-(--ink-3-aa)"), children: description })
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
),
|
|
50
|
+
children,
|
|
51
|
+
table && /* @__PURE__ */ jsx(ChartDataTable, { caption: title, ...table })
|
|
52
|
+
] })
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
ChartDataTable,
|
|
57
|
+
ChartFigure
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=figure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/figure.tsx"],"sourcesContent":["import { useId, type ReactNode } from 'react'\nimport { cn } from '../../lib/cn'\n\nexport interface ChartColumn {\n /** The row field this column reads. */\n key: string\n /** What the column is called in the table header. */\n label: ReactNode\n}\n\nexport interface ChartDataTableProps {\n caption: string\n rows: Record<string, unknown>[]\n columns: ChartColumn[]\n /** The field naming each row — the category, the date, the bucket. */\n rowKey?: string\n}\n\n/**\n * The chart's data, as a table, for a reader who cannot see the chart.\n *\n * An SVG plot is a picture: `accessibilityLayer` gives Recharts' own keyboard\n * cursor and announces one point at a time, which is navigation rather than\n * access to the figures. This is the table view — visually hidden, in the\n * accessibility tree, and generated from the same rows the marks are drawn\n * from, so the two cannot disagree.\n *\n * `sr-only` rather than `hidden`: `display: none` removes it from the\n * accessibility tree as well as from the page, which would defeat the purpose.\n */\nexport function ChartDataTable({ caption, rows, columns, rowKey }: ChartDataTableProps) {\n if (rows.length === 0 || columns.length === 0) return null\n\n return (\n <table className=\"sr-only\">\n <caption>{caption}</caption>\n <thead>\n <tr>\n {rowKey && <th scope=\"col\">{rowKey}</th>}\n {columns.map((column) => (\n <th key={column.key} scope=\"col\">\n {column.label}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr key={index}>\n {rowKey && <th scope=\"row\">{cellText(row[rowKey]) || String(index + 1)}</th>}\n {columns.map((column) => (\n <td key={column.key}>{cellText(row[column.key])}</td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n )\n}\n\n/** A cell value as text. Anything that is not a primitive reads as empty. */\nfunction cellText(value: unknown): string {\n if (value === null || value === undefined) return ''\n if (typeof value === 'number') return value.toLocaleString()\n if (typeof value === 'string' || typeof value === 'boolean') return String(value)\n return ''\n}\n\nexport interface ChartFigureProps {\n /**\n * What the chart shows, in a sentence a reader could act on — \"Visitors per\n * month, 2026\" rather than \"Chart\".\n *\n * Required, and required for the same reason `Table.caption` and\n * `Progress.label` are: a plot with no name announces nothing, and it is not\n * a thing a call site can be trusted to remember. Hidden by default, because\n * most call sites already print a heading above the chart; `showTitle` puts\n * it back when they do not.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n className?: string\n children: ReactNode\n /** The table view. Omit and it is generated; `false` opts out. */\n table?: Omit<ChartDataTableProps, 'caption'> | false\n}\n\n/**\n * The figure a chart is rendered inside: its name, its plot, and its data as a\n * table.\n *\n * Every chart root in the package wraps itself in this, so \"the chart is\n * announced and its numbers are reachable\" is a property of the system rather\n * than something each call site has to remember to add.\n */\nexport function ChartFigure({\n title,\n showTitle = false,\n description,\n className,\n children,\n table,\n}: ChartFigureProps) {\n const captionId = `chart-title-${useId().replace(/:/g, '')}`\n\n return (\n // Named explicitly rather than relying on the caption. A `<figcaption>` IS\n // the figure's accessible name per the spec, but support for deriving it is\n // uneven across assistive technology — and a chart whose name resolves in\n // only some screen readers is a chart with no name.\n <figure aria-labelledby={captionId} className={cn('flex min-h-0 w-full flex-col', className)}>\n <figcaption\n id={captionId}\n className={cn(!showTitle && 'sr-only', showTitle && 'mb-3 flex flex-col gap-1')}\n >\n <span className={cn(showTitle && 'font-heading text-[length:var(--fs-item)] text-(--ink)')}>{title}</span>\n {description && (\n <span className={cn(showTitle && 'mono-meta text-(--ink-3-aa)')}>{description}</span>\n )}\n </figcaption>\n {children}\n {table && <ChartDataTable caption={title} {...table} />}\n </figure>\n )\n}\n"],"mappings":"AAmCM,cAEE,YAFF;AAnCN,SAAS,aAA6B;AACtC,SAAS,UAAU;AA6BZ,SAAS,eAAe,EAAE,SAAS,MAAM,SAAS,OAAO,GAAwB;AACtF,MAAI,KAAK,WAAW,KAAK,QAAQ,WAAW,EAAG,QAAO;AAEtD,SACE,qBAAC,WAAM,WAAU,WACf;AAAA,wBAAC,aAAS,mBAAQ;AAAA,IAClB,oBAAC,WACC,+BAAC,QACE;AAAA,gBAAU,oBAAC,QAAG,OAAM,OAAO,kBAAO;AAAA,MAClC,QAAQ,IAAI,CAAC,WACZ,oBAAC,QAAoB,OAAM,OACxB,iBAAO,SADD,OAAO,GAEhB,CACD;AAAA,OACH,GACF;AAAA,IACA,oBAAC,WACE,eAAK,IAAI,CAAC,KAAK,UACd,qBAAC,QACE;AAAA,gBAAU,oBAAC,QAAG,OAAM,OAAO,mBAAS,IAAI,MAAM,CAAC,KAAK,OAAO,QAAQ,CAAC,GAAE;AAAA,MACtE,QAAQ,IAAI,CAAC,WACZ,oBAAC,QAAqB,mBAAS,IAAI,OAAO,GAAG,CAAC,KAArC,OAAO,GAAgC,CACjD;AAAA,SAJM,KAKT,CACD,GACH;AAAA,KACF;AAEJ;AAGA,SAAS,SAAS,OAAwB;AACxC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,eAAe;AAC3D,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAW,QAAO,OAAO,KAAK;AAChF,SAAO;AACT;AAgCO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqB;AACnB,QAAM,YAAY,eAAe,MAAM,EAAE,QAAQ,MAAM,EAAE,CAAC;AAE1D;AAAA;AAAA;AAAA;AAAA;AAAA,IAKE,qBAAC,YAAO,mBAAiB,WAAW,WAAW,GAAG,gCAAgC,SAAS,GACzF;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,IAAI;AAAA,UACJ,WAAW,GAAG,CAAC,aAAa,WAAW,aAAa,0BAA0B;AAAA,UAE9E;AAAA,gCAAC,UAAK,WAAW,GAAG,aAAa,wDAAwD,GAAI,iBAAM;AAAA,YAClG,eACC,oBAAC,UAAK,WAAW,GAAG,aAAa,6BAA6B,GAAI,uBAAY;AAAA;AAAA;AAAA,MAElF;AAAA,MACC;AAAA,MACA,SAAS,oBAAC,kBAAe,SAAS,OAAQ,GAAG,OAAO;AAAA,OACvD;AAAA;AAEJ;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a number is written on a chart.
|
|
3
|
+
*
|
|
4
|
+
* `compact` is the one an axis almost always wants and almost never gets:
|
|
5
|
+
* an axis reading 1,200,000 spends four characters saying "this is a big
|
|
6
|
+
* number" and pushes the plot inward to do it, where 1.2M says the same thing
|
|
7
|
+
* in four characters total. Every other option here exists because a bare
|
|
8
|
+
* `toLocaleString` cannot say what the number IS — a share, a price, a span.
|
|
9
|
+
*/
|
|
10
|
+
type NumberStyle = 'plain' | 'compact' | 'percent' | 'currency' | 'duration' | 'bytes';
|
|
11
|
+
interface NumberFormatOptions {
|
|
12
|
+
style?: NumberStyle;
|
|
13
|
+
/** BCP 47 tag. Omit and the reader's own locale decides. */
|
|
14
|
+
locale?: string;
|
|
15
|
+
/** ISO 4217, for `currency`. */
|
|
16
|
+
currency?: string;
|
|
17
|
+
/** Digits after the point. Defaults to what the style needs. */
|
|
18
|
+
fractionDigits?: number;
|
|
19
|
+
/** Written after the number — "req/s", "kB", "°C". Ignored by `currency`. */
|
|
20
|
+
unit?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A formatter for one axis, one tooltip, or one label.
|
|
24
|
+
*
|
|
25
|
+
* Returns a function rather than a string so it can be handed straight to
|
|
26
|
+
* Recharts' `tickFormatter`, which is called once per tick and must not
|
|
27
|
+
* rebuild an `Intl.NumberFormat` each time — constructing one is expensive
|
|
28
|
+
* enough that doing it per tick shows up on a chart with a brush.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* <BarChart.YAxis tickFormatter={formatNumber({ style: 'compact' })} />
|
|
32
|
+
* @example
|
|
33
|
+
* <LineChart.YAxis tickFormatter={formatNumber({ style: 'currency', currency: 'AUD' })} />
|
|
34
|
+
*/
|
|
35
|
+
declare function formatNumber(options?: NumberFormatOptions): (value: number) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The default a chart uses when nothing is said: compact above four digits,
|
|
38
|
+
* plain below it.
|
|
39
|
+
*
|
|
40
|
+
* The threshold is where an axis starts costing the plot real width. Below it,
|
|
41
|
+
* "9,400" is more precise and no wider than "9.4K"; above it, the compact form
|
|
42
|
+
* wins on both counts.
|
|
43
|
+
*/
|
|
44
|
+
declare const defaultTick: (value: number) => string;
|
|
45
|
+
|
|
46
|
+
export { type NumberFormatOptions, type NumberStyle, defaultTick, formatNumber };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const MINUTE = 60;
|
|
2
|
+
const HOUR = 3600;
|
|
3
|
+
const DAY = 86400;
|
|
4
|
+
const BYTE_STEPS = ["B", "kB", "MB", "GB", "TB", "PB"];
|
|
5
|
+
function duration(seconds) {
|
|
6
|
+
const sign = seconds < 0 ? "-" : "";
|
|
7
|
+
const total = Math.abs(seconds);
|
|
8
|
+
if (total < MINUTE) return `${sign}${Math.round(total)}s`;
|
|
9
|
+
if (total < HOUR) {
|
|
10
|
+
const minutes = Math.floor(total / MINUTE);
|
|
11
|
+
const rest = Math.round(total % MINUTE);
|
|
12
|
+
return rest === 0 ? `${sign}${minutes}m` : `${sign}${minutes}m ${rest}s`;
|
|
13
|
+
}
|
|
14
|
+
if (total < DAY) {
|
|
15
|
+
const hours2 = Math.floor(total / HOUR);
|
|
16
|
+
const minutes = Math.round(total % HOUR / MINUTE);
|
|
17
|
+
return minutes === 0 ? `${sign}${hours2}h` : `${sign}${hours2}h ${minutes}m`;
|
|
18
|
+
}
|
|
19
|
+
const days = Math.floor(total / DAY);
|
|
20
|
+
const hours = Math.round(total % DAY / HOUR);
|
|
21
|
+
return hours === 0 ? `${sign}${days}d` : `${sign}${days}d ${hours}h`;
|
|
22
|
+
}
|
|
23
|
+
function bytes(value, fractionDigits, locale) {
|
|
24
|
+
const sign = value < 0 ? "-" : "";
|
|
25
|
+
let size = Math.abs(value);
|
|
26
|
+
let step = 0;
|
|
27
|
+
while (size >= 1024 && step < BYTE_STEPS.length - 1) {
|
|
28
|
+
size /= 1024;
|
|
29
|
+
step += 1;
|
|
30
|
+
}
|
|
31
|
+
const digits = fractionDigits ?? (step === 0 || size >= 100 ? 0 : 1);
|
|
32
|
+
return `${sign}${size.toLocaleString(locale, { maximumFractionDigits: digits })} ${BYTE_STEPS[step]}`;
|
|
33
|
+
}
|
|
34
|
+
function formatNumber(options = {}) {
|
|
35
|
+
const { style = "plain", locale, currency = "USD", fractionDigits, unit } = options;
|
|
36
|
+
if (style === "duration") return (value) => duration(value);
|
|
37
|
+
if (style === "bytes") return (value) => bytes(value, fractionDigits, locale);
|
|
38
|
+
const intl = new Intl.NumberFormat(locale, {
|
|
39
|
+
notation: style === "compact" ? "compact" : "standard",
|
|
40
|
+
compactDisplay: "short",
|
|
41
|
+
style: style === "percent" ? "percent" : style === "currency" ? "currency" : "decimal",
|
|
42
|
+
currency: style === "currency" ? currency : void 0,
|
|
43
|
+
minimumFractionDigits: fractionDigits,
|
|
44
|
+
maximumFractionDigits: fractionDigits ?? (style === "compact" ? 1 : void 0)
|
|
45
|
+
});
|
|
46
|
+
if (!unit || style === "currency") return (value) => intl.format(value);
|
|
47
|
+
return (value) => `${intl.format(value)} ${unit}`;
|
|
48
|
+
}
|
|
49
|
+
const defaultTick = (value) => Math.abs(value) >= 1e4 ? COMPACT(value) : PLAIN(value);
|
|
50
|
+
const COMPACT = formatNumber({ style: "compact" });
|
|
51
|
+
const PLAIN = formatNumber({ style: "plain" });
|
|
52
|
+
export {
|
|
53
|
+
defaultTick,
|
|
54
|
+
formatNumber
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/lib/format.ts"],"sourcesContent":["/**\n * How a number is written on a chart.\n *\n * `compact` is the one an axis almost always wants and almost never gets:\n * an axis reading 1,200,000 spends four characters saying \"this is a big\n * number\" and pushes the plot inward to do it, where 1.2M says the same thing\n * in four characters total. Every other option here exists because a bare\n * `toLocaleString` cannot say what the number IS — a share, a price, a span.\n */\nexport type NumberStyle = 'plain' | 'compact' | 'percent' | 'currency' | 'duration' | 'bytes'\n\nexport interface NumberFormatOptions {\n style?: NumberStyle\n /** BCP 47 tag. Omit and the reader's own locale decides. */\n locale?: string\n /** ISO 4217, for `currency`. */\n currency?: string\n /** Digits after the point. Defaults to what the style needs. */\n fractionDigits?: number\n /** Written after the number — \"req/s\", \"kB\", \"°C\". Ignored by `currency`. */\n unit?: string\n}\n\nconst MINUTE = 60\nconst HOUR = 3600\nconst DAY = 86_400\n\n/** 1024-based steps, because a byte count is not a decimal quantity. */\nconst BYTE_STEPS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] as const\n\n/**\n * Seconds as a span a person reads — \"1h 12m\", not \"4320\".\n *\n * Two units at most on purpose: \"1h 12m 30s\" is three facts where the reader\n * asked for one, and the third is never the one that mattered.\n */\nfunction duration(seconds: number): string {\n const sign = seconds < 0 ? '-' : ''\n const total = Math.abs(seconds)\n\n if (total < MINUTE) return `${sign}${Math.round(total)}s`\n if (total < HOUR) {\n const minutes = Math.floor(total / MINUTE)\n const rest = Math.round(total % MINUTE)\n return rest === 0 ? `${sign}${minutes}m` : `${sign}${minutes}m ${rest}s`\n }\n if (total < DAY) {\n const hours = Math.floor(total / HOUR)\n const minutes = Math.round((total % HOUR) / MINUTE)\n return minutes === 0 ? `${sign}${hours}h` : `${sign}${hours}h ${minutes}m`\n }\n const days = Math.floor(total / DAY)\n const hours = Math.round((total % DAY) / HOUR)\n return hours === 0 ? `${sign}${days}d` : `${sign}${days}d ${hours}h`\n}\n\nfunction bytes(value: number, fractionDigits: number | undefined, locale?: string): string {\n const sign = value < 0 ? '-' : ''\n let size = Math.abs(value)\n let step = 0\n while (size >= 1024 && step < BYTE_STEPS.length - 1) {\n size /= 1024\n step += 1\n }\n const digits = fractionDigits ?? (step === 0 || size >= 100 ? 0 : 1)\n // A maximum without a minimum: 2048 bytes is \"2 kB\", not \"2.0 kB\". A\n // trailing zero on a size claims a precision the step conversion does not\n // have.\n return `${sign}${size.toLocaleString(locale, { maximumFractionDigits: digits })} ${BYTE_STEPS[step]}`\n}\n\n/**\n * A formatter for one axis, one tooltip, or one label.\n *\n * Returns a function rather than a string so it can be handed straight to\n * Recharts' `tickFormatter`, which is called once per tick and must not\n * rebuild an `Intl.NumberFormat` each time — constructing one is expensive\n * enough that doing it per tick shows up on a chart with a brush.\n *\n * @example\n * <BarChart.YAxis tickFormatter={formatNumber({ style: 'compact' })} />\n * @example\n * <LineChart.YAxis tickFormatter={formatNumber({ style: 'currency', currency: 'AUD' })} />\n */\nexport function formatNumber(options: NumberFormatOptions = {}): (value: number) => string {\n const { style = 'plain', locale, currency = 'USD', fractionDigits, unit } = options\n\n if (style === 'duration') return (value) => duration(value)\n if (style === 'bytes') return (value) => bytes(value, fractionDigits, locale)\n\n const intl = new Intl.NumberFormat(locale, {\n notation: style === 'compact' ? 'compact' : 'standard',\n compactDisplay: 'short',\n style: style === 'percent' ? 'percent' : style === 'currency' ? 'currency' : 'decimal',\n currency: style === 'currency' ? currency : undefined,\n minimumFractionDigits: fractionDigits,\n maximumFractionDigits: fractionDigits ?? (style === 'compact' ? 1 : undefined),\n })\n\n if (!unit || style === 'currency') return (value) => intl.format(value)\n // A plain space, not a thin one: U+2009 is invisible in a diff and the\n // linter flags it, and at 11px on an axis the two are indistinguishable.\n return (value) => `${intl.format(value)} ${unit}`\n}\n\n/**\n * The default a chart uses when nothing is said: compact above four digits,\n * plain below it.\n *\n * The threshold is where an axis starts costing the plot real width. Below it,\n * \"9,400\" is more precise and no wider than \"9.4K\"; above it, the compact form\n * wins on both counts.\n */\nexport const defaultTick = (value: number): string =>\n Math.abs(value) >= 10_000 ? COMPACT(value) : PLAIN(value)\n\nconst COMPACT = formatNumber({ style: 'compact' })\nconst PLAIN = formatNumber({ style: 'plain' })\n"],"mappings":"AAuBA,MAAM,SAAS;AACf,MAAM,OAAO;AACb,MAAM,MAAM;AAGZ,MAAM,aAAa,CAAC,KAAK,MAAM,MAAM,MAAM,MAAM,IAAI;AAQrD,SAAS,SAAS,SAAyB;AACzC,QAAM,OAAO,UAAU,IAAI,MAAM;AACjC,QAAM,QAAQ,KAAK,IAAI,OAAO;AAE9B,MAAI,QAAQ,OAAQ,QAAO,GAAG,IAAI,GAAG,KAAK,MAAM,KAAK,CAAC;AACtD,MAAI,QAAQ,MAAM;AAChB,UAAM,UAAU,KAAK,MAAM,QAAQ,MAAM;AACzC,UAAM,OAAO,KAAK,MAAM,QAAQ,MAAM;AACtC,WAAO,SAAS,IAAI,GAAG,IAAI,GAAG,OAAO,MAAM,GAAG,IAAI,GAAG,OAAO,KAAK,IAAI;AAAA,EACvE;AACA,MAAI,QAAQ,KAAK;AACf,UAAMA,SAAQ,KAAK,MAAM,QAAQ,IAAI;AACrC,UAAM,UAAU,KAAK,MAAO,QAAQ,OAAQ,MAAM;AAClD,WAAO,YAAY,IAAI,GAAG,IAAI,GAAGA,MAAK,MAAM,GAAG,IAAI,GAAGA,MAAK,KAAK,OAAO;AAAA,EACzE;AACA,QAAM,OAAO,KAAK,MAAM,QAAQ,GAAG;AACnC,QAAM,QAAQ,KAAK,MAAO,QAAQ,MAAO,IAAI;AAC7C,SAAO,UAAU,IAAI,GAAG,IAAI,GAAG,IAAI,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,KAAK;AACnE;AAEA,SAAS,MAAM,OAAe,gBAAoC,QAAyB;AACzF,QAAM,OAAO,QAAQ,IAAI,MAAM;AAC/B,MAAI,OAAO,KAAK,IAAI,KAAK;AACzB,MAAI,OAAO;AACX,SAAO,QAAQ,QAAQ,OAAO,WAAW,SAAS,GAAG;AACnD,YAAQ;AACR,YAAQ;AAAA,EACV;AACA,QAAM,SAAS,mBAAmB,SAAS,KAAK,QAAQ,MAAM,IAAI;AAIlE,SAAO,GAAG,IAAI,GAAG,KAAK,eAAe,QAAQ,EAAE,uBAAuB,OAAO,CAAC,CAAC,IAAI,WAAW,IAAI,CAAC;AACrG;AAeO,SAAS,aAAa,UAA+B,CAAC,GAA8B;AACzF,QAAM,EAAE,QAAQ,SAAS,QAAQ,WAAW,OAAO,gBAAgB,KAAK,IAAI;AAE5E,MAAI,UAAU,WAAY,QAAO,CAAC,UAAU,SAAS,KAAK;AAC1D,MAAI,UAAU,QAAS,QAAO,CAAC,UAAU,MAAM,OAAO,gBAAgB,MAAM;AAE5E,QAAM,OAAO,IAAI,KAAK,aAAa,QAAQ;AAAA,IACzC,UAAU,UAAU,YAAY,YAAY;AAAA,IAC5C,gBAAgB;AAAA,IAChB,OAAO,UAAU,YAAY,YAAY,UAAU,aAAa,aAAa;AAAA,IAC7E,UAAU,UAAU,aAAa,WAAW;AAAA,IAC5C,uBAAuB;AAAA,IACvB,uBAAuB,mBAAmB,UAAU,YAAY,IAAI;AAAA,EACtE,CAAC;AAED,MAAI,CAAC,QAAQ,UAAU,WAAY,QAAO,CAAC,UAAU,KAAK,OAAO,KAAK;AAGtE,SAAO,CAAC,UAAU,GAAG,KAAK,OAAO,KAAK,CAAC,IAAI,IAAI;AACjD;AAUO,MAAM,cAAc,CAAC,UAC1B,KAAK,IAAI,KAAK,KAAK,MAAS,QAAQ,KAAK,IAAI,MAAM,KAAK;AAE1D,MAAM,UAAU,aAAa,EAAE,OAAO,UAAU,CAAC;AACjD,MAAM,QAAQ,aAAa,EAAE,OAAO,QAAQ,CAAC;","names":["hours"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as recharts from 'recharts';
|
|
2
|
+
import { DefaultLegendContentProps } from 'recharts';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The shape of a legend's series mark.
|
|
7
|
+
*
|
|
8
|
+
* Seven, because the mark is doing more work here than in a chromatic system:
|
|
9
|
+
* with the monochrome ramp, a bar-shaped swatch beside a bar chart and a
|
|
10
|
+
* dot-shaped one beside a scatter is often the fastest way a reader ties the
|
|
11
|
+
* key to the plot.
|
|
12
|
+
*/
|
|
13
|
+
type ChartLegendVariant = 'square' | 'circle' | 'circle-outline' | 'rounded-square' | 'rounded-square-outline' | 'vertical-bar' | 'horizontal-bar';
|
|
14
|
+
/** Where the legend sits along the inline axis. Recharts' own vocabulary. */
|
|
15
|
+
type ChartLegendAlign = 'left' | 'center' | 'right';
|
|
16
|
+
interface ChartLegendContentProps extends DefaultLegendContentProps {
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Suppresses a config `icon`, falling back to the shape swatch. */
|
|
19
|
+
hideIcon?: boolean;
|
|
20
|
+
/** The row field holding a series name, when it is not the `dataKey`. */
|
|
21
|
+
nameKey?: string;
|
|
22
|
+
/** The chart's selected series, or null when the whole plot is lit. */
|
|
23
|
+
selected?: string | null;
|
|
24
|
+
/** Makes each entry a control that toggles its series. */
|
|
25
|
+
isClickable?: boolean;
|
|
26
|
+
onSelectChange?: (selected: string | null) => void;
|
|
27
|
+
variant?: ChartLegendVariant;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The key to a plot's series.
|
|
31
|
+
*
|
|
32
|
+
* Required above one series and not decorative: the monochrome ramp separates
|
|
33
|
+
* two series by a step of grey and a texture, and neither is self-describing.
|
|
34
|
+
* The chroma palette does not change that — three of its light steps sit below
|
|
35
|
+
* 3:1 on paper, and the legend is the documented relief.
|
|
36
|
+
*
|
|
37
|
+
* When `isClickable` is set each entry is a real `<button>` with `aria-pressed`
|
|
38
|
+
* rather than a `<div>` with a click handler. That is the difference between a
|
|
39
|
+
* filter a keyboard can reach and one it cannot, and it is why the entry is not
|
|
40
|
+
* simply styled to look pressable.
|
|
41
|
+
*/
|
|
42
|
+
declare function ChartLegendContent({ className, hideIcon, nameKey, payload, verticalAlign, align, selected, onSelectChange, isClickable, variant, }: ChartLegendContentProps): react.JSX.Element | null;
|
|
43
|
+
declare const ChartLegend: react.MemoExoticComponent<(outsideProps: recharts.LegendProps) => React.ReactPortal | null>;
|
|
44
|
+
|
|
45
|
+
export { ChartLegend, type ChartLegendAlign, ChartLegendContent, type ChartLegendContentProps, type ChartLegendVariant };
|