@siyahi/ui 0.1.9-next.29 → 0.1.9-next.30
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/DESIGN.md +79 -10
- package/agents/examples/avatar.tsx +15 -5
- package/agents/examples/chart-line-forecast.tsx +36 -0
- package/agents/examples/chart-line-quiet.tsx +114 -0
- package/agents/examples/chart-panels.tsx +94 -0
- package/agents/examples/code-block.tsx +5 -3
- package/agents/examples/field.tsx +108 -0
- package/agents/examples/formatters.tsx +14 -14
- package/agents/examples/input-states.tsx +12 -22
- package/agents/examples/overview-dashboard.tsx +6 -6
- package/agents/examples/stale-blocks.tsx +56 -0
- package/agents/examples/table.tsx +6 -6
- package/agents/manifest.json +104 -3
- package/dist/components/registry/charts/area-chart-card.d.ts +7 -3
- package/dist/components/registry/charts/area-chart-card.js +52 -25
- package/dist/components/registry/charts/bar-chart-card.d.ts +1 -0
- package/dist/components/registry/charts/bar-chart-card.js +8 -24
- package/dist/components/registry/charts/line-chart-card.d.ts +2 -2
- package/dist/components/registry/charts/line-chart-card.js +42 -7
- package/dist/components/registry/dashboard-01/section-cards.d.ts +2 -0
- package/dist/components/registry/dashboard-01/section-cards.js +3 -0
- package/dist/components/registry/heatmap/calendar-heatmap.js +2 -2
- package/dist/components/registry/heatmap/status-heatmap.js +2 -2
- package/dist/components/registry/heatmap/weekday-heatmap.js +2 -2
- package/dist/components/registry/niko-table/filters/table-faceted-filter.js +1 -1
- package/dist/components/siyahi/histogram-chart.js +8 -8
- package/dist/components/siyahi/scatter-chart.js +1 -1
- package/dist/components/siyahi/slope-chart.js +1 -1
- package/dist/components/siyahi/stale-note.d.ts +9 -0
- package/dist/components/siyahi/stale-note.js +12 -0
- package/dist/components/siyahi/treemap.js +1 -1
- package/dist/components/siyahi/waterfall-chart.js +4 -4
- package/dist/components/ui/field.d.ts +26 -0
- package/dist/components/ui/field.js +109 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.js +13 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/lib/chart-frame.d.ts +28 -4
- package/dist/lib/chart-frame.js +256 -153
- package/dist/lib/chart-marks.d.ts +83 -0
- package/dist/lib/chart-marks.js +216 -0
- package/dist/lib/chart-series.d.ts +15 -0
- package/package.json +1 -1
- package/report.css +6 -4
- package/styles/siyahi.css +260 -18
package/dist/lib/chart-frame.js
CHANGED
|
@@ -2,17 +2,64 @@ import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader,
|
|
|
2
2
|
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent } from "../components/ui/chart.js";
|
|
3
3
|
import { cn } from "./utils.js";
|
|
4
4
|
import { useAxisScale } from "../components/siyahi/scale-toggle.js";
|
|
5
|
+
import { StaleNote } from "../components/siyahi/stale-note.js";
|
|
5
6
|
import { useLocale } from "../components/siyahi/siyahi-provider.js";
|
|
7
|
+
import { EndLabels, QUIET, TimeMarks, endLabelRoom, endLabelsFit, linesEndAtEdge } from "./chart-marks.js";
|
|
6
8
|
import { fitsLog, logAxis } from "./ticks.js";
|
|
9
|
+
import { useElementWidth } from "./use-element-width.js";
|
|
7
10
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
-
import { useMemo } from "react";
|
|
9
|
-
import { CartesianGrid, ReferenceLine, Text, XAxis, YAxis, useChartWidth, useXAxisTicks, useYAxisTicks } from "recharts";
|
|
11
|
+
import { useEffect, useId, useMemo, useState } from "react";
|
|
12
|
+
import { CartesianGrid, Line, ReferenceLine, Text, XAxis, YAxis, useChartWidth, useXAxisTicks, useYAxisTicks } from "recharts";
|
|
10
13
|
//#region src/lib/chart-frame.tsx
|
|
11
14
|
const AXIS_TITLE = "fill-muted-foreground text-xs";
|
|
12
15
|
const TICK = { className: "fill-muted-foreground font-mono" };
|
|
13
16
|
const NO_LINES = [];
|
|
14
17
|
const SERIES_LEGEND = /* @__PURE__ */ jsx(ChartLegendContent, { className: "flex-wrap gap-y-1" });
|
|
15
18
|
/**
|
|
19
|
+
* A bar's fill: a tint of its colour, inside a 1px edge of the full colour the bar is drawn with. A selection dims
|
|
20
|
+
* the bars it leaves out to a fainter tint; their edges stay, so they keep 3:1 against the card.
|
|
21
|
+
*/
|
|
22
|
+
function barFill(color, dim = false) {
|
|
23
|
+
return `color-mix(in oklab, ${color} ${dim ? 10 : 25}%, var(--card))`;
|
|
24
|
+
}
|
|
25
|
+
/** Renders the component again once the page's fonts load, so text measured on a canvas is measured in Geist. */
|
|
26
|
+
function useRenderWhenFontsLoad() {
|
|
27
|
+
const [loaded, setLoaded] = useState(() => typeof document === "undefined" || document.fonts?.status === "loaded");
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (loaded || !document.fonts) return;
|
|
30
|
+
let live = true;
|
|
31
|
+
document.fonts.ready.then(() => live && setLoaded(true));
|
|
32
|
+
return () => {
|
|
33
|
+
live = false;
|
|
34
|
+
};
|
|
35
|
+
}, [loaded]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The legend when some series are quiet: every series by name. The grey ones share a colour, so a legend square
|
|
39
|
+
* cannot say which line is which; on lines each grey name is a button that raises its line in the text colour, over
|
|
40
|
+
* the rest. A stack's grey bands sit in the legend's order, bottom to top.
|
|
41
|
+
*/
|
|
42
|
+
function QuietLegend({ series, raisable, raised, onRaise }) {
|
|
43
|
+
return /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "flex flex-wrap items-center justify-center gap-x-4 gap-y-1 pt-3",
|
|
45
|
+
onFocus: (event) => event.stopPropagation(),
|
|
46
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
47
|
+
children: series.map(({ key, label, color }) => raisable && color === "var(--muted-foreground)" ? /* @__PURE__ */ jsxs("button", {
|
|
48
|
+
type: "button",
|
|
49
|
+
"aria-pressed": raised === key,
|
|
50
|
+
onClick: () => onRaise(raised === key ? void 0 : key),
|
|
51
|
+
className: "flex items-center gap-1.5 text-muted-foreground aria-pressed:text-foreground",
|
|
52
|
+
children: [/* @__PURE__ */ jsx("div", { className: "size-2 shrink-0 bg-current" }), label]
|
|
53
|
+
}, key) : /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
className: cn("flex items-center gap-1.5", color === "var(--muted-foreground)" && "text-muted-foreground"),
|
|
55
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
56
|
+
className: "size-2 shrink-0",
|
|
57
|
+
style: { backgroundColor: color }
|
|
58
|
+
}), label]
|
|
59
|
+
}, key))
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
16
63
|
* A reference line over a card-coloured band, so it stays traceable where it crosses a bar or an area: the text
|
|
17
64
|
* colour on --chart-1 is only 2:1, but each against the card is well over 3:1.
|
|
18
65
|
*/
|
|
@@ -48,8 +95,15 @@ function seriesFill(item, data) {
|
|
|
48
95
|
* A Line or Area's dots: one solid dot on the latest point, ringed in the card's colour so it stands clear of the
|
|
49
96
|
* grid, and none elsewhere. The eye lands on now, where a reader starts.
|
|
50
97
|
*/
|
|
51
|
-
function endDot(color, count) {
|
|
52
|
-
return ({ cx, cy, index }) => index === count - 1 && cx !== void 0 && cy !== void 0 ? /* @__PURE__ */ jsx("circle", {
|
|
98
|
+
function endDot(color, count, hollow = false) {
|
|
99
|
+
return ({ cx, cy, index }) => index === count - 1 && cx !== void 0 && cy !== void 0 ? hollow ? /* @__PURE__ */ jsx("circle", {
|
|
100
|
+
cx,
|
|
101
|
+
cy,
|
|
102
|
+
r: 3.5,
|
|
103
|
+
fill: "var(--card)",
|
|
104
|
+
stroke: color,
|
|
105
|
+
strokeWidth: 1.5
|
|
106
|
+
}, "end") : /* @__PURE__ */ jsx("circle", {
|
|
53
107
|
cx,
|
|
54
108
|
cy,
|
|
55
109
|
r: 4,
|
|
@@ -188,8 +242,18 @@ function HorizontalAxis({ data, xKey, xFormatter, xTickDetail, xTicks }) {
|
|
|
188
242
|
* that gave its category axis its own id (dash#88) got a plot split in two
|
|
189
243
|
* and every tooltip headed with the series name instead of the category.
|
|
190
244
|
*/
|
|
191
|
-
function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, onClick, tooltipRow, legend = SERIES_LEGEND, title, description, action, data, xKey, series, xFormatter = String, xTooltipFormatter = xFormatter, valueFormatter, yTickFormatter, yDomain, yTicks, height, xTickDetail, xLabel, yLabel, referenceLines = NO_LINES, placeholder, footer, xDomain, xTicks, yScale = "linear", yScaleToggle = false, logBlocked }) {
|
|
245
|
+
function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, onClick, tooltipRow, legend = SERIES_LEGEND, title, description, action, data, xKey, series, xFormatter = String, xTooltipFormatter = xFormatter, valueFormatter, yTickFormatter, yDomain, yTicks, height, xTickDetail, xLabel, yLabel, referenceLines = NO_LINES, placeholder, footer, xDomain, xTicks, yScale = "linear", yScaleToggle = false, logBlocked, endLabels = false, forecastHatch = true, curve = "monotone", forecastFrom, today, stale }) {
|
|
192
246
|
const { numberLocale } = useLocale();
|
|
247
|
+
const clipId = useId();
|
|
248
|
+
const [contentRef, contentWidth] = useElementWidth();
|
|
249
|
+
useRenderWhenFontsLoad();
|
|
250
|
+
const [raisedKey, setRaisedKey] = useState();
|
|
251
|
+
const names = endLabels ? series.map(({ label }) => label) : [];
|
|
252
|
+
const labelRoom = endLabels ? endLabelRoom(names) : 0;
|
|
253
|
+
const plotHeight = height - 12 - (xTickDetail ? 40 : 30) - (xLabel ? 18 : 0);
|
|
254
|
+
const ends = endLabels && linesEndAtEdge(data, xKey, series, xDomain) && endLabelsFit(names, contentWidth, plotHeight);
|
|
255
|
+
const quiet = series.some(({ color }) => color === QUIET);
|
|
256
|
+
const raised = endLabels && !ends ? series.find(({ key, color }) => key === raisedKey && color === "var(--muted-foreground)") : void 0;
|
|
193
257
|
const formatValue = useMemo(() => valueFormatter ?? new Intl.NumberFormat(numberLocale).format, [valueFormatter, numberLocale]);
|
|
194
258
|
const config = Object.fromEntries(series.map(({ key, label, color }) => [key, {
|
|
195
259
|
label,
|
|
@@ -220,158 +284,197 @@ function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, o
|
|
|
220
284
|
const plotStyle = {
|
|
221
285
|
height: detailedRows ? Math.max(height, rowCount * 72 + 48) : height,
|
|
222
286
|
minWidth,
|
|
223
|
-
...detailedRows ? { "--wrapped-chart-height": `${rowCount * 104 + 48}px` } : {}
|
|
287
|
+
...detailedRows ? { "--wrapped-chart-height": `${rowCount * 104 + 48}px` } : {},
|
|
288
|
+
...forecastFrom !== void 0 ? {
|
|
289
|
+
"--chart-actual": `url(#${clipId}-actual)`,
|
|
290
|
+
"--chart-forecast": `url(#${clipId}-forecast)`
|
|
291
|
+
} : {}
|
|
224
292
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
children:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
293
|
+
const endRight = !horizontal && xTicks ? endRoom(xFormatter, xTicks) : horizontal && yTickFormatter ? 28 : 8;
|
|
294
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
295
|
+
"data-stale": stale ? "" : void 0,
|
|
296
|
+
children: [
|
|
297
|
+
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
298
|
+
/* @__PURE__ */ jsx(CardTitle, { children: title }),
|
|
299
|
+
description && /* @__PURE__ */ jsx(CardDescription, { children: description }),
|
|
300
|
+
stale && /* @__PURE__ */ jsx(StaleNote, { since: stale }),
|
|
301
|
+
(action || valueAxis.buttons) && /* @__PURE__ */ jsxs(CardAction, {
|
|
302
|
+
className: "flex flex-wrap items-center gap-x-3 gap-y-2",
|
|
303
|
+
children: [action, valueAxis.buttons]
|
|
304
|
+
})
|
|
305
|
+
] }),
|
|
306
|
+
/* @__PURE__ */ jsx(CardContent, {
|
|
307
|
+
ref: contentRef,
|
|
308
|
+
className: "@container/chart overflow-x-auto",
|
|
309
|
+
role: minWidth ? "region" : void 0,
|
|
310
|
+
"aria-label": minWidth ? "Chart, scroll horizontally for all labelled values" : void 0,
|
|
311
|
+
tabIndex: minWidth ? 0 : void 0,
|
|
312
|
+
children: placeholder ? /* @__PURE__ */ jsx("div", {
|
|
313
|
+
"data-slot": "chart-placeholder",
|
|
314
|
+
className: cn("flex items-center justify-center", plotClass),
|
|
315
|
+
style: plotStyle,
|
|
316
|
+
children: placeholder
|
|
317
|
+
}) : /* @__PURE__ */ jsx(ChartContainer, {
|
|
318
|
+
config,
|
|
319
|
+
className: cn("aspect-auto w-full", plotClass, onClick && "cursor-pointer"),
|
|
320
|
+
style: plotStyle,
|
|
321
|
+
children: /* @__PURE__ */ jsxs(Chart, {
|
|
322
|
+
data,
|
|
323
|
+
layout: horizontal ? "vertical" : "horizontal",
|
|
324
|
+
stackOffset,
|
|
325
|
+
onClick,
|
|
326
|
+
throttledEvents: onClick ? [] : void 0,
|
|
327
|
+
margin: {
|
|
328
|
+
top: 12,
|
|
329
|
+
left: 0,
|
|
330
|
+
right: ends ? Math.max(labelRoom, endRight) : endRight
|
|
331
|
+
},
|
|
332
|
+
children: [
|
|
333
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
334
|
+
vertical: horizontal,
|
|
335
|
+
horizontal: !horizontal
|
|
336
|
+
}),
|
|
337
|
+
horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
|
|
338
|
+
type: "number",
|
|
339
|
+
niceTicks: "snap125",
|
|
340
|
+
hide: !yTickFormatter,
|
|
341
|
+
domain: yDomain,
|
|
342
|
+
ticks: yTicks,
|
|
343
|
+
tickLine: false,
|
|
344
|
+
axisLine: false,
|
|
345
|
+
tickMargin: 8,
|
|
346
|
+
tick: yTickFormatter ? /* @__PURE__ */ jsx(ValueTick, {
|
|
347
|
+
axis: "x",
|
|
348
|
+
formatter: yTickFormatter,
|
|
349
|
+
...TICK
|
|
350
|
+
}) : TICK
|
|
351
|
+
}), /* @__PURE__ */ jsx(HorizontalAxis, {
|
|
352
|
+
data,
|
|
353
|
+
xKey,
|
|
354
|
+
xFormatter,
|
|
355
|
+
xTickDetail,
|
|
356
|
+
xTicks
|
|
357
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
|
|
358
|
+
dataKey: xKey,
|
|
359
|
+
type: xDomain ? "number" : "category",
|
|
360
|
+
domain: xDomain,
|
|
361
|
+
ticks: xTicks,
|
|
362
|
+
height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
|
|
363
|
+
label: xLabel ? {
|
|
364
|
+
value: xLabel,
|
|
365
|
+
position: "insideBottom",
|
|
366
|
+
className: AXIS_TITLE
|
|
367
|
+
} : void 0,
|
|
368
|
+
tickLine: false,
|
|
369
|
+
axisLine: false,
|
|
370
|
+
tickMargin: 8,
|
|
371
|
+
tickFormatter: (value) => xFormatter(value),
|
|
372
|
+
tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
|
|
373
|
+
interval: xTickDetail || xTicks ? 0 : void 0
|
|
374
|
+
}), /* @__PURE__ */ jsx(YAxis, {
|
|
375
|
+
scale: valueAxis.scale,
|
|
376
|
+
niceTicks: log ? void 0 : "snap125",
|
|
377
|
+
tickLine: false,
|
|
378
|
+
axisLine: false,
|
|
379
|
+
width: "auto",
|
|
380
|
+
domain: log?.domain ?? yDomain,
|
|
381
|
+
ticks: log?.ticks ?? yTicks,
|
|
382
|
+
label: yLabel ? {
|
|
383
|
+
value: yLabel,
|
|
384
|
+
angle: -90,
|
|
385
|
+
position: "insideLeft",
|
|
386
|
+
style: { textAnchor: "middle" },
|
|
387
|
+
className: AXIS_TITLE
|
|
388
|
+
} : void 0,
|
|
389
|
+
tick: yTickFormatter ? /* @__PURE__ */ jsx(ValueTick, {
|
|
390
|
+
axis: "y",
|
|
391
|
+
formatter: yTickFormatter,
|
|
392
|
+
ownUnit: log !== void 0,
|
|
393
|
+
...TICK
|
|
394
|
+
}) : TICK,
|
|
395
|
+
tickFormatter: yTickFormatter ? void 0 : (value) => formatValue(value)
|
|
396
|
+
})] }),
|
|
397
|
+
crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
|
|
398
|
+
x: 0,
|
|
399
|
+
stroke: "var(--border)"
|
|
400
|
+
}) : /* @__PURE__ */ jsx(ReferenceLine, {
|
|
401
|
+
y: 0,
|
|
402
|
+
stroke: "var(--border)"
|
|
403
|
+
})),
|
|
404
|
+
referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
|
|
405
|
+
...horizontal ? {
|
|
406
|
+
x: at.y,
|
|
407
|
+
y: at.x
|
|
408
|
+
} : at,
|
|
409
|
+
stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
|
|
410
|
+
shape: haloLine,
|
|
411
|
+
strokeDasharray: at.y === void 0 ? void 0 : "4 4",
|
|
412
|
+
label: label === void 0 ? void 0 : {
|
|
413
|
+
value: label,
|
|
414
|
+
position: labelPosition,
|
|
415
|
+
className: "fill-muted-foreground stroke-card text-xs",
|
|
416
|
+
style: {
|
|
417
|
+
paintOrder: "stroke",
|
|
418
|
+
strokeWidth: 3,
|
|
419
|
+
strokeLinejoin: "round"
|
|
420
|
+
}
|
|
348
421
|
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
422
|
+
}, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
|
|
423
|
+
/* @__PURE__ */ jsx(ChartTooltip, {
|
|
424
|
+
cursor: { fillOpacity: .4 },
|
|
425
|
+
content: /* @__PURE__ */ jsx(ChartTooltipContent, {
|
|
426
|
+
labelFormatter: (_, payload) => {
|
|
427
|
+
const row = payload[0]?.payload;
|
|
428
|
+
return row ? xTooltipFormatter(row[xKey]) : null;
|
|
429
|
+
},
|
|
430
|
+
formatter: tooltipRow ?? seriesRow(series, formatValue)
|
|
431
|
+
})
|
|
432
|
+
}),
|
|
433
|
+
(forecastFrom !== void 0 || today !== void 0) && /* @__PURE__ */ jsx(TimeMarks, {
|
|
434
|
+
data,
|
|
435
|
+
xKey,
|
|
436
|
+
forecastFrom,
|
|
437
|
+
today,
|
|
438
|
+
clipId,
|
|
439
|
+
hatch: forecastHatch
|
|
440
|
+
}),
|
|
441
|
+
children,
|
|
442
|
+
raised && (forecastFrom === void 0 ? ["solid"] : ["chart-actual", "chart-forecast"]).map((part) => /* @__PURE__ */ jsx(Line, {
|
|
443
|
+
dataKey: raised.key,
|
|
444
|
+
type: curve,
|
|
445
|
+
stroke: "var(--foreground)",
|
|
446
|
+
strokeWidth: 2,
|
|
447
|
+
strokeDasharray: part === "chart-forecast" ? "5 4" : void 0,
|
|
448
|
+
className: part === "solid" ? "chart-raised" : `chart-raised ${part}`,
|
|
449
|
+
dot: false,
|
|
450
|
+
activeDot: false,
|
|
451
|
+
tooltipType: "none",
|
|
452
|
+
legendType: "none",
|
|
453
|
+
isAnimationActive: false
|
|
454
|
+
}, `raised-${part}`)),
|
|
455
|
+
ends ? /* @__PURE__ */ jsx(EndLabels, {
|
|
456
|
+
data,
|
|
457
|
+
xKey,
|
|
458
|
+
series
|
|
459
|
+
}) : /* @__PURE__ */ jsx(ChartLegend, {
|
|
460
|
+
content: quiet ? /* @__PURE__ */ jsx(QuietLegend, {
|
|
461
|
+
series,
|
|
462
|
+
raisable: endLabels,
|
|
463
|
+
raised: raised?.key,
|
|
464
|
+
onRaise: setRaisedKey
|
|
465
|
+
}) : legend,
|
|
466
|
+
itemSorter: null
|
|
359
467
|
})
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
/* @__PURE__ */ jsx(ChartLegend, {
|
|
363
|
-
content: legend,
|
|
364
|
-
itemSorter: null
|
|
365
|
-
})
|
|
366
|
-
]
|
|
468
|
+
]
|
|
469
|
+
})
|
|
367
470
|
})
|
|
471
|
+
}),
|
|
472
|
+
footer && /* @__PURE__ */ jsx(CardFooter, {
|
|
473
|
+
className: "flex-col items-start gap-2 text-sm",
|
|
474
|
+
children: footer
|
|
368
475
|
})
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
className: "flex-col items-start gap-2 text-sm",
|
|
372
|
-
children: footer
|
|
373
|
-
})
|
|
374
|
-
] });
|
|
476
|
+
]
|
|
477
|
+
});
|
|
375
478
|
}
|
|
376
479
|
//#endregion
|
|
377
|
-
export { ChartFrame, endDot, seriesFill, seriesRow };
|
|
480
|
+
export { ChartFrame, QuietLegend, barFill, endDot, seriesFill, seriesRow };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ChartSeries } from "./chart-series.js";
|
|
2
|
+
//#region src/lib/chart-marks.d.ts
|
|
3
|
+
/** The colour a series takes when it is one of many drawn quietly behind the few a page names in colour. */
|
|
4
|
+
export declare const QUIET = "var(--muted-foreground)";
|
|
5
|
+
/** Quiet series first, so the named ones draw over them; otherwise the page's order. */
|
|
6
|
+
export declare function drawOrder<T>(series: ChartSeries<T>): ChartSeries<T>;
|
|
7
|
+
/** Room the names need right of the plot: the widest name as drawn, after its leader and colour square. */
|
|
8
|
+
export declare function endLabelRoom(names: readonly string[]): number;
|
|
9
|
+
/**
|
|
10
|
+
* Whether every line ends on the plot's right edge: each series has a value in the last row, there is more than one
|
|
11
|
+
* row, and a numeric x axis runs no further than the data. A line that stops short would need a long leader across
|
|
12
|
+
* the others to reach its name, so the chart keeps its legend then.
|
|
13
|
+
*/
|
|
14
|
+
export declare function linesEndAtEdge<T extends object, K extends keyof T>(data: readonly T[], xKey: K, series: ChartSeries<T>, xDomain?: readonly [number, number]): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the names fit at the line ends: a card at least 400px wide, names taking at most 30% of it, and a plot
|
|
17
|
+
* tall enough to hold them all 14px apart. Otherwise the chart keeps its legend below.
|
|
18
|
+
*/
|
|
19
|
+
export declare function endLabelsFit(names: readonly string[], width: number, plotHeight: number): boolean;
|
|
20
|
+
/** A series' line end, in pixels, and the name it carries there. */
|
|
21
|
+
export interface LineEnd {
|
|
22
|
+
/** The series' key: names can repeat, keys cannot. */
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
color: string;
|
|
26
|
+
x: number;
|
|
27
|
+
y: number;
|
|
28
|
+
/** A quiet (grey) series: its name is muted, with no colour square; its place says which line it is. */
|
|
29
|
+
quiet: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** A name placed in the gutter right of the plot. */
|
|
32
|
+
export interface PlacedEnd extends LineEnd {
|
|
33
|
+
/** The level it sits at, after names that would collide are spread apart. */
|
|
34
|
+
at: number;
|
|
35
|
+
/** The plot's right edge, where every name starts, whichever point its line ends on. */
|
|
36
|
+
gutter: number;
|
|
37
|
+
/** A leader joins the name to its line's end: moved off the end's level, or the line ends short of the edge. */
|
|
38
|
+
leader: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** The part of the chart the series are drawn in, as Recharts' usePlotArea gives it. */
|
|
41
|
+
export interface PlotBox {
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Where each name goes: every line's name at its end's level, spread to 14px apart, top to bottom, pushed back up
|
|
49
|
+
* from the plot's foot and never above its top. A point that maps to no number (a scale being replaced) is left out.
|
|
50
|
+
*/
|
|
51
|
+
export declare function placeEnds(lines: readonly LineEnd[], plot: PlotBox): PlacedEnd[];
|
|
52
|
+
interface EndLabelsProps<T extends object, K extends keyof T> {
|
|
53
|
+
data: readonly T[];
|
|
54
|
+
xKey: K;
|
|
55
|
+
series: ChartSeries<T>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Each series' name in the gutter right of the plot, in the text colour beside a square of the line's colour, in
|
|
59
|
+
* place of a legend the reader has to match colours against. A leader joins each name to its line's end where the
|
|
60
|
+
* name was moved off the end's level or the line ends short of the plot's edge. A quiet (grey) series' name is
|
|
61
|
+
* muted, with no square: its place beside its line's end says which line it is.
|
|
62
|
+
*/
|
|
63
|
+
export declare function EndLabels<T extends object, K extends keyof T>({ data, xKey, series }: EndLabelsProps<T, K>): import("react").JSX.Element | null;
|
|
64
|
+
interface TimeMarksProps<T extends object, K extends keyof T> {
|
|
65
|
+
data: readonly T[];
|
|
66
|
+
xKey: K;
|
|
67
|
+
/** The x value from which the data is a forecast. */
|
|
68
|
+
forecastFrom?: T[K];
|
|
69
|
+
/** The x value of today. */
|
|
70
|
+
today?: T[K];
|
|
71
|
+
/** Prefix of the clip paths the series' actual and forecast copies use (`<id>-actual`, `<id>-forecast`). */
|
|
72
|
+
clipId: string;
|
|
73
|
+
/** Hatch the forecast stretch; a stack's fills already cover it, so a stacked chart only labels it. */
|
|
74
|
+
hatch: boolean;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A forecast stretch, from forecastFrom to the last point: a faint hatched band labelled FORECAST behind the lines,
|
|
78
|
+
* and the clip paths that keep each line solid before it and dashed after. And a "today" rule: 1px in the link
|
|
79
|
+
* colour with a small mono capital label.
|
|
80
|
+
*/
|
|
81
|
+
export declare function TimeMarks<T extends object, K extends keyof T>({ data, xKey, forecastFrom, today, clipId, hatch }: TimeMarksProps<T, K>): import("react").JSX.Element | null;
|
|
82
|
+
//#endregion
|
|
83
|
+
export {};
|