@siyahi/ui 0.1.9-next.17 → 0.1.9-next.19
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 +77 -207
- package/agents/examples/accordion.tsx +39 -0
- package/agents/examples/alert.tsx +25 -0
- package/agents/examples/app-sidebar.tsx +38 -0
- package/agents/examples/avatar.tsx +14 -0
- package/agents/examples/badge.tsx +18 -0
- package/agents/examples/bar-list-labels.tsx +22 -0
- package/agents/examples/bar-list-sides.tsx +30 -0
- package/agents/examples/bar-list.tsx +24 -0
- package/agents/examples/breadcrumb.tsx +28 -0
- package/agents/examples/button.tsx +21 -0
- package/agents/examples/calendar.tsx +15 -0
- package/agents/examples/card.tsx +34 -0
- package/agents/examples/chart-area.tsx +38 -0
- package/agents/examples/chart-bar-flows.tsx +31 -0
- package/agents/examples/chart-bar-hourly.tsx +28 -0
- package/agents/examples/chart-bar-losses.tsx +29 -0
- package/agents/examples/chart-bar-sectors.tsx +70 -0
- package/agents/examples/chart-bar-years.tsx +40 -0
- package/agents/examples/chart-bar.tsx +56 -0
- package/agents/examples/chart-category-ticks.tsx +50 -0
- package/agents/examples/chart-line-events.tsx +47 -0
- package/agents/examples/chart-line-timeline.tsx +41 -0
- package/agents/examples/chart-line.tsx +57 -0
- package/agents/examples/code-block.tsx +38 -0
- package/agents/examples/collapsible.tsx +16 -0
- package/agents/examples/command.tsx +27 -0
- package/agents/examples/context-menu.tsx +15 -0
- package/agents/examples/copy-button.tsx +8 -0
- package/agents/examples/data-bar.tsx +121 -0
- package/agents/examples/data-table-filters.tsx +103 -0
- package/agents/examples/data-table.tsx +98 -0
- package/agents/examples/delta.tsx +13 -0
- package/agents/examples/dialog.tsx +28 -0
- package/agents/examples/distribution-bar-budget.tsx +30 -0
- package/agents/examples/distribution-bar-tug.tsx +72 -0
- package/agents/examples/dropdown-menu.tsx +31 -0
- package/agents/examples/formatters.tsx +42 -0
- package/agents/examples/heatmap-calendar.tsx +50 -0
- package/agents/examples/heatmap-status.tsx +51 -0
- package/agents/examples/heatmap-weekday.tsx +53 -0
- package/agents/examples/input.tsx +17 -0
- package/agents/examples/local-time.tsx +14 -0
- package/agents/examples/metric-strip.tsx +40 -0
- package/agents/examples/page-section.tsx +52 -0
- package/agents/examples/popover.tsx +23 -0
- package/agents/examples/price-chart.tsx +32 -0
- package/agents/examples/progress-tone.tsx +28 -0
- package/agents/examples/progress.tsx +10 -0
- package/agents/examples/scroll-area.tsx +20 -0
- package/agents/examples/section-cards-select.tsx +27 -0
- package/agents/examples/section-cards-trend.tsx +30 -0
- package/agents/examples/section-cards.tsx +48 -0
- package/agents/examples/select.tsx +27 -0
- package/agents/examples/separator.tsx +13 -0
- package/agents/examples/sheet.tsx +15 -0
- package/agents/examples/skeleton.tsx +11 -0
- package/agents/examples/slider.tsx +5 -0
- package/agents/examples/surfaces.tsx +115 -0
- package/agents/examples/table.tsx +50 -0
- package/agents/examples/tabs.tsx +16 -0
- package/agents/examples/toggle-group.tsx +12 -0
- package/agents/examples/toggle.tsx +11 -0
- package/agents/examples/tooltip.tsx +13 -0
- package/agents/examples/treemap.tsx +87 -0
- package/agents/manifest.json +931 -0
- package/agents/report-rules.json +7 -0
- package/dist/components/registry/charts/bar-chart-card.js +1 -0
- package/dist/components/registry/dashboardcn/bar-list.js +8 -7
- package/dist/components/registry/heatmap/calendar-heatmap.js +8 -1
- package/dist/components/registry/heatmap/status-heatmap.js +8 -1
- package/dist/components/registry/heatmap/weekday-heatmap.d.ts +6 -2
- package/dist/components/registry/heatmap/weekday-heatmap.js +56 -37
- package/dist/components/registry/niko-table/core/data-table-context.d.ts +2 -0
- package/dist/components/registry/niko-table/core/data-table-context.js +21 -0
- package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
- package/dist/components/registry/niko-table/core/data-table-structure.js +15 -12
- package/dist/components/registry/niko-table/core/data-table.d.ts +2 -2
- package/dist/components/registry/niko-table/core/data-table.js +5 -1
- package/dist/components/registry/niko-table/filters/table-search-filter.js +1 -1
- package/dist/components/registry/niko-table/lib/flex-columns.d.ts +1 -1
- package/dist/components/registry/niko-table/lib/flex-columns.js +3 -3
- package/dist/components/registry/niko-table/lib/styles.d.ts +1 -1
- package/dist/components/registry/niko-table/lib/styles.js +2 -2
- package/dist/components/registry/sidebar-07/nav-main.js +7 -1
- package/dist/components/siyahi/treemap.js +46 -4
- package/dist/lib/chart-frame.js +180 -124
- package/dist/lib/chart-series.d.ts +1 -1
- package/dist/lib/format.js +32 -13
- package/dist/lib/heatmap-navigation.d.ts +8 -0
- package/dist/lib/heatmap-navigation.js +49 -0
- package/package.json +3 -1
- package/styles/siyahi.css +24 -0
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
* Returns the set of column ids to render widthless (empty when fill is off or
|
|
25
25
|
* no eligible column exists — the table then sizes to its columns as before).
|
|
26
26
|
*/
|
|
27
|
-
function resolveFlexColumnIds(table) {
|
|
27
|
+
function resolveFlexColumnIds(table, pinningEnabled = true) {
|
|
28
28
|
const ids = /* @__PURE__ */ new Set();
|
|
29
29
|
if (!table.options.enableColumnResizing) return ids;
|
|
30
30
|
if (table.options.meta?.disableFlexFill) return ids;
|
|
31
31
|
const leafColumns = table.getVisibleLeafColumns();
|
|
32
32
|
const columnSizing = table.state.columnSizing;
|
|
33
33
|
let hasExplicit = false;
|
|
34
|
-
for (const column of leafColumns) if (column.columnDef.meta?.flex === true && !column.getIsPinned() && columnSizing[column.id] == null) {
|
|
34
|
+
for (const column of leafColumns) if (column.columnDef.meta?.flex === true && !(pinningEnabled && column.getIsPinned()) && columnSizing[column.id] == null) {
|
|
35
35
|
ids.add(column.id);
|
|
36
36
|
hasExplicit = true;
|
|
37
37
|
}
|
|
38
38
|
if (hasExplicit) return ids;
|
|
39
|
-
const auto = leafColumns.find((column) => column.getCanResize() && !column.getIsPinned() && column.columnDef.meta?.flex !== false && columnSizing[column.id] == null);
|
|
39
|
+
const auto = leafColumns.find((column) => column.getCanResize() && !(pinningEnabled && column.getIsPinned()) && column.columnDef.meta?.flex !== false && columnSizing[column.id] == null);
|
|
40
40
|
if (auto) ids.add(auto.id);
|
|
41
41
|
return ids;
|
|
42
42
|
}
|
|
@@ -2,5 +2,5 @@ import { DataTableFeatures } from "./data-table-features.js";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Column, RowData } from "@tanstack/react-table";
|
|
4
4
|
//#region src/components/registry/niko-table/lib/styles.d.ts
|
|
5
|
-
export declare const getCommonPinningStyles: <TData extends RowData>(column: Column<DataTableFeatures, TData>, isHeader?: boolean) => React.CSSProperties;
|
|
5
|
+
export declare const getCommonPinningStyles: <TData extends RowData>(column: Column<DataTableFeatures, TData>, isHeader?: boolean, pinningEnabled?: boolean) => React.CSSProperties;
|
|
6
6
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "@tanstack/react-table";
|
|
2
2
|
//#region src/components/registry/niko-table/lib/styles.ts
|
|
3
|
-
const getCommonPinningStyles = (column, isHeader = false) => {
|
|
3
|
+
const getCommonPinningStyles = (column, isHeader = false, pinningEnabled = true) => {
|
|
4
4
|
const isPinned = column.getIsPinned();
|
|
5
|
-
if (!isPinned) return {};
|
|
5
|
+
if (!isPinned || !pinningEnabled) return {};
|
|
6
6
|
const isLeft = isPinned === "start";
|
|
7
7
|
const columnSize = column.getSize();
|
|
8
8
|
return {
|
|
@@ -2,9 +2,11 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../ui/co
|
|
|
2
2
|
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, useSidebar } from "../../ui/sidebar.js";
|
|
3
3
|
import { ChevronRightIcon } from "lucide-react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useState } from "react";
|
|
5
6
|
//#region src/components/registry/sidebar-07/nav-main.tsx
|
|
6
7
|
function NavMain({ items, label = "Navigation" }) {
|
|
7
8
|
const { isMobile, setOpenMobile } = useSidebar();
|
|
9
|
+
const [expanded, setExpanded] = useState({});
|
|
8
10
|
const closeDrawer = () => {
|
|
9
11
|
if (isMobile) setOpenMobile(false);
|
|
10
12
|
};
|
|
@@ -19,7 +21,11 @@ function NavMain({ items, label = "Navigation" }) {
|
|
|
19
21
|
onClick: closeDrawer,
|
|
20
22
|
children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
|
|
21
23
|
}) }, item.title) : /* @__PURE__ */ jsxs(Collapsible, {
|
|
22
|
-
|
|
24
|
+
open: expanded[item.title] ?? !!item.isActive,
|
|
25
|
+
onOpenChange: (open) => setExpanded((previous) => ({
|
|
26
|
+
...previous,
|
|
27
|
+
[item.title]: open
|
|
28
|
+
})),
|
|
23
29
|
className: "group/collapsible",
|
|
24
30
|
render: /* @__PURE__ */ jsx(SidebarMenuItem, {}),
|
|
25
31
|
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
@@ -2,12 +2,13 @@ import { useFormat } from "../../lib/format.js";
|
|
|
2
2
|
import { heat, heatFill, heatRange, layout } from "../../lib/treemap.js";
|
|
3
3
|
import { cn } from "cn";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { useEffect, useEffectEvent, useLayoutEffect, useRef, useState } from "react";
|
|
5
|
+
import { useEffect, useEffectEvent, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
//#region src/components/siyahi/treemap.tsx
|
|
7
7
|
/** Holdings heat map: area by weight, colour by signed change, optionally grouped; hand-drawn SVG. */
|
|
8
8
|
function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
9
9
|
const ref = useRef(null);
|
|
10
10
|
const [width, setWidth] = useState(0);
|
|
11
|
+
const [focusedKey, setFocusedKey] = useState(null);
|
|
11
12
|
const selectedKey = selected ? selected.key ?? selected.name : null;
|
|
12
13
|
const { pct } = useFormat();
|
|
13
14
|
useLayoutEffect(() => {
|
|
@@ -25,7 +26,12 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
25
26
|
document.addEventListener("pointerdown", onPointerDown);
|
|
26
27
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
27
28
|
}, [selectedKey]);
|
|
28
|
-
const { groups, tiles } = layout(items, width, height)
|
|
29
|
+
const { groups, tiles } = useMemo(() => layout(items, width, height), [
|
|
30
|
+
items,
|
|
31
|
+
width,
|
|
32
|
+
height
|
|
33
|
+
]);
|
|
34
|
+
const focusKey = tiles.some(({ item }) => (item.key ?? item.name) === focusedKey) ? focusedKey : tiles[0]?.item.key ?? tiles[0]?.item.name;
|
|
29
35
|
const change = (value) => value === null ? "—" : `${value > 0 ? "▲ " : value < 0 ? "▼ " : ""}${pct(Math.abs(value))}`;
|
|
30
36
|
return /* @__PURE__ */ jsx("div", {
|
|
31
37
|
ref,
|
|
@@ -34,7 +40,7 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
34
40
|
children: /* @__PURE__ */ jsxs("svg", {
|
|
35
41
|
width,
|
|
36
42
|
height,
|
|
37
|
-
role: "img",
|
|
43
|
+
role: onSelect ? "group" : "img",
|
|
38
44
|
"aria-label": "Treemap of holdings by weight and change",
|
|
39
45
|
className: "text-xs",
|
|
40
46
|
onPointerLeave: () => onHover?.(null),
|
|
@@ -53,12 +59,48 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
53
59
|
className: "fill-muted-foreground font-medium",
|
|
54
60
|
children: group
|
|
55
61
|
})]
|
|
56
|
-
}, group)), tiles.map(({ item, ...box }) => {
|
|
62
|
+
}, group)), tiles.map(({ item, ...box }, index) => {
|
|
57
63
|
const key = item.key ?? item.name;
|
|
58
64
|
return /* @__PURE__ */ jsxs("svg", {
|
|
59
65
|
...box,
|
|
60
66
|
"data-slot": "treemap-tile",
|
|
61
67
|
"data-selected": key === selectedKey || void 0,
|
|
68
|
+
role: onSelect ? "button" : void 0,
|
|
69
|
+
tabIndex: onSelect ? key === focusKey ? 0 : -1 : void 0,
|
|
70
|
+
"aria-label": `${item.name}${item.group ? `, ${item.group}` : ""}, ${change(item.change)}`,
|
|
71
|
+
"aria-pressed": onSelect ? key === selectedKey : void 0,
|
|
72
|
+
onFocus: () => setFocusedKey(key),
|
|
73
|
+
onKeyDown: (event) => {
|
|
74
|
+
if (!onSelect) return;
|
|
75
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
onSelect(key === selectedKey ? null : item);
|
|
78
|
+
} else if (event.key === "Escape") onSelect(null);
|
|
79
|
+
else {
|
|
80
|
+
let next;
|
|
81
|
+
switch (event.key) {
|
|
82
|
+
case "Home":
|
|
83
|
+
next = 0;
|
|
84
|
+
break;
|
|
85
|
+
case "End":
|
|
86
|
+
next = tiles.length - 1;
|
|
87
|
+
break;
|
|
88
|
+
case "ArrowRight":
|
|
89
|
+
case "ArrowDown":
|
|
90
|
+
next = (index + 1) % tiles.length;
|
|
91
|
+
break;
|
|
92
|
+
case "ArrowLeft":
|
|
93
|
+
case "ArrowUp":
|
|
94
|
+
next = (index + tiles.length - 1) % tiles.length;
|
|
95
|
+
break;
|
|
96
|
+
default: return;
|
|
97
|
+
}
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
const target = tiles[next]?.item;
|
|
100
|
+
if (target) setFocusedKey(target.key ?? target.name);
|
|
101
|
+
ref.current?.querySelectorAll("[data-slot=\"treemap-tile\"]")[next]?.focus();
|
|
102
|
+
}
|
|
103
|
+
},
|
|
62
104
|
onPointerEnter: () => onHover?.(item),
|
|
63
105
|
onClick: () => onSelect?.(key === selectedKey ? null : item),
|
|
64
106
|
children: [
|
package/dist/lib/chart-frame.js
CHANGED
|
@@ -3,7 +3,8 @@ import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartToo
|
|
|
3
3
|
import { cn } from "./utils.js";
|
|
4
4
|
import { useLocale } from "../components/siyahi/siyahi-provider.js";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import {
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { CartesianGrid, ReferenceLine, Text, XAxis, YAxis, useChartWidth } from "recharts";
|
|
7
8
|
//#region src/lib/chart-frame.tsx
|
|
8
9
|
const AXIS_TITLE = "fill-muted-foreground text-xs";
|
|
9
10
|
const TICK = { className: "fill-muted-foreground font-mono" };
|
|
@@ -47,26 +48,51 @@ function endRoom(xFormatter, xTicks) {
|
|
|
47
48
|
const last = xTicks.at(-1);
|
|
48
49
|
return Math.max(24, Math.ceil((last === void 0 ? 0 : xFormatter(last).length * MONO_WIDTH) / 2) + 4);
|
|
49
50
|
}
|
|
50
|
-
/** Squeezes a line into its column with SVG's own textLength when it would run into the next one. */
|
|
51
|
-
const fitTo = (text, room, charWidth) => room > 0 && text.length * charWidth > room ? {
|
|
52
|
-
textLength: room,
|
|
53
|
-
lengthAdjust: "spacingAndGlyphs"
|
|
54
|
-
} : {};
|
|
55
51
|
/**
|
|
56
|
-
* An x tick
|
|
52
|
+
* An x tick with a second line or explicit position: Recharts'
|
|
57
53
|
* own ticks would rather drop a label than let it run into the next. With a detail, the second line comes from the
|
|
58
54
|
* tick's row; under a column the lines hang below the axis, and beside a horizontal bar they straddle its middle.
|
|
59
55
|
* A two-line tick is set in Geist, not Mono: a name with a figure under it reads as a label, as in BarList, and
|
|
60
56
|
* sector names in Mono would take most of a phone's width.
|
|
61
57
|
*/
|
|
62
|
-
function fittedTick(data, xKey, xFormatter, detailOf, beside) {
|
|
63
|
-
const
|
|
64
|
-
|
|
58
|
+
function fittedTick(data, xKey, xFormatter, detailOf, beside, labelWidth) {
|
|
59
|
+
const rows = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const row of data) if (!rows.has(row[xKey])) rows.set(row[xKey], row);
|
|
61
|
+
return ({ x, y, payload }) => {
|
|
65
62
|
const value = payload?.value;
|
|
66
|
-
const row =
|
|
63
|
+
const row = rows.get(value);
|
|
67
64
|
const name = xFormatter(value);
|
|
68
65
|
const detail = row && detailOf ? detailOf(row) : null;
|
|
69
|
-
|
|
66
|
+
if (beside) return /* @__PURE__ */ jsxs("g", {
|
|
67
|
+
className: "fill-muted-foreground",
|
|
68
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
69
|
+
x,
|
|
70
|
+
y: Number(y) - (detail ? 4 : 0),
|
|
71
|
+
width: labelWidth,
|
|
72
|
+
maxLines: 2,
|
|
73
|
+
textAnchor: "end",
|
|
74
|
+
verticalAnchor: detail ? "end" : "middle",
|
|
75
|
+
lineHeight: 14,
|
|
76
|
+
style: { fontSize: "var(--text-xs)" },
|
|
77
|
+
"aria-label": name,
|
|
78
|
+
className: "recharts-cartesian-axis-tick-value fill-muted-foreground",
|
|
79
|
+
children: name
|
|
80
|
+
}), detail !== null && /* @__PURE__ */ jsx(Text, {
|
|
81
|
+
x,
|
|
82
|
+
y: Number(y) + 4,
|
|
83
|
+
width: labelWidth,
|
|
84
|
+
textAnchor: "end",
|
|
85
|
+
verticalAnchor: "start",
|
|
86
|
+
lineHeight: 14,
|
|
87
|
+
style: {
|
|
88
|
+
fontSize: "var(--text-xs)",
|
|
89
|
+
fontVariantNumeric: "tabular-nums"
|
|
90
|
+
},
|
|
91
|
+
"aria-label": detail,
|
|
92
|
+
className: "fill-muted-foreground tabular-nums",
|
|
93
|
+
children: detail
|
|
94
|
+
})]
|
|
95
|
+
});
|
|
70
96
|
return /* @__PURE__ */ jsxs("text", {
|
|
71
97
|
x,
|
|
72
98
|
y,
|
|
@@ -74,19 +100,32 @@ function fittedTick(data, xKey, xFormatter, detailOf, beside) {
|
|
|
74
100
|
className: cn("recharts-cartesian-axis-tick-value fill-muted-foreground", !detailOf && "font-mono"),
|
|
75
101
|
children: [/* @__PURE__ */ jsx("tspan", {
|
|
76
102
|
x,
|
|
77
|
-
dy:
|
|
78
|
-
...fitTo(name, room, charWidth),
|
|
103
|
+
dy: 12,
|
|
79
104
|
children: name
|
|
80
105
|
}), detail !== null && /* @__PURE__ */ jsx("tspan", {
|
|
81
106
|
x,
|
|
82
107
|
dy: 15,
|
|
83
108
|
className: "tabular-nums",
|
|
84
|
-
...fitTo(detail, room, charWidth),
|
|
85
109
|
children: detail
|
|
86
110
|
})]
|
|
87
111
|
});
|
|
88
112
|
};
|
|
89
113
|
}
|
|
114
|
+
function HorizontalAxis({ data, xKey, xFormatter, xTickDetail, xTicks }) {
|
|
115
|
+
const width = Math.min(240, Math.floor((useChartWidth() ?? 320) * .45));
|
|
116
|
+
return /* @__PURE__ */ jsx(YAxis, {
|
|
117
|
+
dataKey: xKey,
|
|
118
|
+
type: "category",
|
|
119
|
+
width,
|
|
120
|
+
tickLine: false,
|
|
121
|
+
axisLine: false,
|
|
122
|
+
tickMargin: 8,
|
|
123
|
+
tickFormatter: xFormatter,
|
|
124
|
+
interval: 0,
|
|
125
|
+
ticks: xTicks,
|
|
126
|
+
tick: fittedTick(data, xKey, xFormatter, xTickDetail, true, width - 16)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
90
129
|
/**
|
|
91
130
|
* The card, axes, grid, tooltip and legend the three chart cards share. Both
|
|
92
131
|
* axes keep Recharts' default ids: marks find their axes by id, and a page
|
|
@@ -95,127 +134,144 @@ function fittedTick(data, xKey, xFormatter, detailOf, beside) {
|
|
|
95
134
|
*/
|
|
96
135
|
function ChartFrame({ chart: Chart, children, horizontal = false, stackOffset, onClick, 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 }) {
|
|
97
136
|
const { numberLocale } = useLocale();
|
|
98
|
-
const formatValue = valueFormatter ?? new Intl.NumberFormat(numberLocale).format;
|
|
137
|
+
const formatValue = useMemo(() => valueFormatter ?? new Intl.NumberFormat(numberLocale).format, [valueFormatter, numberLocale]);
|
|
99
138
|
const config = Object.fromEntries(series.map(({ key, label, color }) => [key, {
|
|
100
139
|
label,
|
|
101
140
|
color
|
|
102
141
|
}]));
|
|
103
142
|
const crossesZero = data.some((row) => series.some(({ key }) => Number(row[key]) < 0));
|
|
143
|
+
const detailedRows = horizontal && xTickDetail !== void 0;
|
|
144
|
+
const rowCount = (xTicks ?? data).length;
|
|
145
|
+
const plotClass = detailedRows ? "min-h-(--wrapped-chart-height) @min-[24rem]/chart:min-h-0" : void 0;
|
|
146
|
+
let minWidth;
|
|
147
|
+
if (!horizontal && (xTicks || xTickDetail)) {
|
|
148
|
+
const labelledValues = xTicks ?? data.map((row) => row[xKey]);
|
|
149
|
+
let tickWidth = 0;
|
|
150
|
+
for (const value of labelledValues) tickWidth = Math.max(tickWidth, xFormatter(value).length * MONO_WIDTH);
|
|
151
|
+
if (xTickDetail) for (const row of data) tickWidth = Math.max(tickWidth, xTickDetail(row).length * GEIST_WIDTH);
|
|
152
|
+
minWidth = Math.ceil((tickWidth + 12) * labelledValues.length + 64);
|
|
153
|
+
}
|
|
154
|
+
const plotStyle = {
|
|
155
|
+
height: detailedRows ? Math.max(height, rowCount * 72 + 48) : height,
|
|
156
|
+
minWidth,
|
|
157
|
+
...detailedRows ? { "--wrapped-chart-height": `${rowCount * 104 + 48}px` } : {}
|
|
158
|
+
};
|
|
104
159
|
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
105
160
|
/* @__PURE__ */ jsxs(CardHeader, { children: [
|
|
106
161
|
/* @__PURE__ */ jsx(CardTitle, { children: title }),
|
|
107
162
|
description && /* @__PURE__ */ jsx(CardDescription, { children: description }),
|
|
108
163
|
action && /* @__PURE__ */ jsx(CardAction, { children: action })
|
|
109
164
|
] }),
|
|
110
|
-
/* @__PURE__ */ jsx(CardContent, {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
165
|
+
/* @__PURE__ */ jsx(CardContent, {
|
|
166
|
+
className: "@container/chart overflow-x-auto",
|
|
167
|
+
role: minWidth ? "region" : void 0,
|
|
168
|
+
"aria-label": minWidth ? "Chart, scroll horizontally for all labelled values" : void 0,
|
|
169
|
+
tabIndex: minWidth ? 0 : void 0,
|
|
170
|
+
children: placeholder ? /* @__PURE__ */ jsx("div", {
|
|
171
|
+
"data-slot": "chart-placeholder",
|
|
172
|
+
className: cn("flex items-center justify-center", plotClass),
|
|
173
|
+
style: plotStyle,
|
|
174
|
+
children: placeholder
|
|
175
|
+
}) : /* @__PURE__ */ jsx(ChartContainer, {
|
|
176
|
+
config,
|
|
177
|
+
className: cn("aspect-auto w-full", plotClass, onClick && "cursor-pointer"),
|
|
178
|
+
style: plotStyle,
|
|
179
|
+
children: /* @__PURE__ */ jsxs(Chart, {
|
|
180
|
+
data,
|
|
181
|
+
layout: horizontal ? "vertical" : "horizontal",
|
|
182
|
+
stackOffset,
|
|
183
|
+
onClick,
|
|
184
|
+
throttledEvents: onClick ? [] : void 0,
|
|
185
|
+
margin: {
|
|
186
|
+
left: 0,
|
|
187
|
+
right: !horizontal && xTicks ? endRoom(xFormatter, xTicks) : 8
|
|
188
|
+
},
|
|
189
|
+
children: [
|
|
190
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
191
|
+
vertical: horizontal,
|
|
192
|
+
horizontal: !horizontal,
|
|
193
|
+
strokeDasharray: "3 3"
|
|
194
|
+
}),
|
|
195
|
+
horizontal ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
|
|
196
|
+
type: "number",
|
|
197
|
+
hide: true,
|
|
198
|
+
domain: yDomain,
|
|
199
|
+
ticks: yTicks
|
|
200
|
+
}), /* @__PURE__ */ jsx(HorizontalAxis, {
|
|
201
|
+
data,
|
|
202
|
+
xKey,
|
|
203
|
+
xFormatter,
|
|
204
|
+
xTickDetail,
|
|
205
|
+
xTicks
|
|
206
|
+
})] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(XAxis, {
|
|
207
|
+
dataKey: xKey,
|
|
208
|
+
type: xDomain ? "number" : "category",
|
|
209
|
+
domain: xDomain,
|
|
210
|
+
ticks: xTicks,
|
|
211
|
+
height: (xTickDetail ? 40 : 30) + (xLabel ? 18 : 0),
|
|
212
|
+
label: xLabel ? {
|
|
213
|
+
value: xLabel,
|
|
214
|
+
position: "insideBottom",
|
|
215
|
+
className: AXIS_TITLE
|
|
216
|
+
} : void 0,
|
|
217
|
+
tickLine: false,
|
|
218
|
+
axisLine: false,
|
|
219
|
+
tickMargin: 8,
|
|
220
|
+
tickFormatter: (value) => xFormatter(value),
|
|
221
|
+
tick: xTickDetail || xTicks ? fittedTick(data, xKey, xFormatter, xTickDetail, false) : TICK,
|
|
222
|
+
interval: xTickDetail || xTicks ? 0 : void 0
|
|
223
|
+
}), /* @__PURE__ */ jsx(YAxis, {
|
|
224
|
+
tickLine: false,
|
|
225
|
+
axisLine: false,
|
|
226
|
+
width: "auto",
|
|
227
|
+
domain: yDomain,
|
|
228
|
+
ticks: yTicks,
|
|
229
|
+
label: yLabel ? {
|
|
230
|
+
value: yLabel,
|
|
231
|
+
angle: -90,
|
|
232
|
+
position: "insideLeft",
|
|
233
|
+
style: { textAnchor: "middle" },
|
|
234
|
+
className: AXIS_TITLE
|
|
235
|
+
} : void 0,
|
|
236
|
+
tickFormatter: (value) => (yTickFormatter ?? formatValue)(value),
|
|
237
|
+
tick: TICK
|
|
238
|
+
})] }),
|
|
239
|
+
crossesZero && (horizontal ? /* @__PURE__ */ jsx(ReferenceLine, {
|
|
240
|
+
x: 0,
|
|
241
|
+
stroke: "var(--border)"
|
|
242
|
+
}) : /* @__PURE__ */ jsx(ReferenceLine, {
|
|
243
|
+
y: 0,
|
|
244
|
+
stroke: "var(--border)"
|
|
245
|
+
})),
|
|
246
|
+
referenceLines.map(({ label, emphasis = "muted", labelPosition = "insideTopLeft", ...at }) => /* @__PURE__ */ jsx(ReferenceLine, {
|
|
247
|
+
...horizontal ? {
|
|
248
|
+
x: at.y,
|
|
249
|
+
y: at.x
|
|
250
|
+
} : at,
|
|
251
|
+
stroke: emphasis === "strong" ? "var(--foreground)" : "var(--muted-foreground)",
|
|
252
|
+
strokeDasharray: at.y === void 0 ? void 0 : "4 4",
|
|
253
|
+
label: label === void 0 ? void 0 : {
|
|
254
|
+
value: label,
|
|
255
|
+
position: labelPosition,
|
|
256
|
+
className: "fill-muted-foreground text-xs"
|
|
257
|
+
}
|
|
258
|
+
}, `${at.x ?? ""}:${at.y ?? ""}:${label ?? ""}`)),
|
|
259
|
+
/* @__PURE__ */ jsx(ChartTooltip, { content: /* @__PURE__ */ jsx(ChartTooltipContent, {
|
|
260
|
+
labelFormatter: (_, payload) => {
|
|
261
|
+
const row = payload[0]?.payload;
|
|
262
|
+
return row ? xTooltipFormatter(row[xKey]) : null;
|
|
263
|
+
},
|
|
264
|
+
formatter: seriesRow(series, formatValue)
|
|
265
|
+
}) }),
|
|
266
|
+
children,
|
|
267
|
+
/* @__PURE__ */ jsx(ChartLegend, {
|
|
268
|
+
content: /* @__PURE__ */ jsx(ChartLegendContent, {}),
|
|
269
|
+
itemSorter: null
|
|
270
|
+
})
|
|
271
|
+
]
|
|
272
|
+
})
|
|
217
273
|
})
|
|
218
|
-
})
|
|
274
|
+
}),
|
|
219
275
|
footer && /* @__PURE__ */ jsx(CardFooter, {
|
|
220
276
|
className: "flex-col items-start gap-2 text-sm",
|
|
221
277
|
children: footer
|
|
@@ -66,7 +66,7 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
|
|
|
66
66
|
yDomain?: [number, number];
|
|
67
67
|
/** Where the value axis puts its ticks, such as [0, 25, 50, 75, 100]. */
|
|
68
68
|
yTicks?: readonly number[];
|
|
69
|
-
/** Height of the plot and legend, in pixels. */
|
|
69
|
+
/** Height of the plot and legend, in pixels. Sideways bars grow when wrapped tick details need more room. */
|
|
70
70
|
height: number;
|
|
71
71
|
/** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "▲₹1.2L". */
|
|
72
72
|
xTickDetail?: (row: T) => string;
|
package/dist/lib/format.js
CHANGED
|
@@ -2,9 +2,21 @@ import { defaultLocale } from "./locale.js";
|
|
|
2
2
|
import { useLocale } from "../components/siyahi/siyahi-provider.js";
|
|
3
3
|
import { useMemo } from "react";
|
|
4
4
|
//#region src/lib/format.ts
|
|
5
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
6
|
+
const clocks = /* @__PURE__ */ new Map();
|
|
7
|
+
const regions = /* @__PURE__ */ new Map();
|
|
8
|
+
function numberFormat(locale, options) {
|
|
9
|
+
const key = JSON.stringify([locale, options]);
|
|
10
|
+
let formatter = numbers.get(key);
|
|
11
|
+
if (!formatter) {
|
|
12
|
+
formatter = new Intl.NumberFormat(locale, options);
|
|
13
|
+
numbers.set(key, formatter);
|
|
14
|
+
}
|
|
15
|
+
return formatter;
|
|
16
|
+
}
|
|
5
17
|
/** An amount in the locale's currency, whole units by default: ₹1,53,110. Signed figures belong in `Delta`. */
|
|
6
18
|
function money(value, locale = defaultLocale, fractionDigits = 0) {
|
|
7
|
-
return
|
|
19
|
+
return numberFormat(locale.numberLocale, {
|
|
8
20
|
style: "currency",
|
|
9
21
|
currency: locale.currency,
|
|
10
22
|
minimumFractionDigits: fractionDigits,
|
|
@@ -18,8 +30,9 @@ function money(value, locale = defaultLocale, fractionDigits = 0) {
|
|
|
18
30
|
* `Delta`, which adds the arrow.
|
|
19
31
|
*/
|
|
20
32
|
function moneyCompact(value, locale = defaultLocale) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
33
|
+
if (!regions.has(locale.numberLocale)) regions.set(locale.numberLocale, new Intl.Locale(locale.numberLocale).region);
|
|
34
|
+
if (regions.get(locale.numberLocale) === "IN" && Math.abs(value) < 1e5) return money(value, locale);
|
|
35
|
+
return numberFormat(locale.numberLocale, {
|
|
23
36
|
style: "currency",
|
|
24
37
|
currency: locale.currency,
|
|
25
38
|
notation: "compact",
|
|
@@ -28,11 +41,11 @@ function moneyCompact(value, locale = defaultLocale) {
|
|
|
28
41
|
}
|
|
29
42
|
/** A whole count with the locale's grouping: 1,53,110 in en-IN. */
|
|
30
43
|
function count(value, locale = defaultLocale) {
|
|
31
|
-
return
|
|
44
|
+
return numberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
|
|
32
45
|
}
|
|
33
46
|
/** A fraction as a percentage: 0.1234 is 12.3%. */
|
|
34
47
|
function pct(value, locale = defaultLocale, fractionDigits = 1) {
|
|
35
|
-
return
|
|
48
|
+
return numberFormat(locale.numberLocale, {
|
|
36
49
|
style: "percent",
|
|
37
50
|
minimumFractionDigits: fractionDigits,
|
|
38
51
|
maximumFractionDigits: fractionDigits
|
|
@@ -40,19 +53,25 @@ function pct(value, locale = defaultLocale, fractionDigits = 1) {
|
|
|
40
53
|
}
|
|
41
54
|
/** A 24-hour time in the locale's zone, with the zone written: 14:30 IST. */
|
|
42
55
|
function time(value, locale = defaultLocale, { withDate = false } = {}) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...date,
|
|
56
|
+
const options = {
|
|
57
|
+
...withDate ? {
|
|
58
|
+
day: "numeric",
|
|
59
|
+
month: "short",
|
|
60
|
+
year: "numeric"
|
|
61
|
+
} : {},
|
|
50
62
|
hour: "2-digit",
|
|
51
63
|
minute: "2-digit",
|
|
52
64
|
hourCycle: "h23",
|
|
53
65
|
timeZone: locale.timeZone,
|
|
54
66
|
timeZoneName: "short"
|
|
55
|
-
}
|
|
67
|
+
};
|
|
68
|
+
const key = JSON.stringify([locale.numberLocale, options]);
|
|
69
|
+
let formatter = clocks.get(key);
|
|
70
|
+
if (!formatter) {
|
|
71
|
+
formatter = new Intl.DateTimeFormat(locale.numberLocale, options);
|
|
72
|
+
clocks.set(key, formatter);
|
|
73
|
+
}
|
|
74
|
+
return formatter.format(new Date(value));
|
|
56
75
|
}
|
|
57
76
|
/** The formatters above, bound to the locale of the nearest `SiyahiProvider`. */
|
|
58
77
|
function useFormat() {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { KeyboardEventHandler, RefObject } from "react";
|
|
2
|
+
//#region src/lib/heatmap-navigation.d.ts
|
|
3
|
+
/** One tab stop per heatmap; arrows move between its actual drawn cells. */
|
|
4
|
+
export declare function useHeatmapNavigation(): {
|
|
5
|
+
ref: RefObject<HTMLDivElement | null>;
|
|
6
|
+
onKeyDown: KeyboardEventHandler<HTMLDivElement>;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|