@karimsa/mdxserve 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +233 -2
- package/client/App.tsx +7 -0
- package/client/CodeBlock.tsx +395 -0
- package/client/CrossFade.tsx +72 -0
- package/client/DocContext.ts +14 -0
- package/client/DocView.tsx +107 -0
- package/client/ErrorBox.tsx +23 -0
- package/client/Heading.tsx +31 -0
- package/client/HomeEmptyState.tsx +101 -0
- package/client/HomeView.tsx +78 -0
- package/client/ListingView.tsx +663 -0
- package/client/MdSection.tsx +234 -0
- package/client/MdSectionEditor.tsx +233 -0
- package/client/Mermaid.tsx +435 -0
- package/client/RenderErrorBoundary.tsx +40 -0
- package/client/Table.tsx +14 -0
- package/client/TaskCheckbox.tsx +38 -0
- package/client/api.ts +138 -0
- package/client/app.css +372 -0
- package/client/builtins/Badge.tsx +109 -0
- package/client/builtins/Button.tsx +111 -0
- package/client/builtins/Callout.tsx +97 -0
- package/client/builtins/Card.tsx +111 -0
- package/client/builtins/Chart.tsx +875 -0
- package/client/builtins/Diff.tsx +722 -0
- package/client/builtins/Dropdown.tsx +417 -0
- package/client/builtins/FileTree.tsx +87 -0
- package/client/builtins/Kbd.tsx +18 -0
- package/client/builtins/Screenshot.tsx +209 -0
- package/client/builtins/Sparkline.tsx +63 -0
- package/client/builtins/Tabs.tsx +169 -0
- package/client/builtins/Tooltip.tsx +52 -0
- package/client/builtins/chart-data.ts +133 -0
- package/client/builtins/index.ts +167 -0
- package/client/design/base/editor.css +151 -0
- package/client/design/base/prose.css +143 -0
- package/client/design/base/reset.css +79 -0
- package/client/design/tokens/colors.css +188 -0
- package/client/design/tokens/elevation.css +42 -0
- package/client/design/tokens/fonts.css +6 -0
- package/client/design/tokens/motion.css +76 -0
- package/client/design/tokens/spacing.css +34 -0
- package/client/design/tokens/typography.css +56 -0
- package/client/doc-module-cache.ts +17 -0
- package/client/editor-link.ts +27 -0
- package/client/entry.tsx +51 -0
- package/client/export-doc.ts +80 -0
- package/client/export-save.ts +96 -0
- package/client/favicon.svg +1 -0
- package/client/file-system-access.d.ts +29 -0
- package/client/format.ts +17 -0
- package/client/hooks.ts +34 -0
- package/client/lucide-icons.d.ts +9 -0
- package/client/mdx-components-base.ts +32 -0
- package/client/mdx-components.ts +18 -0
- package/client/mermaid-chart.ts +109 -0
- package/client/mermaid-direction.ts +73 -0
- package/client/motion.ts +104 -0
- package/client/platform.ts +16 -0
- package/client/route-path.ts +15 -0
- package/client/router.ts +452 -0
- package/client/shell/AppShell.tsx +401 -0
- package/client/shell/Footer.tsx +33 -0
- package/client/shell/NotFoundView.tsx +22 -0
- package/client/shell/Sidebar.tsx +169 -0
- package/client/shell/StandaloneShell.tsx +65 -0
- package/client/shell/TocRail.tsx +53 -0
- package/client/shell/TopBar.tsx +117 -0
- package/client/shell/use-doc-width.ts +61 -0
- package/client/shell/useToc.ts +77 -0
- package/client/ssr-entry.tsx +22 -0
- package/client/standalone-entry.tsx +51 -0
- package/client/state.ts +90 -0
- package/client/theme.ts +65 -0
- package/client/ui/Breadcrumb.tsx +49 -0
- package/client/ui/ConfirmDeleteDialog.tsx +113 -0
- package/client/ui/ExpandModal.tsx +342 -0
- package/client/ui/Icon.tsx +114 -0
- package/client/ui/IconButton.tsx +63 -0
- package/client/ui/Kbd.tsx +17 -0
- package/client/ui/PageNav.tsx +77 -0
- package/client/ui/ResizeHandle.tsx +201 -0
- package/client/ui/SearchDialog.tsx +187 -0
- package/client/ui/Tag.tsx +44 -0
- package/client/ui/Toast.tsx +189 -0
- package/client/ui/TocList.tsx +71 -0
- package/client/ui/icon-set.ts +102 -0
- package/client/ui/toast-count.ts +28 -0
- package/dist/cli.js +5091 -0
- package/dist/registry.json +703 -0
- package/dist/render-worker.js +145 -0
- package/package.json +115 -5
- package/skills/mdxserve/SKILL.md +178 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Icon } from "../ui/Icon";
|
|
4
|
+
import { IconButton } from "../ui/IconButton";
|
|
5
|
+
import { ExpandModal } from "../ui/ExpandModal";
|
|
6
|
+
import Dropdown from "./Dropdown";
|
|
7
|
+
import {
|
|
8
|
+
binValues,
|
|
9
|
+
countTicks,
|
|
10
|
+
edgeTickIndices,
|
|
11
|
+
fitLabel,
|
|
12
|
+
formatBinEdge,
|
|
13
|
+
labelColumnWidth,
|
|
14
|
+
niceMax,
|
|
15
|
+
toNumber,
|
|
16
|
+
} from "./chart-data";
|
|
17
|
+
|
|
18
|
+
const SERIES_COLORS = [
|
|
19
|
+
"var(--chart-1)",
|
|
20
|
+
"var(--chart-2)",
|
|
21
|
+
"var(--chart-3)",
|
|
22
|
+
"var(--chart-4)",
|
|
23
|
+
"var(--chart-5)",
|
|
24
|
+
"var(--chart-6)",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export interface ChartDatum {
|
|
28
|
+
label: string;
|
|
29
|
+
[series: string]: string | number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A chart datum is `{ label, <series>: number }` with series keys chosen by
|
|
33
|
+
// the author (`series` below lists which keys to plot) — z.record covers the
|
|
34
|
+
// open-ended numeric keys while `label` stays required and typed.
|
|
35
|
+
const chartDatumSchema = z
|
|
36
|
+
.record(z.string(), z.union([z.string(), z.number()]))
|
|
37
|
+
.and(z.object({ label: z.string() }));
|
|
38
|
+
|
|
39
|
+
export const chartProps = z.object({
|
|
40
|
+
type: z
|
|
41
|
+
.enum(["bar", "line", "area", "histogram"])
|
|
42
|
+
.default("bar")
|
|
43
|
+
.describe("Chart shape. `histogram` bins `values` on the client and ignores `data`/`series`."),
|
|
44
|
+
orientation: z
|
|
45
|
+
.enum(["vertical", "horizontal"])
|
|
46
|
+
.default("vertical")
|
|
47
|
+
.describe(
|
|
48
|
+
"Bar layout direction; ignored for `line`/`area`/`histogram`. Readers can flip it from the chart header for their own view only.",
|
|
49
|
+
),
|
|
50
|
+
data: z
|
|
51
|
+
.array(chartDatumSchema)
|
|
52
|
+
.default([])
|
|
53
|
+
.describe("One object per x position: `{ label, <series>: number }`."),
|
|
54
|
+
series: z
|
|
55
|
+
.array(z.string())
|
|
56
|
+
.optional()
|
|
57
|
+
.describe('Keys of `data` entries to plot. Defaults to `["value"]`.'),
|
|
58
|
+
values: z
|
|
59
|
+
.array(z.number())
|
|
60
|
+
.default([])
|
|
61
|
+
.describe('Raw numbers to bin for `type="histogram"`; ignored otherwise.'),
|
|
62
|
+
bins: z
|
|
63
|
+
.number()
|
|
64
|
+
.int()
|
|
65
|
+
.positive()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe("Histogram bin count. Defaults to a Sturges estimate, capped at 20."),
|
|
68
|
+
height: z.number().default(220).describe("SVG height in px."),
|
|
69
|
+
filename: z.string().optional().describe("Header label; defaults to the chart type."),
|
|
70
|
+
caption: z.string().optional().describe("Caption shown below the chart on a hairline rule."),
|
|
71
|
+
xLabel: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe(
|
|
75
|
+
"Caption for the horizontal axis, rendered as uppercase mono — the categories on a vertical bar, line or area chart; the values on a horizontal bar chart or a histogram.",
|
|
76
|
+
),
|
|
77
|
+
yLabel: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe(
|
|
81
|
+
"Caption for the vertical axis, rendered as uppercase mono — the values on a vertical bar, line or area chart; the categories on a horizontal bar chart; the count on a histogram.",
|
|
82
|
+
),
|
|
83
|
+
unit: z
|
|
84
|
+
.string()
|
|
85
|
+
.optional()
|
|
86
|
+
.describe(
|
|
87
|
+
'Suffix appended to axis tick and tooltip values, e.g. "ms" or "%". On a histogram it labels the value axis (x); y is a count.',
|
|
88
|
+
),
|
|
89
|
+
legend: z
|
|
90
|
+
.boolean()
|
|
91
|
+
.default(true)
|
|
92
|
+
.describe("Show the series legend in the header when there's more than one series."),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type ChartProps = z.infer<typeof chartProps>;
|
|
96
|
+
export type Orientation = ChartProps["orientation"];
|
|
97
|
+
|
|
98
|
+
interface PlotProps {
|
|
99
|
+
type: ChartProps["type"];
|
|
100
|
+
data: ChartDatum[];
|
|
101
|
+
/** Series keys to plot, already defaulted. */
|
|
102
|
+
keys: string[];
|
|
103
|
+
unit?: string;
|
|
104
|
+
orientation: Orientation;
|
|
105
|
+
values: number[];
|
|
106
|
+
bins?: number;
|
|
107
|
+
/** Logical (viewBox) size. Inline charts fix the width at 640 and stretch to
|
|
108
|
+
the card; the expanded view passes the measured panel size so labels stay
|
|
109
|
+
at their native pixel size instead of scaling up with the viewBox. */
|
|
110
|
+
width: number;
|
|
111
|
+
height: number;
|
|
112
|
+
/** Roomier padding and type for the full-screen view. */
|
|
113
|
+
expanded?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Padding preset shared by every coordinate system; `HorizontalBars`
|
|
117
|
+
overrides `.left` with a label-driven width. */
|
|
118
|
+
function layout(expanded: boolean): { top: number; right: number; bottom: number; left: number } {
|
|
119
|
+
return expanded
|
|
120
|
+
? { top: 18, right: 16, bottom: 32, left: 52 }
|
|
121
|
+
: { top: 12, right: 12, bottom: 26, left: 38 };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface Pad {
|
|
125
|
+
top: number;
|
|
126
|
+
right: number;
|
|
127
|
+
bottom: number;
|
|
128
|
+
left: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Gridlines, mono tick labels and the `--chart-axis` baseline for a
|
|
132
|
+
numeric axis — `"y"` (vertical value axis, horizontal gridlines,
|
|
133
|
+
baseline along the bottom) for `VerticalPlot`/`HistogramBars`, `"x"`
|
|
134
|
+
(horizontal value axis, vertical gridlines, baseline on the left) for
|
|
135
|
+
`HorizontalBars`. */
|
|
136
|
+
function ValueGrid({
|
|
137
|
+
axis,
|
|
138
|
+
ticks,
|
|
139
|
+
scale,
|
|
140
|
+
pad,
|
|
141
|
+
plotWidth,
|
|
142
|
+
plotHeight,
|
|
143
|
+
format,
|
|
144
|
+
fontSize,
|
|
145
|
+
}: {
|
|
146
|
+
axis: "x" | "y";
|
|
147
|
+
ticks: number[];
|
|
148
|
+
scale: (tick: number) => number;
|
|
149
|
+
pad: Pad;
|
|
150
|
+
plotWidth: number;
|
|
151
|
+
plotHeight: number;
|
|
152
|
+
format: (value: number) => string;
|
|
153
|
+
fontSize: number;
|
|
154
|
+
}) {
|
|
155
|
+
const innerH = plotHeight - pad.top - pad.bottom;
|
|
156
|
+
const tickClass = "font-mono font-normal leading-[1.62] text-[length:var(--size-sm)]";
|
|
157
|
+
if (axis === "y") {
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
{ticks.map((tick) => (
|
|
161
|
+
<g key={tick}>
|
|
162
|
+
<line
|
|
163
|
+
x1={pad.left}
|
|
164
|
+
x2={plotWidth - pad.right}
|
|
165
|
+
y1={scale(tick)}
|
|
166
|
+
y2={scale(tick)}
|
|
167
|
+
stroke="var(--chart-grid)"
|
|
168
|
+
strokeWidth="1"
|
|
169
|
+
/>
|
|
170
|
+
<text
|
|
171
|
+
x={pad.left - 8}
|
|
172
|
+
y={scale(tick) + 3.5}
|
|
173
|
+
textAnchor="end"
|
|
174
|
+
className={tickClass}
|
|
175
|
+
style={{ fontSize, fill: "var(--chart-label)" }}
|
|
176
|
+
>
|
|
177
|
+
{format(tick)}
|
|
178
|
+
</text>
|
|
179
|
+
</g>
|
|
180
|
+
))}
|
|
181
|
+
<line
|
|
182
|
+
x1={pad.left}
|
|
183
|
+
x2={plotWidth - pad.right}
|
|
184
|
+
y1={pad.top + innerH}
|
|
185
|
+
y2={pad.top + innerH}
|
|
186
|
+
stroke="var(--chart-axis)"
|
|
187
|
+
strokeWidth="1"
|
|
188
|
+
/>
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return (
|
|
193
|
+
<>
|
|
194
|
+
{ticks.map((tick) => (
|
|
195
|
+
<g key={tick}>
|
|
196
|
+
<line
|
|
197
|
+
x1={scale(tick)}
|
|
198
|
+
x2={scale(tick)}
|
|
199
|
+
y1={pad.top}
|
|
200
|
+
y2={pad.top + innerH}
|
|
201
|
+
stroke="var(--chart-grid)"
|
|
202
|
+
strokeWidth="1"
|
|
203
|
+
/>
|
|
204
|
+
<text
|
|
205
|
+
x={scale(tick)}
|
|
206
|
+
y={plotHeight - 8}
|
|
207
|
+
textAnchor="middle"
|
|
208
|
+
className={tickClass}
|
|
209
|
+
style={{ fontSize, fill: "var(--chart-label)" }}
|
|
210
|
+
>
|
|
211
|
+
{format(tick)}
|
|
212
|
+
</text>
|
|
213
|
+
</g>
|
|
214
|
+
))}
|
|
215
|
+
<line
|
|
216
|
+
x1={pad.left}
|
|
217
|
+
x2={pad.left}
|
|
218
|
+
y1={pad.top}
|
|
219
|
+
y2={pad.top + innerH}
|
|
220
|
+
stroke="var(--chart-axis)"
|
|
221
|
+
strokeWidth="1"
|
|
222
|
+
/>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The hover value readout: bold mono text over the plot area. */
|
|
228
|
+
function HoverReadout({
|
|
229
|
+
x,
|
|
230
|
+
y,
|
|
231
|
+
textAnchor = "middle",
|
|
232
|
+
fontSize,
|
|
233
|
+
text,
|
|
234
|
+
}: {
|
|
235
|
+
x: number;
|
|
236
|
+
y: number;
|
|
237
|
+
textAnchor?: "start" | "middle" | "end";
|
|
238
|
+
fontSize: number;
|
|
239
|
+
text: string;
|
|
240
|
+
}) {
|
|
241
|
+
return (
|
|
242
|
+
<text
|
|
243
|
+
x={x}
|
|
244
|
+
y={y}
|
|
245
|
+
textAnchor={textAnchor}
|
|
246
|
+
className="font-mono leading-[1.62] text-[length:var(--size-sm)] font-semibold"
|
|
247
|
+
style={{ fontSize, fill: "var(--text-heading)" }}
|
|
248
|
+
>
|
|
249
|
+
{text}
|
|
250
|
+
</text>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
interface PlotBodyProps {
|
|
255
|
+
unit?: string;
|
|
256
|
+
width: number;
|
|
257
|
+
height: number;
|
|
258
|
+
expanded: boolean;
|
|
259
|
+
fontSize: number;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Vertical bar / line / area — today's `ChartPlot` body, unchanged apart
|
|
263
|
+
from `toNumber` and `countTicks`. */
|
|
264
|
+
function VerticalPlot({
|
|
265
|
+
type,
|
|
266
|
+
data,
|
|
267
|
+
keys,
|
|
268
|
+
unit,
|
|
269
|
+
width: plotWidth,
|
|
270
|
+
height: plotHeight,
|
|
271
|
+
expanded,
|
|
272
|
+
fontSize,
|
|
273
|
+
}: PlotBodyProps & { type: "bar" | "line" | "area"; data: ChartDatum[]; keys: string[] }) {
|
|
274
|
+
const [hover, setHover] = useState(-1);
|
|
275
|
+
const fmt = (value: number) => `${value}${unit ?? ""}`;
|
|
276
|
+
const pad = layout(expanded);
|
|
277
|
+
const innerW = plotWidth - pad.left - pad.right;
|
|
278
|
+
const innerH = plotHeight - pad.top - pad.bottom;
|
|
279
|
+
const max = niceMax(
|
|
280
|
+
Math.max(1, ...data.flatMap((datum) => keys.map((key) => toNumber(datum[key])))),
|
|
281
|
+
);
|
|
282
|
+
const ticks = countTicks(max);
|
|
283
|
+
const xAt = (index: number) => pad.left + (innerW / Math.max(1, data.length)) * (index + 0.5);
|
|
284
|
+
const yAt = (value: number) => pad.top + innerH - (value / max) * innerH;
|
|
285
|
+
const bandW = innerW / Math.max(1, data.length);
|
|
286
|
+
const barW = Math.min(expanded ? 72 : 34, (bandW * 0.62) / keys.length);
|
|
287
|
+
|
|
288
|
+
const linePath = (key: string) =>
|
|
289
|
+
data
|
|
290
|
+
.map(
|
|
291
|
+
(datum, index) =>
|
|
292
|
+
(index ? "L" : "M") + xAt(index).toFixed(1) + " " + yAt(toNumber(datum[key])).toFixed(1),
|
|
293
|
+
)
|
|
294
|
+
.join(" ");
|
|
295
|
+
const areaPath = (key: string) =>
|
|
296
|
+
linePath(key) +
|
|
297
|
+
" L" +
|
|
298
|
+
xAt(data.length - 1).toFixed(1) +
|
|
299
|
+
" " +
|
|
300
|
+
(pad.top + innerH) +
|
|
301
|
+
" L" +
|
|
302
|
+
xAt(0).toFixed(1) +
|
|
303
|
+
" " +
|
|
304
|
+
(pad.top + innerH) +
|
|
305
|
+
" Z";
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<>
|
|
309
|
+
<ValueGrid
|
|
310
|
+
axis="y"
|
|
311
|
+
ticks={ticks}
|
|
312
|
+
scale={yAt}
|
|
313
|
+
pad={pad}
|
|
314
|
+
plotWidth={plotWidth}
|
|
315
|
+
plotHeight={plotHeight}
|
|
316
|
+
format={fmt}
|
|
317
|
+
fontSize={fontSize}
|
|
318
|
+
/>
|
|
319
|
+
{type === "area"
|
|
320
|
+
? keys.map((key, seriesIndex) => (
|
|
321
|
+
<path
|
|
322
|
+
key={`a${key}`}
|
|
323
|
+
d={areaPath(key)}
|
|
324
|
+
fill={SERIES_COLORS[seriesIndex % 6]}
|
|
325
|
+
opacity="0.14"
|
|
326
|
+
/>
|
|
327
|
+
))
|
|
328
|
+
: null}
|
|
329
|
+
{type === "line" || type === "area"
|
|
330
|
+
? keys.map((key, seriesIndex) => (
|
|
331
|
+
<g key={`l${key}`}>
|
|
332
|
+
<path
|
|
333
|
+
d={linePath(key)}
|
|
334
|
+
fill="none"
|
|
335
|
+
stroke={SERIES_COLORS[seriesIndex % 6]}
|
|
336
|
+
strokeWidth="2"
|
|
337
|
+
strokeLinecap="round"
|
|
338
|
+
strokeLinejoin="round"
|
|
339
|
+
/>
|
|
340
|
+
{data.map((datum, index) => (
|
|
341
|
+
<circle
|
|
342
|
+
key={index}
|
|
343
|
+
cx={xAt(index)}
|
|
344
|
+
cy={yAt(toNumber(datum[key]))}
|
|
345
|
+
r={hover === index ? 4.5 : 3}
|
|
346
|
+
fill="var(--surface-card)"
|
|
347
|
+
stroke={SERIES_COLORS[seriesIndex % 6]}
|
|
348
|
+
strokeWidth="2"
|
|
349
|
+
/>
|
|
350
|
+
))}
|
|
351
|
+
</g>
|
|
352
|
+
))
|
|
353
|
+
: null}
|
|
354
|
+
{type === "bar"
|
|
355
|
+
? data.map((datum, index) => (
|
|
356
|
+
<g key={index}>
|
|
357
|
+
<rect
|
|
358
|
+
x={pad.left + bandW * index}
|
|
359
|
+
y={pad.top}
|
|
360
|
+
width={bandW}
|
|
361
|
+
height={innerH}
|
|
362
|
+
fill={hover === index ? "var(--surface-hover)" : "transparent"}
|
|
363
|
+
/>
|
|
364
|
+
{keys.map((key, seriesIndex) => {
|
|
365
|
+
const barX = xAt(index) - (barW * keys.length) / 2 + barW * seriesIndex;
|
|
366
|
+
const barH = Math.max(1, (toNumber(datum[key]) / max) * innerH);
|
|
367
|
+
return (
|
|
368
|
+
<rect
|
|
369
|
+
key={key}
|
|
370
|
+
x={barX}
|
|
371
|
+
y={pad.top + innerH - barH}
|
|
372
|
+
width={barW - 2}
|
|
373
|
+
height={barH}
|
|
374
|
+
rx="3"
|
|
375
|
+
fill={SERIES_COLORS[seriesIndex % 6]}
|
|
376
|
+
opacity={hover === -1 || hover === index ? 1 : 0.45}
|
|
377
|
+
/>
|
|
378
|
+
);
|
|
379
|
+
})}
|
|
380
|
+
</g>
|
|
381
|
+
))
|
|
382
|
+
: null}
|
|
383
|
+
{data.map((datum, index) => (
|
|
384
|
+
<text
|
|
385
|
+
key={`x${index}`}
|
|
386
|
+
x={xAt(index)}
|
|
387
|
+
y={plotHeight - 8}
|
|
388
|
+
textAnchor="middle"
|
|
389
|
+
className="font-sans font-medium leading-normal text-[length:var(--size-sm)]"
|
|
390
|
+
style={{ fontSize, fill: "var(--chart-label)" }}
|
|
391
|
+
>
|
|
392
|
+
{datum.label}
|
|
393
|
+
</text>
|
|
394
|
+
))}
|
|
395
|
+
{hover >= 0 && data[hover] ? (
|
|
396
|
+
<HoverReadout
|
|
397
|
+
x={xAt(hover)}
|
|
398
|
+
y={pad.top - 2}
|
|
399
|
+
fontSize={fontSize}
|
|
400
|
+
text={keys.map((key) => fmt(toNumber(data[hover][key]))).join(" · ")}
|
|
401
|
+
/>
|
|
402
|
+
) : null}
|
|
403
|
+
{/* One invisible hit column per x position, on top of every series, so
|
|
404
|
+
hover works the same for bars, lines and areas. */}
|
|
405
|
+
{data.map((_datum, index) => (
|
|
406
|
+
<rect
|
|
407
|
+
key={`hit${index}`}
|
|
408
|
+
x={pad.left + bandW * index}
|
|
409
|
+
y={pad.top}
|
|
410
|
+
width={bandW}
|
|
411
|
+
height={innerH}
|
|
412
|
+
fill="transparent"
|
|
413
|
+
onMouseEnter={() => setHover(index)}
|
|
414
|
+
onMouseLeave={() => setHover(-1)}
|
|
415
|
+
/>
|
|
416
|
+
))}
|
|
417
|
+
</>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Horizontal bars — the mirror image of `VerticalPlot`: the value axis
|
|
422
|
+
runs along x, categories stack along y, and the label column on the
|
|
423
|
+
left is sized to the longest category label. */
|
|
424
|
+
function HorizontalBars({
|
|
425
|
+
data,
|
|
426
|
+
keys,
|
|
427
|
+
unit,
|
|
428
|
+
width: plotWidth,
|
|
429
|
+
height: plotHeight,
|
|
430
|
+
expanded,
|
|
431
|
+
fontSize,
|
|
432
|
+
}: PlotBodyProps & { data: ChartDatum[]; keys: string[] }) {
|
|
433
|
+
const [hover, setHover] = useState(-1);
|
|
434
|
+
const fmt = (value: number) => `${value}${unit ?? ""}`;
|
|
435
|
+
const labels = data.map((datum) => datum.label);
|
|
436
|
+
const pad: Pad = {
|
|
437
|
+
...layout(expanded),
|
|
438
|
+
left: labelColumnWidth(labels, {
|
|
439
|
+
fontSize,
|
|
440
|
+
min: expanded ? 52 : 38,
|
|
441
|
+
max: Math.min(expanded ? 260 : 180, plotWidth * 0.4),
|
|
442
|
+
}),
|
|
443
|
+
};
|
|
444
|
+
const innerW = plotWidth - pad.left - pad.right;
|
|
445
|
+
const innerH = plotHeight - pad.top - pad.bottom;
|
|
446
|
+
const max = niceMax(
|
|
447
|
+
Math.max(1, ...data.flatMap((datum) => keys.map((key) => toNumber(datum[key])))),
|
|
448
|
+
);
|
|
449
|
+
const ticks = countTicks(max);
|
|
450
|
+
const yAt = (index: number) => pad.top + (innerH / Math.max(1, data.length)) * (index + 0.5);
|
|
451
|
+
const bandH = innerH / Math.max(1, data.length);
|
|
452
|
+
const barH = Math.min(expanded ? 72 : 34, (bandH * 0.62) / keys.length);
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<>
|
|
456
|
+
<ValueGrid
|
|
457
|
+
axis="x"
|
|
458
|
+
ticks={ticks}
|
|
459
|
+
scale={(tick) => pad.left + (tick / max) * innerW}
|
|
460
|
+
pad={pad}
|
|
461
|
+
plotWidth={plotWidth}
|
|
462
|
+
plotHeight={plotHeight}
|
|
463
|
+
format={fmt}
|
|
464
|
+
fontSize={fontSize}
|
|
465
|
+
/>
|
|
466
|
+
{data.map((datum, index) => (
|
|
467
|
+
<g key={index}>
|
|
468
|
+
<rect
|
|
469
|
+
x={pad.left}
|
|
470
|
+
y={pad.top + bandH * index}
|
|
471
|
+
width={innerW}
|
|
472
|
+
height={bandH}
|
|
473
|
+
fill={hover === index ? "var(--surface-hover)" : "transparent"}
|
|
474
|
+
/>
|
|
475
|
+
{keys.map((key, seriesIndex) => {
|
|
476
|
+
const barY = yAt(index) - (barH * keys.length) / 2 + barH * seriesIndex;
|
|
477
|
+
const barLength = Math.max(1, (toNumber(datum[key]) / max) * innerW);
|
|
478
|
+
return (
|
|
479
|
+
<rect
|
|
480
|
+
key={key}
|
|
481
|
+
x={pad.left}
|
|
482
|
+
y={barY}
|
|
483
|
+
width={barLength}
|
|
484
|
+
height={barH - 2}
|
|
485
|
+
rx="3"
|
|
486
|
+
fill={SERIES_COLORS[seriesIndex % 6]}
|
|
487
|
+
opacity={hover === -1 || hover === index ? 1 : 0.45}
|
|
488
|
+
/>
|
|
489
|
+
);
|
|
490
|
+
})}
|
|
491
|
+
<text
|
|
492
|
+
x={pad.left - 8}
|
|
493
|
+
y={yAt(index) + 3.5}
|
|
494
|
+
textAnchor="end"
|
|
495
|
+
className="font-sans font-medium leading-normal text-[length:var(--size-sm)]"
|
|
496
|
+
style={{ fontSize, fill: "var(--chart-label)" }}
|
|
497
|
+
>
|
|
498
|
+
{fitLabel(datum.label, pad.left - 8, fontSize)}
|
|
499
|
+
</text>
|
|
500
|
+
</g>
|
|
501
|
+
))}
|
|
502
|
+
{hover >= 0 && data[hover] ? (
|
|
503
|
+
<HoverReadout
|
|
504
|
+
x={plotWidth - 4}
|
|
505
|
+
y={pad.top - 4}
|
|
506
|
+
textAnchor="end"
|
|
507
|
+
fontSize={fontSize}
|
|
508
|
+
text={keys.map((key) => fmt(toNumber(data[hover][key]))).join(" · ")}
|
|
509
|
+
/>
|
|
510
|
+
) : null}
|
|
511
|
+
{data.map((_datum, index) => (
|
|
512
|
+
<rect
|
|
513
|
+
key={`hit${index}`}
|
|
514
|
+
x={pad.left}
|
|
515
|
+
y={pad.top + bandH * index}
|
|
516
|
+
width={innerW}
|
|
517
|
+
height={bandH}
|
|
518
|
+
fill="transparent"
|
|
519
|
+
onMouseEnter={() => setHover(index)}
|
|
520
|
+
onMouseLeave={() => setHover(-1)}
|
|
521
|
+
/>
|
|
522
|
+
))}
|
|
523
|
+
</>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** A histogram over raw `values`, binned on the client. Bars sit flush
|
|
528
|
+
against each other (a 2px seam, no band padding); the x axis labels bin
|
|
529
|
+
edges instead of categories. */
|
|
530
|
+
function HistogramBars({
|
|
531
|
+
values,
|
|
532
|
+
bins,
|
|
533
|
+
unit,
|
|
534
|
+
width: plotWidth,
|
|
535
|
+
height: plotHeight,
|
|
536
|
+
expanded,
|
|
537
|
+
fontSize,
|
|
538
|
+
}: PlotBodyProps & { values: number[]; bins?: number }) {
|
|
539
|
+
const [hover, setHover] = useState(-1);
|
|
540
|
+
const binList = useMemo(() => binValues(values, bins), [values, bins]);
|
|
541
|
+
const pad = layout(expanded);
|
|
542
|
+
const innerW = plotWidth - pad.left - pad.right;
|
|
543
|
+
const innerH = plotHeight - pad.top - pad.bottom;
|
|
544
|
+
const max = niceMax(Math.max(1, ...binList.map((bin) => bin.value)));
|
|
545
|
+
const ticks = countTicks(max);
|
|
546
|
+
const binW = innerW / Math.max(1, binList.length);
|
|
547
|
+
const edgeIndices = edgeTickIndices(binList.length + 1, expanded ? 12 : 8);
|
|
548
|
+
const edgeStep = binList[0] ? binList[0].end - binList[0].start : 1;
|
|
549
|
+
const edgeValue = (index: number) =>
|
|
550
|
+
index < binList.length ? binList[index]!.start : (binList[binList.length - 1]?.end ?? 0);
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
<>
|
|
554
|
+
<ValueGrid
|
|
555
|
+
axis="y"
|
|
556
|
+
ticks={ticks}
|
|
557
|
+
scale={(tick) => pad.top + innerH - (tick / max) * innerH}
|
|
558
|
+
pad={pad}
|
|
559
|
+
plotWidth={plotWidth}
|
|
560
|
+
plotHeight={plotHeight}
|
|
561
|
+
format={(value) => `${value}`}
|
|
562
|
+
fontSize={fontSize}
|
|
563
|
+
/>
|
|
564
|
+
{binList.map((bin, index) => {
|
|
565
|
+
const barHeight = bin.value > 0 ? Math.max(1, (bin.value / max) * innerH) : 0;
|
|
566
|
+
return (
|
|
567
|
+
<rect
|
|
568
|
+
key={index}
|
|
569
|
+
x={pad.left + binW * index}
|
|
570
|
+
y={pad.top + innerH - barHeight}
|
|
571
|
+
width={binW - 2}
|
|
572
|
+
height={barHeight}
|
|
573
|
+
fill="var(--chart-1)"
|
|
574
|
+
opacity={hover === -1 || hover === index ? 1 : 0.45}
|
|
575
|
+
/>
|
|
576
|
+
);
|
|
577
|
+
})}
|
|
578
|
+
{binList.length > 0
|
|
579
|
+
? edgeIndices.map((index) => (
|
|
580
|
+
<text
|
|
581
|
+
key={index}
|
|
582
|
+
x={pad.left + binW * index}
|
|
583
|
+
y={plotHeight - 8}
|
|
584
|
+
textAnchor={index === 0 ? "start" : index === binList.length ? "end" : "middle"}
|
|
585
|
+
className="font-mono font-normal leading-[1.62] text-[length:var(--size-sm)]"
|
|
586
|
+
style={{ fontSize, fill: "var(--chart-label)" }}
|
|
587
|
+
>
|
|
588
|
+
{formatBinEdge(edgeValue(index), edgeStep)}
|
|
589
|
+
{unit ?? ""}
|
|
590
|
+
</text>
|
|
591
|
+
))
|
|
592
|
+
: null}
|
|
593
|
+
{hover >= 0 && binList[hover] ? (
|
|
594
|
+
<HoverReadout
|
|
595
|
+
x={clampToPlot(pad.left + binW * (hover + 0.5), pad, plotWidth)}
|
|
596
|
+
y={pad.top - 2}
|
|
597
|
+
fontSize={fontSize}
|
|
598
|
+
text={`${binList[hover]!.label}${unit ?? ""} · ${binList[hover]!.value}`}
|
|
599
|
+
/>
|
|
600
|
+
) : null}
|
|
601
|
+
{binList.map((_bin, index) => (
|
|
602
|
+
<rect
|
|
603
|
+
key={`hit${index}`}
|
|
604
|
+
x={pad.left + binW * index}
|
|
605
|
+
y={pad.top}
|
|
606
|
+
width={binW}
|
|
607
|
+
height={innerH}
|
|
608
|
+
fill="transparent"
|
|
609
|
+
onMouseEnter={() => setHover(index)}
|
|
610
|
+
onMouseLeave={() => setHover(-1)}
|
|
611
|
+
/>
|
|
612
|
+
))}
|
|
613
|
+
</>
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Keeps the histogram hover readout inside the plot area so a bin near
|
|
618
|
+
either edge can't push the text past the card's border. */
|
|
619
|
+
function clampToPlot(x: number, pad: Pad, plotWidth: number): number {
|
|
620
|
+
return Math.min(plotWidth - pad.right - 4, Math.max(pad.left + 4, x));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** The frame: one `<svg>` shared by every chart shape, sized by `layout()`
|
|
624
|
+
and filled by whichever coordinate system `type`/`orientation` selects. */
|
|
625
|
+
function ChartPlot({
|
|
626
|
+
type,
|
|
627
|
+
data,
|
|
628
|
+
keys,
|
|
629
|
+
unit,
|
|
630
|
+
orientation,
|
|
631
|
+
values,
|
|
632
|
+
bins,
|
|
633
|
+
width: plotWidth,
|
|
634
|
+
height: plotHeight,
|
|
635
|
+
expanded = false,
|
|
636
|
+
}: PlotProps) {
|
|
637
|
+
const fontSize = expanded ? 12 : 10;
|
|
638
|
+
const body = { unit, width: plotWidth, height: plotHeight, expanded, fontSize };
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<svg
|
|
642
|
+
viewBox={`0 0 ${plotWidth} ${plotHeight}`}
|
|
643
|
+
width={expanded ? plotWidth : "100%"}
|
|
644
|
+
height={plotHeight}
|
|
645
|
+
role="img"
|
|
646
|
+
className="block overflow-visible"
|
|
647
|
+
>
|
|
648
|
+
{type === "histogram" ? (
|
|
649
|
+
<HistogramBars {...body} values={values} bins={bins} />
|
|
650
|
+
) : type === "bar" && orientation === "horizontal" ? (
|
|
651
|
+
<HorizontalBars {...body} data={data} keys={keys} />
|
|
652
|
+
) : (
|
|
653
|
+
<VerticalPlot {...body} type={type} data={data} keys={keys} />
|
|
654
|
+
)}
|
|
655
|
+
</svg>
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function Legend({ keys }: { keys: string[] }) {
|
|
660
|
+
return (
|
|
661
|
+
<span className="flex gap-3">
|
|
662
|
+
{keys.map((key, index) => (
|
|
663
|
+
<span
|
|
664
|
+
key={key}
|
|
665
|
+
className="inline-flex items-center gap-1.5 font-sans font-medium leading-normal text-[length:var(--size-xs)] text-text-muted"
|
|
666
|
+
>
|
|
667
|
+
<span
|
|
668
|
+
className="h-2 w-2 rounded-[2px]"
|
|
669
|
+
style={{ background: SERIES_COLORS[index % 6] }}
|
|
670
|
+
/>
|
|
671
|
+
{key}
|
|
672
|
+
</span>
|
|
673
|
+
))}
|
|
674
|
+
</span>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function AxisLabels({ xLabel, yLabel }: { xLabel?: string; yLabel?: string }) {
|
|
679
|
+
if (!xLabel && !yLabel) return null;
|
|
680
|
+
return (
|
|
681
|
+
<div className="mt-2 flex justify-between font-mono font-semibold leading-[1.2] text-[length:var(--size-2xs)] uppercase tracking-[var(--tracking-caps)] text-text-subtle">
|
|
682
|
+
<span>{yLabel}</span>
|
|
683
|
+
<span>{xLabel}</span>
|
|
684
|
+
</div>
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const ORIENTATION_ICON: Record<Orientation, string> = {
|
|
689
|
+
vertical: "chart-column",
|
|
690
|
+
horizontal: "chart-bar",
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
const ORIENTATION_LABEL: Record<Orientation, string> = {
|
|
694
|
+
vertical: "Vertical bars",
|
|
695
|
+
horizontal: "Horizontal bars",
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Flips a bar chart's orientation for the reader's own view, the same way
|
|
700
|
+
* `DirectionMenu` (`CodeBlock.tsx`) flips a flowchart's layout — the doc's
|
|
701
|
+
* `orientation` prop, and anything exported or copied, stays as written.
|
|
702
|
+
*/
|
|
703
|
+
function OrientationMenu({
|
|
704
|
+
value,
|
|
705
|
+
authored,
|
|
706
|
+
onChange,
|
|
707
|
+
}: {
|
|
708
|
+
value: Orientation;
|
|
709
|
+
authored: Orientation;
|
|
710
|
+
onChange: (orientation: Orientation) => void;
|
|
711
|
+
}) {
|
|
712
|
+
return (
|
|
713
|
+
<Dropdown
|
|
714
|
+
icon={ORIENTATION_ICON[value]}
|
|
715
|
+
size="sm"
|
|
716
|
+
label="Orientation"
|
|
717
|
+
value={value}
|
|
718
|
+
onChange={(next) => onChange(next as Orientation)}
|
|
719
|
+
options={(["vertical", "horizontal"] as const).map((orientation) => ({
|
|
720
|
+
value: orientation,
|
|
721
|
+
label: ORIENTATION_LABEL[orientation],
|
|
722
|
+
description: orientation === authored ? "As written" : undefined,
|
|
723
|
+
}))}
|
|
724
|
+
/>
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* The modal body: measures the space it's given and redraws the chart at that
|
|
730
|
+
* size, so the expanded view gains plot area rather than magnified type.
|
|
731
|
+
*/
|
|
732
|
+
function ExpandedChart({
|
|
733
|
+
plot,
|
|
734
|
+
keys,
|
|
735
|
+
showLegend,
|
|
736
|
+
xLabel,
|
|
737
|
+
yLabel,
|
|
738
|
+
caption,
|
|
739
|
+
}: {
|
|
740
|
+
plot: Omit<PlotProps, "width" | "height" | "expanded">;
|
|
741
|
+
keys: string[];
|
|
742
|
+
showLegend: boolean;
|
|
743
|
+
xLabel?: string;
|
|
744
|
+
yLabel?: string;
|
|
745
|
+
caption?: string;
|
|
746
|
+
}) {
|
|
747
|
+
const areaRef = useRef<HTMLDivElement>(null);
|
|
748
|
+
const [size, setSize] = useState<{ width: number; height: number } | null>(null);
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
const area = areaRef.current;
|
|
752
|
+
if (!area) return;
|
|
753
|
+
const observer = new ResizeObserver((entries) => {
|
|
754
|
+
const entry = entries[0];
|
|
755
|
+
if (!entry) return;
|
|
756
|
+
const { width, height } = entry.contentRect;
|
|
757
|
+
setSize({ width: Math.floor(width), height: Math.floor(height) });
|
|
758
|
+
});
|
|
759
|
+
observer.observe(area);
|
|
760
|
+
return () => observer.disconnect();
|
|
761
|
+
}, []);
|
|
762
|
+
|
|
763
|
+
return (
|
|
764
|
+
<div className="flex h-full flex-col bg-surface-card">
|
|
765
|
+
{showLegend ? (
|
|
766
|
+
<div className="flex shrink-0 justify-end px-6 pt-4">
|
|
767
|
+
<Legend keys={keys} />
|
|
768
|
+
</div>
|
|
769
|
+
) : null}
|
|
770
|
+
<div className="flex min-h-0 flex-1 flex-col px-6 pt-4 pb-5">
|
|
771
|
+
<div ref={areaRef} className="min-h-0 flex-1">
|
|
772
|
+
{size && size.width > 0 && size.height > 0 ? (
|
|
773
|
+
<ChartPlot {...plot} width={size.width} height={size.height} expanded />
|
|
774
|
+
) : null}
|
|
775
|
+
</div>
|
|
776
|
+
<AxisLabels xLabel={xLabel} yLabel={yLabel} />
|
|
777
|
+
</div>
|
|
778
|
+
{caption ? (
|
|
779
|
+
<div className="shrink-0 border-t border-border-subtle px-6 py-2.5 font-sans font-medium leading-normal text-[length:var(--size-xs)] text-text-subtle">
|
|
780
|
+
{caption}
|
|
781
|
+
</div>
|
|
782
|
+
) : null}
|
|
783
|
+
</div>
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export default function Chart({
|
|
788
|
+
type = "bar",
|
|
789
|
+
orientation = "vertical",
|
|
790
|
+
data = [],
|
|
791
|
+
series,
|
|
792
|
+
values = [],
|
|
793
|
+
bins,
|
|
794
|
+
height = 220,
|
|
795
|
+
filename,
|
|
796
|
+
caption,
|
|
797
|
+
xLabel,
|
|
798
|
+
yLabel,
|
|
799
|
+
unit,
|
|
800
|
+
legend = true,
|
|
801
|
+
}: ChartProps) {
|
|
802
|
+
const keys = series && series.length ? series : ["value"];
|
|
803
|
+
const [expanded, setExpanded] = useState(false);
|
|
804
|
+
const [chosenOrientation, setChosenOrientation] = useState<{
|
|
805
|
+
authored: Orientation;
|
|
806
|
+
value: Orientation;
|
|
807
|
+
} | null>(null);
|
|
808
|
+
const shownOrientation =
|
|
809
|
+
chosenOrientation !== null && chosenOrientation.authored === orientation
|
|
810
|
+
? chosenOrientation.value
|
|
811
|
+
: orientation;
|
|
812
|
+
const isHorizontalBar = type === "bar" && shownOrientation === "horizontal";
|
|
813
|
+
const showLegend = legend && keys.length > 1 && type !== "histogram";
|
|
814
|
+
const plot = { type, data, keys, unit, orientation: shownOrientation, values, bins };
|
|
815
|
+
const orientationMenu =
|
|
816
|
+
type === "bar" ? (
|
|
817
|
+
<OrientationMenu
|
|
818
|
+
value={shownOrientation}
|
|
819
|
+
authored={orientation}
|
|
820
|
+
onChange={(next) => setChosenOrientation({ authored: orientation, value: next })}
|
|
821
|
+
/>
|
|
822
|
+
) : null;
|
|
823
|
+
const hint =
|
|
824
|
+
type === "histogram"
|
|
825
|
+
? "Hover a bin for its range and count"
|
|
826
|
+
: isHorizontalBar
|
|
827
|
+
? "Hover a row for values"
|
|
828
|
+
: "Hover a column for values";
|
|
829
|
+
|
|
830
|
+
return (
|
|
831
|
+
<figure className="not-prose overflow-hidden rounded-lg border border-border-default bg-surface-card">
|
|
832
|
+
<div className="flex h-[38px] items-center gap-3 border-b border-border-subtle pr-2.5 pl-4">
|
|
833
|
+
<Icon name="chart-column" size={13} className="text-text-subtle" />
|
|
834
|
+
<span className="flex-1 font-mono font-normal leading-[1.62] text-[length:var(--size-xs)] text-text-subtle">
|
|
835
|
+
{filename || type}
|
|
836
|
+
</span>
|
|
837
|
+
{showLegend ? <Legend keys={keys} /> : null}
|
|
838
|
+
{orientationMenu}
|
|
839
|
+
<IconButton
|
|
840
|
+
icon="expand"
|
|
841
|
+
label="Expand chart"
|
|
842
|
+
size="sm"
|
|
843
|
+
className="-mr-2"
|
|
844
|
+
onClick={() => setExpanded(true)}
|
|
845
|
+
/>
|
|
846
|
+
</div>
|
|
847
|
+
<div className="p-4">
|
|
848
|
+
<ChartPlot {...plot} width={640} height={height} />
|
|
849
|
+
<AxisLabels xLabel={xLabel} yLabel={yLabel} />
|
|
850
|
+
</div>
|
|
851
|
+
{caption ? (
|
|
852
|
+
<figcaption className="border-t border-border-subtle px-4 py-2.5 font-sans font-medium leading-normal text-[length:var(--size-xs)] text-text-subtle">
|
|
853
|
+
{caption}
|
|
854
|
+
</figcaption>
|
|
855
|
+
) : null}
|
|
856
|
+
<ExpandModal
|
|
857
|
+
open={expanded}
|
|
858
|
+
onClose={() => setExpanded(false)}
|
|
859
|
+
icon="chart-column"
|
|
860
|
+
title={filename || "Chart"}
|
|
861
|
+
hint={hint}
|
|
862
|
+
actions={orientationMenu}
|
|
863
|
+
>
|
|
864
|
+
<ExpandedChart
|
|
865
|
+
plot={plot}
|
|
866
|
+
keys={keys}
|
|
867
|
+
showLegend={showLegend}
|
|
868
|
+
xLabel={xLabel}
|
|
869
|
+
yLabel={yLabel}
|
|
870
|
+
caption={caption}
|
|
871
|
+
/>
|
|
872
|
+
</ExpandModal>
|
|
873
|
+
</figure>
|
|
874
|
+
);
|
|
875
|
+
}
|