@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.
Files changed (46) hide show
  1. package/DESIGN.md +79 -10
  2. package/agents/examples/avatar.tsx +15 -5
  3. package/agents/examples/chart-line-forecast.tsx +36 -0
  4. package/agents/examples/chart-line-quiet.tsx +114 -0
  5. package/agents/examples/chart-panels.tsx +94 -0
  6. package/agents/examples/code-block.tsx +5 -3
  7. package/agents/examples/field.tsx +108 -0
  8. package/agents/examples/formatters.tsx +14 -14
  9. package/agents/examples/input-states.tsx +12 -22
  10. package/agents/examples/overview-dashboard.tsx +6 -6
  11. package/agents/examples/stale-blocks.tsx +56 -0
  12. package/agents/examples/table.tsx +6 -6
  13. package/agents/manifest.json +104 -3
  14. package/dist/components/registry/charts/area-chart-card.d.ts +7 -3
  15. package/dist/components/registry/charts/area-chart-card.js +52 -25
  16. package/dist/components/registry/charts/bar-chart-card.d.ts +1 -0
  17. package/dist/components/registry/charts/bar-chart-card.js +8 -24
  18. package/dist/components/registry/charts/line-chart-card.d.ts +2 -2
  19. package/dist/components/registry/charts/line-chart-card.js +42 -7
  20. package/dist/components/registry/dashboard-01/section-cards.d.ts +2 -0
  21. package/dist/components/registry/dashboard-01/section-cards.js +3 -0
  22. package/dist/components/registry/heatmap/calendar-heatmap.js +2 -2
  23. package/dist/components/registry/heatmap/status-heatmap.js +2 -2
  24. package/dist/components/registry/heatmap/weekday-heatmap.js +2 -2
  25. package/dist/components/registry/niko-table/filters/table-faceted-filter.js +1 -1
  26. package/dist/components/siyahi/histogram-chart.js +8 -8
  27. package/dist/components/siyahi/scatter-chart.js +1 -1
  28. package/dist/components/siyahi/slope-chart.js +1 -1
  29. package/dist/components/siyahi/stale-note.d.ts +9 -0
  30. package/dist/components/siyahi/stale-note.js +12 -0
  31. package/dist/components/siyahi/treemap.js +1 -1
  32. package/dist/components/siyahi/waterfall-chart.js +4 -4
  33. package/dist/components/ui/field.d.ts +26 -0
  34. package/dist/components/ui/field.js +109 -0
  35. package/dist/components/ui/label.d.ts +5 -0
  36. package/dist/components/ui/label.js +13 -0
  37. package/dist/index.d.ts +5 -2
  38. package/dist/index.js +5 -2
  39. package/dist/lib/chart-frame.d.ts +28 -4
  40. package/dist/lib/chart-frame.js +256 -153
  41. package/dist/lib/chart-marks.d.ts +83 -0
  42. package/dist/lib/chart-marks.js +216 -0
  43. package/dist/lib/chart-series.d.ts +15 -0
  44. package/package.json +1 -1
  45. package/report.css +6 -4
  46. package/styles/siyahi.css +260 -18
