@siyahi/ui 0.1.9-next.3 → 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 +170 -0
- package/README.md +94 -177
- package/THIRD_PARTY_NOTICES.md +11 -11
- package/agents/examples/accordion.tsx +39 -0
- package/agents/examples/alert.tsx +24 -0
- package/agents/examples/app-sidebar.tsx +40 -0
- package/agents/examples/avatar.tsx +24 -0
- package/agents/examples/badge.tsx +26 -0
- package/agents/examples/bar-list-labels.tsx +22 -0
- package/agents/examples/bar-list-sides.tsx +40 -0
- package/agents/examples/bar-list.tsx +24 -0
- package/agents/examples/breadcrumb.tsx +28 -0
- package/agents/examples/button-states.tsx +23 -0
- package/agents/examples/button.tsx +21 -0
- package/agents/examples/calendar.tsx +11 -0
- package/agents/examples/callout.tsx +33 -0
- package/agents/examples/card.tsx +36 -0
- package/agents/examples/change-bar.tsx +104 -0
- package/agents/examples/change-grid-money.tsx +36 -0
- package/agents/examples/change-grid.tsx +42 -0
- package/agents/examples/chart-area-log.tsx +45 -0
- package/agents/examples/chart-area.tsx +39 -0
- package/agents/examples/chart-bar-flows.tsx +32 -0
- package/agents/examples/chart-bar-hourly.tsx +28 -0
- package/agents/examples/chart-bar-losses.tsx +29 -0
- package/agents/examples/chart-bar-teams.tsx +72 -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-forecast.tsx +36 -0
- package/agents/examples/chart-line-log.tsx +39 -0
- package/agents/examples/chart-line-quiet.tsx +114 -0
- package/agents/examples/chart-line-timeline.tsx +41 -0
- package/agents/examples/chart-line.tsx +57 -0
- package/agents/examples/chart-panels.tsx +94 -0
- package/agents/examples/code-block.tsx +40 -0
- package/agents/examples/collapsible.tsx +18 -0
- package/agents/examples/command.tsx +29 -0
- package/agents/examples/context-menu.tsx +17 -0
- package/agents/examples/copy-button.tsx +8 -0
- package/agents/examples/data-bar.tsx +108 -0
- package/agents/examples/data-table-compact.tsx +96 -0
- package/agents/examples/data-table-filters.tsx +103 -0
- package/agents/examples/data-table.tsx +99 -0
- package/agents/examples/delta.tsx +18 -0
- package/agents/examples/dialog.tsx +28 -0
- package/agents/examples/distribution-bar-budget.tsx +30 -0
- package/agents/examples/distribution-bar-tug.tsx +74 -0
- package/agents/examples/drawdown-chart.tsx +35 -0
- package/agents/examples/dropdown-menu.tsx +31 -0
- package/agents/examples/field.tsx +108 -0
- package/agents/examples/formatters.tsx +42 -0
- package/agents/examples/heatmap-calendar-signed.tsx +67 -0
- package/agents/examples/heatmap-calendar.tsx +62 -0
- package/agents/examples/heatmap-status.tsx +61 -0
- package/agents/examples/heatmap-weekday.tsx +72 -0
- package/agents/examples/histogram-chart.tsx +28 -0
- package/agents/examples/input-states.tsx +23 -0
- package/agents/examples/input.tsx +17 -0
- package/agents/examples/local-time.tsx +14 -0
- package/agents/examples/metric-strip.tsx +46 -0
- package/agents/examples/overview-dashboard.tsx +180 -0
- package/agents/examples/page-hero.tsx +38 -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 +29 -0
- package/agents/examples/progress.tsx +10 -0
- package/agents/examples/range-position-table.tsx +101 -0
- package/agents/examples/range-position.tsx +55 -0
- package/agents/examples/scatter-chart-quadrants.tsx +63 -0
- package/agents/examples/scatter-chart-scales.tsx +46 -0
- package/agents/examples/scatter-chart.tsx +78 -0
- package/agents/examples/scroll-area.tsx +22 -0
- package/agents/examples/section-cards-select.tsx +28 -0
- package/agents/examples/section-cards-trend.tsx +32 -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/slope-chart-market.tsx +33 -0
- package/agents/examples/slope-chart.tsx +26 -0
- package/agents/examples/stale-blocks.tsx +56 -0
- package/agents/examples/surfaces.tsx +116 -0
- package/agents/examples/table.tsx +60 -0
- package/agents/examples/tabs.tsx +16 -0
- package/agents/examples/target-rows.tsx +74 -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 +93 -0
- package/agents/examples/waterfall-chart-stores.tsx +27 -0
- package/agents/examples/waterfall-chart.tsx +37 -0
- package/agents/manifest.json +1391 -0
- package/agents/report-rules.json +7 -0
- package/dist/components/registry/charts/area-chart-card.d.ts +7 -3
- package/dist/components/registry/charts/area-chart-card.js +56 -10
- package/dist/components/registry/charts/bar-chart-card.d.ts +1 -1
- package/dist/components/registry/charts/bar-chart-card.js +20 -22
- package/dist/components/registry/charts/line-chart-card.d.ts +3 -3
- package/dist/components/registry/charts/line-chart-card.js +44 -9
- package/dist/components/registry/dashboard-01/section-cards.d.ts +6 -0
- package/dist/components/registry/dashboard-01/section-cards.js +51 -35
- package/dist/components/registry/dashboardcn/bar-list.js +17 -10
- package/dist/components/registry/dashboardcn/distribution-bar.js +6 -4
- package/dist/components/registry/dashboardcn/kpi-row-card.d.ts +29 -0
- package/dist/components/registry/dashboardcn/kpi-row-card.js +81 -0
- package/dist/components/registry/dashboardcn/sparkline.d.ts +22 -0
- package/dist/components/registry/dashboardcn/sparkline.js +117 -0
- package/dist/components/registry/heatmap/calendar-heatmap.d.ts +11 -3
- package/dist/components/registry/heatmap/calendar-heatmap.js +164 -45
- package/dist/components/registry/heatmap/status-heatmap.d.ts +5 -3
- package/dist/components/registry/heatmap/status-heatmap.js +115 -54
- package/dist/components/registry/heatmap/weekday-heatmap.d.ts +9 -3
- package/dist/components/registry/heatmap/weekday-heatmap.js +83 -53
- package/dist/components/registry/niko-table/core/data-table-context.d.ts +18 -1
- package/dist/components/registry/niko-table/core/data-table-context.js +7 -1
- package/dist/components/registry/niko-table/core/data-table-grouped-row.js +3 -1
- package/dist/components/registry/niko-table/core/data-table-root.js +39 -4
- package/dist/components/registry/niko-table/core/data-table-structure.js +17 -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-faceted-filter.js +5 -5
- 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 +3 -3
- package/dist/components/registry/niko-table/types/index.d.ts +9 -0
- package/dist/components/registry/niko-table/ui/table.js +2 -2
- package/dist/components/registry/sidebar-07/app-sidebar.d.ts +1 -1
- package/dist/components/registry/sidebar-07/app-sidebar.js +3 -1
- package/dist/components/registry/sidebar-07/nav-main.d.ts +2 -1
- package/dist/components/registry/sidebar-07/nav-main.js +16 -3
- package/dist/components/siyahi/callout.d.ts +17 -0
- package/dist/components/siyahi/callout.js +34 -0
- package/dist/components/siyahi/change-bar.d.ts +21 -0
- package/dist/components/siyahi/change-bar.js +35 -0
- package/dist/components/siyahi/change-grid.d.ts +31 -0
- package/dist/components/siyahi/change-grid.js +74 -0
- package/dist/components/siyahi/data-bar.d.ts +19 -0
- package/dist/components/siyahi/data-bar.js +29 -0
- package/dist/components/siyahi/data-table-compact-columns-toggle.d.ts +4 -0
- package/dist/components/siyahi/data-table-compact-columns-toggle.js +19 -0
- package/dist/components/siyahi/data-table-footnote.d.ts +12 -0
- package/dist/components/siyahi/data-table-footnote.js +35 -0
- package/dist/components/siyahi/default-sidebar-open.d.ts +8 -0
- package/dist/components/siyahi/default-sidebar-open.js +13 -0
- package/dist/components/siyahi/delta.d.ts +5 -1
- package/dist/components/siyahi/delta.js +14 -9
- package/dist/components/siyahi/drawdown-chart.d.ts +34 -0
- package/dist/components/siyahi/drawdown-chart.js +102 -0
- package/dist/components/siyahi/histogram-chart.d.ts +57 -0
- package/dist/components/siyahi/histogram-chart.js +134 -0
- package/dist/components/siyahi/page-hero.d.ts +23 -0
- package/dist/components/siyahi/page-hero.js +43 -0
- package/dist/components/siyahi/page-section.d.ts +4 -1
- package/dist/components/siyahi/page-section.js +6 -3
- package/dist/components/siyahi/page-toc.d.ts +20 -0
- package/dist/components/siyahi/page-toc.js +76 -0
- package/dist/components/siyahi/panel-grid.d.ts +24 -0
- package/dist/components/siyahi/panel-grid.js +37 -0
- package/dist/components/siyahi/quote.d.ts +12 -0
- package/dist/components/siyahi/quote.js +23 -0
- package/dist/components/siyahi/range-position.d.ts +24 -0
- package/dist/components/siyahi/range-position.js +105 -0
- package/dist/components/siyahi/scale-toggle.d.ts +32 -0
- package/dist/components/siyahi/scale-toggle.js +61 -0
- package/dist/components/siyahi/scatter-chart.d.ts +69 -0
- package/dist/components/siyahi/scatter-chart.js +395 -0
- package/dist/components/siyahi/siyahi-provider.d.ts +15 -2
- package/dist/components/siyahi/siyahi-provider.js +17 -4
- package/dist/components/siyahi/slope-chart.d.ts +35 -0
- package/dist/components/siyahi/slope-chart.js +159 -0
- package/dist/components/siyahi/stale-note.d.ts +9 -0
- package/dist/components/siyahi/stale-note.js +12 -0
- package/dist/components/siyahi/status-alert.d.ts +15 -0
- package/dist/components/siyahi/status-alert.js +29 -0
- package/dist/components/siyahi/status-badge.d.ts +15 -0
- package/dist/components/siyahi/status-badge.js +24 -0
- package/dist/components/siyahi/target-rows.d.ts +34 -0
- package/dist/components/siyahi/target-rows.js +97 -0
- package/dist/components/siyahi/tone-progress.d.ts +1 -1
- package/dist/components/siyahi/tone-progress.js +1 -1
- package/dist/components/siyahi/treemap.d.ts +8 -5
- package/dist/components/siyahi/treemap.js +80 -34
- package/dist/components/siyahi/waterfall-chart.d.ts +51 -0
- package/dist/components/siyahi/waterfall-chart.js +147 -0
- package/dist/components/ui/accordion.js +5 -5
- package/dist/components/ui/alert.js +2 -2
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/breadcrumb.js +2 -2
- package/dist/components/ui/button.js +10 -10
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/card.js +4 -4
- package/dist/components/ui/chart.js +1 -1
- package/dist/components/ui/code-block.js +13 -12
- package/dist/components/ui/command.js +7 -6
- package/dist/components/ui/context-menu.js +7 -7
- package/dist/components/ui/dialog.js +5 -5
- package/dist/components/ui/dropdown-menu.js +8 -8
- package/dist/components/ui/field.d.ts +26 -0
- package/dist/components/ui/field.js +109 -0
- package/dist/components/ui/input-group.js +11 -11
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.js +13 -0
- package/dist/components/ui/popover.js +3 -3
- package/dist/components/ui/progress.js +1 -1
- package/dist/components/ui/select.js +6 -6
- package/dist/components/ui/sheet.js +5 -5
- package/dist/components/ui/sidebar.js +17 -18
- package/dist/components/ui/skeleton.js +1 -1
- package/dist/components/ui/slider.js +2 -2
- package/dist/components/ui/table.js +2 -2
- package/dist/components/ui/tabs.js +2 -2
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/toggle-group.js +2 -2
- package/dist/components/ui/toggle.js +4 -4
- package/dist/components/ui/tooltip.js +2 -2
- package/dist/index.d.ts +35 -8
- package/dist/index.js +30 -4
- package/dist/lib/chart-frame.d.ts +51 -7
- package/dist/lib/chart-frame.js +390 -133
- package/dist/lib/chart-marks.d.ts +83 -0
- package/dist/lib/chart-marks.js +216 -0
- package/dist/lib/chart-series.d.ts +42 -10
- package/dist/lib/compact-columns.d.ts +19 -0
- package/dist/lib/compact-columns.js +55 -0
- package/dist/lib/fit-text.d.ts +6 -0
- package/dist/lib/fit-text.js +11 -0
- package/dist/lib/format.d.ts +8 -3
- package/dist/lib/format.js +55 -17
- package/dist/lib/heat.d.ts +7 -0
- package/dist/lib/heat.js +14 -0
- package/dist/lib/heatmap-navigation.d.ts +8 -0
- package/dist/lib/heatmap-navigation.js +49 -0
- package/dist/lib/scatter.d.ts +88 -0
- package/dist/lib/scatter.js +179 -0
- package/dist/lib/ticks.d.ts +27 -0
- package/dist/lib/ticks.js +67 -0
- package/dist/lib/tone.d.ts +36 -0
- package/dist/lib/tone.js +35 -0
- package/dist/lib/treemap.d.ts +0 -5
- package/dist/lib/treemap.js +1 -11
- package/dist/lib/use-element-width.d.ts +5 -0
- package/dist/lib/use-element-width.js +15 -0
- package/package.json +5 -1
- package/registry.lock.json +16 -0
- package/report.css +63 -47
- package/styles/siyahi.css +708 -55
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Tone } from "../../lib/tone.js";
|
|
2
|
+
import { Alert } from "../ui/alert.js";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
4
|
+
//#region src/components/siyahi/status-alert.d.ts
|
|
5
|
+
export interface StatusAlertProps extends Omit<ComponentProps<typeof Alert>, "variant"> {
|
|
6
|
+
/** neutral for information, warning for a caution, negative for an error, positive for a success. */
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A notice the reader must not miss: the whole card takes its tone, a faint wash inside a border of the same colour,
|
|
11
|
+
* with the tone's icon. Title and description stay in the text colours so they read on the wash. Never mark a notice
|
|
12
|
+
* with a single coloured edge. Put an AlertTitle and an AlertDescription inside.
|
|
13
|
+
*/
|
|
14
|
+
export declare function StatusAlert({ tone, className, children, ...props }: StatusAlertProps): import("react").JSX.Element;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Alert } from "../ui/alert.js";
|
|
2
|
+
import { TONES } from "../../lib/tone.js";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
import { CircleAlertIcon, CircleCheckIcon, InfoIcon, LightbulbIcon, TriangleAlertIcon } from "lucide-react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/siyahi/status-alert.tsx
|
|
7
|
+
const ICONS = {
|
|
8
|
+
neutral: InfoIcon,
|
|
9
|
+
accent: LightbulbIcon,
|
|
10
|
+
warning: TriangleAlertIcon,
|
|
11
|
+
positive: CircleCheckIcon,
|
|
12
|
+
negative: CircleAlertIcon
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A notice the reader must not miss: the whole card takes its tone, a faint wash inside a border of the same colour,
|
|
16
|
+
* with the tone's icon. Title and description stay in the text colours so they read on the wash. Never mark a notice
|
|
17
|
+
* with a single coloured edge. Put an AlertTitle and an AlertDescription inside.
|
|
18
|
+
*/
|
|
19
|
+
function StatusAlert({ tone = "neutral", className, children, ...props }) {
|
|
20
|
+
const Icon = ICONS[tone];
|
|
21
|
+
return /* @__PURE__ */ jsxs(Alert, {
|
|
22
|
+
"data-tone": tone,
|
|
23
|
+
className: cn("gap-x-3 px-4 py-3", TONES[tone].box, className),
|
|
24
|
+
...props,
|
|
25
|
+
children: [/* @__PURE__ */ jsx(Icon, { className: TONES[tone].ink }), children]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { StatusAlert };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Tone } from "../../lib/tone.js";
|
|
2
|
+
import { Badge } from "../ui/badge.js";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
4
|
+
//#region src/components/siyahi/status-badge.d.ts
|
|
5
|
+
export interface StatusBadgeProps extends Omit<ComponentProps<typeof Badge>, "variant"> {
|
|
6
|
+
/** positive for done or paid, warning for pending, negative for failed, accent for new, neutral for a draft. */
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A state, such as Paid, Pending or Failed: the badge takes its tone's wash and border, and a dot in the tone's
|
|
11
|
+
* colour marks it. The label stays in the text colour, since small coloured text on its own wash falls under 4.5:1.
|
|
12
|
+
* Categories (a team, a region) are not states: they stay outline badges.
|
|
13
|
+
*/
|
|
14
|
+
export declare function StatusBadge({ tone, className, children, ...props }: StatusBadgeProps): import("react").JSX.Element;
|
|
15
|
+
//#endregion
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Badge } from "../ui/badge.js";
|
|
2
|
+
import { TONES } from "../../lib/tone.js";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/siyahi/status-badge.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A state, such as Paid, Pending or Failed: the badge takes its tone's wash and border, and a dot in the tone's
|
|
8
|
+
* colour marks it. The label stays in the text colour, since small coloured text on its own wash falls under 4.5:1.
|
|
9
|
+
* Categories (a team, a region) are not states: they stay outline badges.
|
|
10
|
+
*/
|
|
11
|
+
function StatusBadge({ tone = "neutral", className, children, ...props }) {
|
|
12
|
+
return /* @__PURE__ */ jsxs(Badge, {
|
|
13
|
+
variant: "outline",
|
|
14
|
+
"data-tone": tone,
|
|
15
|
+
className: cn(TONES[tone].box, className),
|
|
16
|
+
...props,
|
|
17
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
18
|
+
"aria-hidden": "true",
|
|
19
|
+
className: cn("size-1.5 rounded-full", TONES[tone].dot)
|
|
20
|
+
}), children]
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { StatusBadge };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/siyahi/target-rows.d.ts
|
|
3
|
+
/** Where a row stands against its target. A warning is for a real problem, not merely a miss. */
|
|
4
|
+
export interface TargetStatus {
|
|
5
|
+
kind: "met" | "missed" | "warning";
|
|
6
|
+
/** A few words: "On target", "2.1 pts under", "Rebalance". */
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TargetRow {
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** A muted line under the label, such as the asset class. */
|
|
12
|
+
detail?: ReactNode;
|
|
13
|
+
value: number;
|
|
14
|
+
target: number;
|
|
15
|
+
/** Qualitative ranges behind the bar as [from, to]; each later band draws a grey step darker, so nest them. */
|
|
16
|
+
bands?: [number, number][];
|
|
17
|
+
status?: TargetStatus;
|
|
18
|
+
/** Tells rows apart when labels are not plain unique text. */
|
|
19
|
+
key?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TargetRowsProps {
|
|
22
|
+
rows: TargetRow[];
|
|
23
|
+
/** The scale every row shares; by default 0 to the largest value, target or band edge. */
|
|
24
|
+
domain?: [number, number];
|
|
25
|
+
/** Writes the value and the target; useFormat().count by default. */
|
|
26
|
+
valueFormatter?: (value: number) => string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Bullet rows: each row's label, value and status beside a track where the value is a solid bar, the target a tall
|
|
31
|
+
* tick and the qualitative ranges grey bands. On a narrow card the label and value stack above the track.
|
|
32
|
+
*/
|
|
33
|
+
export declare function TargetRows({ rows, domain, valueFormatter, className }: TargetRowsProps): import("react").JSX.Element;
|
|
34
|
+
//#endregion
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useFormat } from "../../lib/format.js";
|
|
2
|
+
import { cn } from "cn";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/siyahi/target-rows.tsx
|
|
5
|
+
const GLYPH = {
|
|
6
|
+
met: "✓",
|
|
7
|
+
missed: "—",
|
|
8
|
+
warning: "⚠"
|
|
9
|
+
};
|
|
10
|
+
const BAND_DEPTH = [
|
|
11
|
+
14,
|
|
12
|
+
26,
|
|
13
|
+
38
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Bullet rows: each row's label, value and status beside a track where the value is a solid bar, the target a tall
|
|
17
|
+
* tick and the qualitative ranges grey bands. On a narrow card the label and value stack above the track.
|
|
18
|
+
*/
|
|
19
|
+
function TargetRows({ rows, domain, valueFormatter, className }) {
|
|
20
|
+
const f = useFormat();
|
|
21
|
+
const write = valueFormatter ?? f.count;
|
|
22
|
+
const [low, high] = domain ?? [0, Math.max(...rows.flatMap((row) => [
|
|
23
|
+
row.value,
|
|
24
|
+
row.target,
|
|
25
|
+
...row.bands?.flat() ?? []
|
|
26
|
+
]))];
|
|
27
|
+
const at = (value) => `${(Math.min(1, Math.max(0, (value - low) / (high - low || 1))) * 100).toFixed(2)}%`;
|
|
28
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
29
|
+
"data-slot": "target-rows",
|
|
30
|
+
className: cn("@container/target divide-y", className),
|
|
31
|
+
children: rows.map((row, index) => /* @__PURE__ */ jsxs("li", {
|
|
32
|
+
"data-slot": "target-row",
|
|
33
|
+
className: "flex flex-col gap-3 py-3 @lg/target:flex-row @lg/target:items-center @lg/target:gap-6",
|
|
34
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: "flex min-w-0 flex-col text-sm @lg/target:w-64 @lg/target:shrink-0",
|
|
36
|
+
children: [
|
|
37
|
+
/* @__PURE__ */ jsxs("div", {
|
|
38
|
+
className: "flex items-baseline justify-between gap-3",
|
|
39
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
40
|
+
className: "truncate font-medium",
|
|
41
|
+
children: row.label
|
|
42
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "shrink-0 text-base font-semibold tabular-nums",
|
|
44
|
+
children: write(row.value)
|
|
45
|
+
})]
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", {
|
|
48
|
+
className: "flex justify-between gap-3 text-muted-foreground",
|
|
49
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
50
|
+
className: "truncate",
|
|
51
|
+
children: row.detail
|
|
52
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
53
|
+
className: "shrink-0 tabular-nums",
|
|
54
|
+
children: ["Target ", write(row.target)]
|
|
55
|
+
})]
|
|
56
|
+
}),
|
|
57
|
+
row.status && /* @__PURE__ */ jsxs("span", {
|
|
58
|
+
"data-slot": "target-status",
|
|
59
|
+
"data-kind": row.status.kind,
|
|
60
|
+
className: "mt-1 flex gap-1.5",
|
|
61
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
62
|
+
"aria-hidden": "true",
|
|
63
|
+
className: cn("w-4 text-center", row.status.kind === "warning" && "text-warning", row.status.kind === "missed" && "text-muted-foreground"),
|
|
64
|
+
children: GLYPH[row.status.kind]
|
|
65
|
+
}), row.status.label]
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
69
|
+
"aria-hidden": "true",
|
|
70
|
+
className: "relative h-5 rounded-sm bg-muted @lg/target:flex-1",
|
|
71
|
+
children: [
|
|
72
|
+
row.bands?.map(([from, to], step) => /* @__PURE__ */ jsx("div", {
|
|
73
|
+
"data-slot": "target-band",
|
|
74
|
+
className: "absolute inset-y-0",
|
|
75
|
+
style: {
|
|
76
|
+
left: at(Math.min(from, to)),
|
|
77
|
+
right: `calc(100% - ${at(Math.max(from, to))})`,
|
|
78
|
+
background: `color-mix(in oklab, var(--foreground) ${BAND_DEPTH[Math.min(step, BAND_DEPTH.length - 1)]}%, var(--muted))`
|
|
79
|
+
}
|
|
80
|
+
}, `${from}:${to}`)),
|
|
81
|
+
/* @__PURE__ */ jsx("div", {
|
|
82
|
+
"data-slot": "target-value",
|
|
83
|
+
className: "absolute inset-y-1.5 left-0 bg-foreground",
|
|
84
|
+
style: { width: at(row.value) }
|
|
85
|
+
}),
|
|
86
|
+
/* @__PURE__ */ jsx("div", {
|
|
87
|
+
"data-slot": "target-tick",
|
|
88
|
+
className: "absolute -inset-y-1 w-0.5 -translate-x-1/2 bg-foreground ring-1 ring-card",
|
|
89
|
+
style: { left: at(row.target) }
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
})]
|
|
93
|
+
}, row.key ?? (typeof row.label === "string" ? row.label : index)))
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { TargetRows };
|
|
@@ -9,7 +9,7 @@ export interface ToneProgressProps extends ComponentProps<typeof Progress> {
|
|
|
9
9
|
/** The meaning colour of the bar: a gain's share in --positive, a loss's in --negative. */
|
|
10
10
|
tone: keyof typeof TONE;
|
|
11
11
|
}
|
|
12
|
-
/** shadcn's Progress with its bar in a meaning colour, such as each
|
|
12
|
+
/** shadcn's Progress with its bar in a meaning colour, such as each channel's share of profit or loss. */
|
|
13
13
|
export declare function ToneProgress({ tone, className, ...props }: ToneProgressProps): import("react").JSX.Element;
|
|
14
14
|
//#endregion
|
|
15
15
|
export {};
|
|
@@ -6,7 +6,7 @@ const TONE = {
|
|
|
6
6
|
positive: "**:data-[slot=progress-indicator]:bg-positive",
|
|
7
7
|
negative: "**:data-[slot=progress-indicator]:bg-negative"
|
|
8
8
|
};
|
|
9
|
-
/** shadcn's Progress with its bar in a meaning colour, such as each
|
|
9
|
+
/** shadcn's Progress with its bar in a meaning colour, such as each channel's share of profit or loss. */
|
|
10
10
|
function ToneProgress({ tone, className, ...props }) {
|
|
11
11
|
return /* @__PURE__ */ jsx(Progress, {
|
|
12
12
|
"data-tone": tone,
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
//#region src/components/siyahi/treemap.d.ts
|
|
2
2
|
export interface TreemapItem {
|
|
3
3
|
name: string;
|
|
4
|
-
/** Tile area, such as
|
|
4
|
+
/** Tile area, such as a city's sales. Zero, negative and non-finite weights draw no tile. */
|
|
5
5
|
weight: number;
|
|
6
6
|
/** Signed change as a fraction; sets the heat. `null` when unknown: a muted tile marked "—". */
|
|
7
7
|
change: number | null;
|
|
8
|
-
/** Unique key when names repeat, such as one
|
|
8
|
+
/** Unique key when names repeat, such as one city sold to online and in stores. Defaults to `name`. */
|
|
9
9
|
key?: string;
|
|
10
|
-
/** Tiles of one group, such as a
|
|
10
|
+
/** Tiles of one group, such as a region, sit together under the group's name, even a group of one. */
|
|
11
11
|
group?: string;
|
|
12
12
|
}
|
|
13
13
|
export interface TreemapProps<T extends TreemapItem> {
|
|
@@ -15,7 +15,10 @@ export interface TreemapProps<T extends TreemapItem> {
|
|
|
15
15
|
height: number;
|
|
16
16
|
/** ±range: the change, as a fraction, that takes the deepest heat step. Default 0.05 (±5%); a range that is not a positive number uses the default. */
|
|
17
17
|
range?: number;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Gets the item under a mouse, and null when it moves onto a group's name or leaves the map. A tap fires no hover:
|
|
20
|
+
* on a touch screen it selects, so the page shows the tile in place rather than in a card at the finger.
|
|
21
|
+
*/
|
|
19
22
|
onHover?: (item: T | null) => void;
|
|
20
23
|
/** The selected item, matched by key, whose tile carries a ring. Pass back what onSelect gave. */
|
|
21
24
|
selected?: T | null;
|
|
@@ -26,7 +29,7 @@ export interface TreemapProps<T extends TreemapItem> {
|
|
|
26
29
|
*/
|
|
27
30
|
onSelect?: (item: T | null) => void;
|
|
28
31
|
}
|
|
29
|
-
/**
|
|
32
|
+
/** Heat map of parts of a whole: area by weight, colour by signed change, optionally grouped; hand-drawn SVG. */
|
|
30
33
|
export declare function Treemap<T extends TreemapItem>({ items, height, range, onHover, selected, onSelect }: TreemapProps<T>): import("react").JSX.Element;
|
|
31
34
|
export interface TreemapLegendProps {
|
|
32
35
|
/** The Treemap's own range, so each swatch matches its tiles. */
|
|
@@ -1,20 +1,18 @@
|
|
|
1
|
+
import { useElementWidth } from "../../lib/use-element-width.js";
|
|
1
2
|
import { useFormat } from "../../lib/format.js";
|
|
2
|
-
import { heat, heatFill, heatRange
|
|
3
|
+
import { heat, heatFill, heatRange } from "../../lib/heat.js";
|
|
4
|
+
import { fit, textWidth } from "../../lib/fit-text.js";
|
|
5
|
+
import { layout } from "../../lib/treemap.js";
|
|
3
6
|
import { cn } from "cn";
|
|
4
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { useEffect, useEffectEvent,
|
|
8
|
+
import { useEffect, useEffectEvent, useMemo, useState } from "react";
|
|
6
9
|
//#region src/components/siyahi/treemap.tsx
|
|
7
|
-
/**
|
|
10
|
+
/** Heat map of parts of a whole: area by weight, colour by signed change, optionally grouped; hand-drawn SVG. */
|
|
8
11
|
function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
9
|
-
const ref =
|
|
10
|
-
const [
|
|
12
|
+
const [ref, width] = useElementWidth();
|
|
13
|
+
const [focusedKey, setFocusedKey] = useState(null);
|
|
11
14
|
const selectedKey = selected ? selected.key ?? selected.name : null;
|
|
12
15
|
const { pct } = useFormat();
|
|
13
|
-
useLayoutEffect(() => {
|
|
14
|
-
const observer = new ResizeObserver(([entry]) => setWidth(entry?.contentRect.width ?? 0));
|
|
15
|
-
if (ref.current) observer.observe(ref.current);
|
|
16
|
-
return () => observer.disconnect();
|
|
17
|
-
}, []);
|
|
18
16
|
const clearOutside = useEffectEvent((event) => {
|
|
19
17
|
const tile = event.target instanceof Element ? event.target.closest("[data-slot=\"treemap-tile\"]") : null;
|
|
20
18
|
if (!ref.current?.contains(tile)) onSelect?.(null);
|
|
@@ -25,8 +23,13 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
25
23
|
document.addEventListener("pointerdown", onPointerDown);
|
|
26
24
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
27
25
|
}, [selectedKey]);
|
|
28
|
-
const { groups, tiles } = layout(items, width, height)
|
|
29
|
-
|
|
26
|
+
const { groups, tiles } = useMemo(() => layout(items, width, height), [
|
|
27
|
+
items,
|
|
28
|
+
width,
|
|
29
|
+
height
|
|
30
|
+
]);
|
|
31
|
+
const focusKey = tiles.some(({ item }) => (item.key ?? item.name) === focusedKey) ? focusedKey : tiles[0]?.item.key ?? tiles[0]?.item.name;
|
|
32
|
+
const change = (value) => value === null ? "—" : `${value > 0 ? "+" : value < 0 ? "−" : ""}${pct(Math.abs(value))}`;
|
|
30
33
|
return /* @__PURE__ */ jsx("div", {
|
|
31
34
|
ref,
|
|
32
35
|
"data-slot": "treemap",
|
|
@@ -34,32 +37,72 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
34
37
|
children: /* @__PURE__ */ jsxs("svg", {
|
|
35
38
|
width,
|
|
36
39
|
height,
|
|
37
|
-
role: "img",
|
|
38
|
-
"aria-label": "Treemap
|
|
40
|
+
role: onSelect ? "group" : "img",
|
|
41
|
+
"aria-label": "Treemap by weight and change",
|
|
39
42
|
className: "text-xs",
|
|
40
|
-
onPointerLeave: () => onHover?.(null),
|
|
43
|
+
onPointerLeave: (event) => event.pointerType === "mouse" && onHover?.(null),
|
|
41
44
|
children: [groups.map(({ group, ...box }) => /* @__PURE__ */ jsxs("svg", {
|
|
42
45
|
...box,
|
|
43
46
|
"data-slot": "treemap-group",
|
|
44
|
-
onPointerEnter: () => onHover?.(null),
|
|
45
|
-
children: [
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
onPointerEnter: (event) => event.pointerType === "mouse" && onHover?.(null),
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx("rect", {
|
|
50
|
+
width: "100%",
|
|
51
|
+
height: "100%",
|
|
52
|
+
fill: "none",
|
|
53
|
+
pointerEvents: "all"
|
|
54
|
+
}),
|
|
55
|
+
/* @__PURE__ */ jsx("title", { children: group }),
|
|
56
|
+
textWidth(group) <= box.width - 4 && /* @__PURE__ */ jsx("text", {
|
|
57
|
+
x: 2,
|
|
58
|
+
y: 12,
|
|
59
|
+
className: "fill-muted-foreground font-medium",
|
|
60
|
+
children: group
|
|
61
|
+
})
|
|
62
|
+
]
|
|
63
|
+
}, group)), tiles.map(({ item, ...box }, index) => {
|
|
57
64
|
const key = item.key ?? item.name;
|
|
58
65
|
return /* @__PURE__ */ jsxs("svg", {
|
|
59
66
|
...box,
|
|
60
67
|
"data-slot": "treemap-tile",
|
|
61
68
|
"data-selected": key === selectedKey || void 0,
|
|
62
|
-
|
|
69
|
+
role: onSelect ? "button" : void 0,
|
|
70
|
+
tabIndex: onSelect ? key === focusKey ? 0 : -1 : void 0,
|
|
71
|
+
"aria-label": `${item.name}${item.group ? `, ${item.group}` : ""}, ${change(item.change)}`,
|
|
72
|
+
"aria-pressed": onSelect ? key === selectedKey : void 0,
|
|
73
|
+
onFocus: () => setFocusedKey(key),
|
|
74
|
+
onKeyDown: (event) => {
|
|
75
|
+
if (!onSelect) return;
|
|
76
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
onSelect(key === selectedKey ? null : item);
|
|
79
|
+
} else if (event.key === "Escape") onSelect(null);
|
|
80
|
+
else {
|
|
81
|
+
let next;
|
|
82
|
+
switch (event.key) {
|
|
83
|
+
case "Home":
|
|
84
|
+
next = 0;
|
|
85
|
+
break;
|
|
86
|
+
case "End":
|
|
87
|
+
next = tiles.length - 1;
|
|
88
|
+
break;
|
|
89
|
+
case "ArrowRight":
|
|
90
|
+
case "ArrowDown":
|
|
91
|
+
next = (index + 1) % tiles.length;
|
|
92
|
+
break;
|
|
93
|
+
case "ArrowLeft":
|
|
94
|
+
case "ArrowUp":
|
|
95
|
+
next = (index + tiles.length - 1) % tiles.length;
|
|
96
|
+
break;
|
|
97
|
+
default: return;
|
|
98
|
+
}
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const target = tiles[next]?.item;
|
|
101
|
+
if (target) setFocusedKey(target.key ?? target.name);
|
|
102
|
+
ref.current?.querySelectorAll("[data-slot=\"treemap-tile\"]")[next]?.focus();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
onPointerEnter: (event) => event.pointerType === "mouse" && onHover?.(item),
|
|
63
106
|
onClick: () => onSelect?.(key === selectedKey ? null : item),
|
|
64
107
|
children: [
|
|
65
108
|
/* @__PURE__ */ jsx("rect", {
|
|
@@ -78,14 +121,14 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
78
121
|
strokeWidth: 2,
|
|
79
122
|
className: "stroke-foreground"
|
|
80
123
|
}),
|
|
81
|
-
box.width
|
|
124
|
+
box.width >= MIN_LABEL_WIDTH && box.height >= MIN_LABEL_HEIGHT && /* @__PURE__ */ jsxs("text", {
|
|
82
125
|
x: 8,
|
|
83
126
|
y: 18,
|
|
84
127
|
className: "fill-foreground",
|
|
85
128
|
children: [/* @__PURE__ */ jsx("tspan", {
|
|
86
129
|
className: "font-medium",
|
|
87
|
-
children: item.name
|
|
88
|
-
}), /* @__PURE__ */ jsx("tspan", {
|
|
130
|
+
children: fit(item.name, box.width - 16)
|
|
131
|
+
}), box.height >= TWO_LINE_HEIGHT && /* @__PURE__ */ jsx("tspan", {
|
|
89
132
|
x: 8,
|
|
90
133
|
dy: 16,
|
|
91
134
|
children: change(item.change)
|
|
@@ -97,6 +140,9 @@ function Treemap({ items, height, range, onHover, selected, onSelect }) {
|
|
|
97
140
|
})
|
|
98
141
|
});
|
|
99
142
|
}
|
|
143
|
+
const MIN_LABEL_WIDTH = 56;
|
|
144
|
+
const MIN_LABEL_HEIGHT = 28;
|
|
145
|
+
const TWO_LINE_HEIGHT = 44;
|
|
100
146
|
function Swatch({ label, fill }) {
|
|
101
147
|
return /* @__PURE__ */ jsxs("svg", {
|
|
102
148
|
"data-slot": "treemap-legend-swatch",
|
|
@@ -143,9 +189,9 @@ function TreemapLegend({ range, className }) {
|
|
|
143
189
|
/* @__PURE__ */ jsxs("div", {
|
|
144
190
|
className: "flex justify-between gap-2 tabular-nums",
|
|
145
191
|
children: [
|
|
146
|
-
/* @__PURE__ */ jsxs("span", { children: ["
|
|
192
|
+
/* @__PURE__ */ jsxs("span", { children: ["down over ", over] }),
|
|
147
193
|
/* @__PURE__ */ jsxs("span", { children: [pct(step, digits), " a step"] }),
|
|
148
|
-
/* @__PURE__ */ jsxs("span", { children: ["
|
|
194
|
+
/* @__PURE__ */ jsxs("span", { children: ["up over ", over] })
|
|
149
195
|
]
|
|
150
196
|
}),
|
|
151
197
|
/* @__PURE__ */ jsxs("p", {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SeriesChartProps } from "../../lib/chart-series.js";
|
|
2
|
+
//#region src/components/siyahi/waterfall-chart.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* One bar of a waterfall. A change moves the running total by its value. A total draws the running total from zero;
|
|
5
|
+
* give it a value to set the total instead, as an opening balance does. Labels may repeat, such as "Balance" at
|
|
6
|
+
* both ends: each bar is placed by its position.
|
|
7
|
+
*/
|
|
8
|
+
export type WaterfallStep = {
|
|
9
|
+
kind: "change";
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "total";
|
|
14
|
+
label: string;
|
|
15
|
+
value?: number;
|
|
16
|
+
};
|
|
17
|
+
/** A step as drawn: the running total after it, and the span its bar covers. */
|
|
18
|
+
export interface WaterfallBar {
|
|
19
|
+
/** The step's position, as text: the category the bar is placed by. */
|
|
20
|
+
key: string;
|
|
21
|
+
label: string;
|
|
22
|
+
kind: "rise" | "fall" | "total";
|
|
23
|
+
/** How far the step moved the running total. */
|
|
24
|
+
change: number;
|
|
25
|
+
/** The running total after the step. */
|
|
26
|
+
level: number;
|
|
27
|
+
/** The bar's low and high ends, low first whichever way the step moved: Recharts reads a range that way. */
|
|
28
|
+
span: [number, number];
|
|
29
|
+
/** The bar picks up at the level the one before left, so a dashed line joins them. */
|
|
30
|
+
continues: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Walks the steps in order, keeping the running total. */
|
|
33
|
+
export declare function waterfallBars(steps: readonly WaterfallStep[]): WaterfallBar[];
|
|
34
|
+
export interface WaterfallChartCardProps extends Pick<SeriesChartProps<WaterfallBar, "key">, "title" | "description" | "action" | "height" | "valueFormatter" | "yTickFormatter" | "yLabel" | "referenceLines" | "placeholder" | "footer"> {
|
|
35
|
+
/** The bars in order: usually a total to start from, the changes, and a total to end on. */
|
|
36
|
+
steps: readonly WaterfallStep[];
|
|
37
|
+
/** Bars run sideways with the labels down the left, for long names such as product categories. */
|
|
38
|
+
horizontal?: boolean;
|
|
39
|
+
/** What the legend calls each kind of bar. */
|
|
40
|
+
labels?: {
|
|
41
|
+
rise: string;
|
|
42
|
+
fall: string;
|
|
43
|
+
total: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A running total bridged step by step: where it starts, each change that moved it, and where it ends, such as a
|
|
48
|
+
* year's profit built up month by month. Rises and falls float at the level the one before left; totals stand on zero.
|
|
49
|
+
*/
|
|
50
|
+
export declare function WaterfallChartCard({ steps, horizontal, labels, ...props }: WaterfallChartCardProps): import("react").JSX.Element;
|
|
51
|
+
//#endregion
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useLocale } from "./siyahi-provider.js";
|
|
2
|
+
import { ChartFrame, barFill } from "../../lib/chart-frame.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { Bar, BarChart, Cell, ReferenceLine } from "recharts";
|
|
6
|
+
//#region src/components/siyahi/waterfall-chart.tsx
|
|
7
|
+
const span = (a, b) => [Math.min(a, b), Math.max(a, b)];
|
|
8
|
+
/** Walks the steps in order, keeping the running total. */
|
|
9
|
+
function waterfallBars(steps) {
|
|
10
|
+
let level = 0;
|
|
11
|
+
return steps.map((step, index) => {
|
|
12
|
+
const before = level;
|
|
13
|
+
const key = String(index);
|
|
14
|
+
if (step.kind === "total") {
|
|
15
|
+
level = step.value ?? level;
|
|
16
|
+
const continues = level === before;
|
|
17
|
+
return {
|
|
18
|
+
key,
|
|
19
|
+
label: step.label,
|
|
20
|
+
kind: "total",
|
|
21
|
+
change: level - before,
|
|
22
|
+
level,
|
|
23
|
+
span: span(0, level),
|
|
24
|
+
continues
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
level += step.value;
|
|
28
|
+
const kind = step.value < 0 ? "fall" : "rise";
|
|
29
|
+
return {
|
|
30
|
+
key,
|
|
31
|
+
label: step.label,
|
|
32
|
+
kind,
|
|
33
|
+
change: step.value,
|
|
34
|
+
level,
|
|
35
|
+
span: span(before, level),
|
|
36
|
+
continues: true
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The first step whose figure is not a number. Every bar after it stands on the running total, so a missing change
|
|
42
|
+
* cannot be skipped without drawing wrong balances; the card says which step is missing instead.
|
|
43
|
+
*/
|
|
44
|
+
const unknownStep = (steps) => steps.find((step) => step.value !== void 0 && !Number.isFinite(step.value));
|
|
45
|
+
const COLOR = {
|
|
46
|
+
rise: "var(--positive)",
|
|
47
|
+
fall: "var(--negative)",
|
|
48
|
+
total: "var(--chart-1)"
|
|
49
|
+
};
|
|
50
|
+
const LABELS = {
|
|
51
|
+
rise: "Increase",
|
|
52
|
+
fall: "Decrease",
|
|
53
|
+
total: "Total"
|
|
54
|
+
};
|
|
55
|
+
const MINUS = "−";
|
|
56
|
+
const fill = (kind) => barFill(COLOR[kind]);
|
|
57
|
+
function Legend({ labels }) {
|
|
58
|
+
return /* @__PURE__ */ jsx("div", {
|
|
59
|
+
className: "flex items-center justify-center gap-4 pt-3",
|
|
60
|
+
children: [
|
|
61
|
+
"rise",
|
|
62
|
+
"fall",
|
|
63
|
+
"total"
|
|
64
|
+
].map((kind) => /* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "flex items-center gap-1.5",
|
|
66
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
67
|
+
className: "size-2 shrink-0 rounded-xs border",
|
|
68
|
+
style: {
|
|
69
|
+
backgroundColor: fill(kind),
|
|
70
|
+
borderColor: COLOR[kind]
|
|
71
|
+
}
|
|
72
|
+
}), labels[kind]]
|
|
73
|
+
}, kind))
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A running total bridged step by step: where it starts, each change that moved it, and where it ends, such as a
|
|
78
|
+
* year's profit built up month by month. Rises and falls float at the level the one before left; totals stand on zero.
|
|
79
|
+
*/
|
|
80
|
+
function WaterfallChartCard({ steps, horizontal = false, labels = LABELS, ...props }) {
|
|
81
|
+
const { numberLocale } = useLocale();
|
|
82
|
+
const bars = useMemo(() => waterfallBars(steps), [steps]);
|
|
83
|
+
const format = props.valueFormatter ?? new Intl.NumberFormat(numberLocale).format;
|
|
84
|
+
const signed = (value) => `${value > 0 ? "+" : value < 0 ? MINUS : ""}${format(Math.abs(value))}`;
|
|
85
|
+
const names = new Map(bars.map((bar) => [bar.key, bar.label]));
|
|
86
|
+
const unknown = unknownStep(steps);
|
|
87
|
+
return /* @__PURE__ */ jsxs(ChartFrame, {
|
|
88
|
+
chart: BarChart,
|
|
89
|
+
...props,
|
|
90
|
+
placeholder: props.placeholder ?? (unknown && /* @__PURE__ */ jsxs("p", {
|
|
91
|
+
className: "text-sm text-muted-foreground",
|
|
92
|
+
children: [unknown.label, " has no figure, so the running total after it is unknown."]
|
|
93
|
+
})),
|
|
94
|
+
horizontal,
|
|
95
|
+
data: bars,
|
|
96
|
+
xKey: "key",
|
|
97
|
+
xFormatter: (key) => names.get(key) ?? key,
|
|
98
|
+
series: [{
|
|
99
|
+
key: "level",
|
|
100
|
+
label: labels.total,
|
|
101
|
+
color: COLOR.total
|
|
102
|
+
}],
|
|
103
|
+
legend: /* @__PURE__ */ jsx(Legend, { labels }),
|
|
104
|
+
tooltipRow: (_value, _name, item) => {
|
|
105
|
+
const bar = item.payload;
|
|
106
|
+
return (bar.kind === "total" ? [[labels.total, format(bar.level)]] : [[bar.kind === "rise" ? labels.rise : labels.fall, signed(bar.change)], ["Running total", format(bar.level)]]).map(([name, figure]) => /* @__PURE__ */ jsxs("div", {
|
|
107
|
+
className: "flex w-full items-center gap-2",
|
|
108
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
109
|
+
className: "text-muted-foreground",
|
|
110
|
+
children: name
|
|
111
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
112
|
+
className: "ml-auto font-mono font-medium text-foreground tabular-nums",
|
|
113
|
+
children: figure
|
|
114
|
+
})]
|
|
115
|
+
}, name));
|
|
116
|
+
},
|
|
117
|
+
children: [bars.slice(0, -1).map((bar, i) => {
|
|
118
|
+
const next = bars[i + 1];
|
|
119
|
+
if (!next.continues) return null;
|
|
120
|
+
const at = (key) => horizontal ? {
|
|
121
|
+
x: bar.level,
|
|
122
|
+
y: key
|
|
123
|
+
} : {
|
|
124
|
+
x: key,
|
|
125
|
+
y: bar.level
|
|
126
|
+
};
|
|
127
|
+
return /* @__PURE__ */ jsx(ReferenceLine, {
|
|
128
|
+
segment: [at(bar.key), at(next.key)],
|
|
129
|
+
stroke: "var(--muted-foreground)",
|
|
130
|
+
strokeDasharray: "2 2",
|
|
131
|
+
"data-slot": "waterfall-connector"
|
|
132
|
+
}, bar.key);
|
|
133
|
+
}), /* @__PURE__ */ jsx(Bar, {
|
|
134
|
+
dataKey: "span",
|
|
135
|
+
name: labels.total,
|
|
136
|
+
maxBarSize: horizontal ? 24 : 48,
|
|
137
|
+
isAnimationActive: false,
|
|
138
|
+
children: bars.map((bar) => /* @__PURE__ */ jsx(Cell, {
|
|
139
|
+
fill: fill(bar.kind),
|
|
140
|
+
stroke: COLOR[bar.kind],
|
|
141
|
+
strokeWidth: 1
|
|
142
|
+
}, bar.key))
|
|
143
|
+
})]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { WaterfallChartCard, waterfallBars };
|