@@ -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
- return /* @__PURE__ */ jsxs(Card, { children: [
226
- /* @__PURE__ */ jsxs(CardHeader, { children: [
227
- /* @__PURE__ */ jsx(CardTitle, { children: title }),
228
- description && /* @__PURE__ */ jsx(CardDescription, { children: description }),
229
- (action || valueAxis.buttons) && /* @__PURE__ */ jsxs(CardAction, {
230
- className: "flex flex-wrap items-center gap-x-3 gap-y-2",
231
- children: [action, valueAxis.buttons]
232
- })
233
- ] }),
234
- /* @__PURE__ */ jsx(CardContent, {
235
- className: "@container/chart overflow-x-auto",
236
- role: minWidth ? "region" : void 0,
237
- "aria-label": minWidth ? "Chart, scroll horizontally for all labelled values" : void 0,
238
- tabIndex: minWidth ? 0 : void 0,
239
- children: placeholder ? /* @__PURE__ */ jsx("div", {
240
- "data-slot": "chart-placeholder",
241
- className: cn("flex items-center justify-center", plotClass),
242
- style: plotStyle,
243
- children: placeholder
244
- }) : /* @__PURE__ */ jsx(ChartContainer, {
245
- config,
246
- className: cn("aspect-auto w-full", plotClass, onClick && "cursor-pointer"),
247
- style: plotStyle,
248
- children: /* @__PURE__ */ jsxs(Chart, {
249
- data,
250
- layout: horizontal ? "vertical" : "horizontal",
251
- stackOffset,
252
- onClick,
253
- throttledEvents: onClick ? [] : void 0,
254
- margin: {
255
- top: 12,
256
- left: 0,
257
- right: !horizontal && xTicks ? endRoom(xFormatter, xTicks) : horizontal && yTickFormatter ? 28 : 8
258
- },
259
- children: [
260
- /* @__PURE__ */ jsx(CartesianGrid, {
261
- vertical: horizontal,
262
- horizontal: !horizontal,
263
- strokeDasharray: "3 3"
264
- }),
265
- horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
266
- type: "number",
267
- niceTicks: "snap125",
268
- hide: !yTickFormatter,
269
- domain: yDomain,
270
- ticks: yTicks,
271
- tickLine: false,
272
- axisLine: false,
273
- tickMargin: 8,
274
- tick: yTickFormatter ? /* @__PURE__ */ jsx(ValueTick, {
275
- axis: "x",
276
- formatter: yTickFormatter,
277
- ...TICK
278
- }) : TICK
279
- }), /* @__PURE__ */ jsx(HorizontalAxis, {
280
- data,
281
- xKey,
282
- xFormatter,
283
- xTickDetail,
284
- xTicks
285
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
286
- dataKey: xKey,
287
- type: xDomain ? "number" : "category",
288
- domain: xDomain,
289
- ticks: xTicks,
290
- height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
291
- label: xLabel ? {
292
- value: xLabel,
293
- position: "insideBottom",
294
- className: AXIS_TITLE
295
- } : void 0,
296
- tickLine: false,
297
- axisLine: false,
298
- tickMargin: 8,
299
- tickFormatter: (value) => xFormatter(value),
300
- tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
301
- interval: xTickDetail || xTicks ? 0 : void 0
302
- }), /* @__PURE__ */ jsx(YAxis, {
303
- scale: valueAxis.scale,
304
- niceTicks: log ? void 0 : "snap125",
305
- tickLine: false,
306
- axisLine: false,
307
- width: "auto",
308
- domain: log?.domain ?? yDomain,
309
- ticks: log?.ticks ?? yTicks,
310
- label: yLabel ? {
311
- value: yLabel,
312
- angle: -90,
313
- position: "insideLeft",
314
- style: { textAnchor: "middle" },
315
- className: AXIS_TITLE
316
- } : void 0,
317
- tick: yTickFormatter ? /* @__PURE__ */ jsx(ValueTick, {
318
- axis: "y",
319
- formatter: yTickFormatter,
320
- ownUnit: log !== void 0,
321
- ...TICK
322
- }) : TICK,
323
- tickFormatter: yTickFormatter ? void 0 : (value) => formatValue(value)
324
- })] }),
325
- crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
326
- x: 0,
327
- stroke: "var(--border)"
328
- }) : /* @__PURE__ */ jsx(ReferenceLine, {
329
- y: 0,
330
- stroke: "var(--border)"
331
- })),
332
- referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
333
- ...horizontal ? {
334
- x: at.y,
335
- y: at.x
336
- } : at,
337
- stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
338
- shape: haloLine,
339
- strokeDasharray: at.y === void 0 ? void 0 : "4 4",
340
- label: label === void 0 ? void 0 : {
341
- value: label,
342
- position: labelPosition,
343
- className: "fill-muted-foreground stroke-card text-xs",
344
- style: {
345
- paintOrder: "stroke",
346
- strokeWidth: 3,
347
- strokeLinejoin: "round"
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
- }, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
351
- /* @__PURE__ */ jsx(ChartTooltip, {
352
- cursor: { fillOpacity: .4 },
353
- content: /* @__PURE__ */ jsx(ChartTooltipContent, {
354
- labelFormatter: (_, payload) => {
355
- const row = payload[0]?.payload;
356
- return row ? xTooltipFormatter(row[xKey]) : null;
357
- },
358
- formatter: tooltipRow ?? seriesRow(series, formatValue)
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
- children,
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
- footer && /* @__PURE__ */ jsx(CardFooter, {
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 {};