@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,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 {};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePlotArea, useXAxisScale, useYAxisScale } from "recharts";
|
|
3
|
+
//#region src/lib/chart-marks.tsx
|
|
4
|
+
/** The colour a series takes when it is one of many drawn quietly behind the few a page names in colour. */
|
|
5
|
+
const QUIET = "var(--muted-foreground)";
|
|
6
|
+
/** Quiet series first, so the named ones draw over them; otherwise the page's order. */
|
|
7
|
+
function drawOrder(series) {
|
|
8
|
+
return [...series.filter(({ color }) => color === QUIET), ...series.filter(({ color }) => color !== QUIET)];
|
|
9
|
+
}
|
|
10
|
+
const NAME_START = 24;
|
|
11
|
+
const NAME_FONT = "500 11px \"Geist Variable\", \"Geist Fallback\", ui-sans-serif, sans-serif";
|
|
12
|
+
const NAME_WIDTH_GUESS = 7.5;
|
|
13
|
+
const NAME_GAP = 14;
|
|
14
|
+
let measure;
|
|
15
|
+
/** A name's width as the browser draws it, measured on a canvas; a wide guess where there is none. */
|
|
16
|
+
function nameWidth(name) {
|
|
17
|
+
if (measure === void 0) measure = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
|
|
18
|
+
if (!measure) return name.length * NAME_WIDTH_GUESS;
|
|
19
|
+
measure.font = NAME_FONT;
|
|
20
|
+
return measure.measureText(name).width;
|
|
21
|
+
}
|
|
22
|
+
/** Room the names need right of the plot: the widest name as drawn, after its leader and colour square. */
|
|
23
|
+
function endLabelRoom(names) {
|
|
24
|
+
return Math.ceil(NAME_START + Math.max(0, ...names.map(nameWidth)) + 8);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether every line ends on the plot's right edge: each series has a value in the last row, there is more than one
|
|
28
|
+
* row, and a numeric x axis runs no further than the data. A line that stops short would need a long leader across
|
|
29
|
+
* the others to reach its name, so the chart keeps its legend then.
|
|
30
|
+
*/
|
|
31
|
+
function linesEndAtEdge(data, xKey, series, xDomain) {
|
|
32
|
+
const last = data.at(-1);
|
|
33
|
+
if (!last || data.length < 2) return false;
|
|
34
|
+
if (xDomain && Number(last[xKey]) < xDomain[1]) return false;
|
|
35
|
+
return series.every(({ key }) => Number.isFinite(last[key]));
|
|
36
|
+
}
|
|
37
|
+
const END_LABELS_MIN_WIDTH = 400;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the names fit at the line ends: a card at least 400px wide, names taking at most 30% of it, and a plot
|
|
40
|
+
* tall enough to hold them all 14px apart. Otherwise the chart keeps its legend below.
|
|
41
|
+
*/
|
|
42
|
+
function endLabelsFit(names, width, plotHeight) {
|
|
43
|
+
return width >= END_LABELS_MIN_WIDTH && endLabelRoom(names) <= width * .3 && (names.length - 1) * NAME_GAP + 12 <= plotHeight;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where each name goes: every line's name at its end's level, spread to 14px apart, top to bottom, pushed back up
|
|
47
|
+
* from the plot's foot and never above its top. A point that maps to no number (a scale being replaced) is left out.
|
|
48
|
+
*/
|
|
49
|
+
function placeEnds(lines, plot) {
|
|
50
|
+
const gutter = plot.x + plot.width;
|
|
51
|
+
const ends = [];
|
|
52
|
+
for (const end of lines) if (Number.isFinite(end.x) && Number.isFinite(end.y)) ends.push({
|
|
53
|
+
...end,
|
|
54
|
+
at: end.y,
|
|
55
|
+
gutter,
|
|
56
|
+
leader: false
|
|
57
|
+
});
|
|
58
|
+
ends.sort((a, b) => a.y - b.y);
|
|
59
|
+
let previous = -Infinity;
|
|
60
|
+
for (const end of ends) {
|
|
61
|
+
end.at = Math.max(end.y, previous + NAME_GAP);
|
|
62
|
+
previous = end.at;
|
|
63
|
+
}
|
|
64
|
+
let limit = plot.y + plot.height;
|
|
65
|
+
for (const end of ends.toReversed()) {
|
|
66
|
+
end.at = Math.min(end.at, limit);
|
|
67
|
+
limit = end.at - NAME_GAP;
|
|
68
|
+
}
|
|
69
|
+
for (const end of ends) {
|
|
70
|
+
end.at = Math.max(end.at, plot.y + 4);
|
|
71
|
+
end.leader = Math.abs(end.at - end.y) > 3 || end.x < gutter - 1;
|
|
72
|
+
}
|
|
73
|
+
return ends;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Each series' name in the gutter right of the plot, in the text colour beside a square of the line's colour, in
|
|
77
|
+
* place of a legend the reader has to match colours against. A leader joins each name to its line's end where the
|
|
78
|
+
* name was moved off the end's level or the line ends short of the plot's edge. A quiet (grey) series' name is
|
|
79
|
+
* muted, with no square: its place beside its line's end says which line it is.
|
|
80
|
+
*/
|
|
81
|
+
function EndLabels({ data, xKey, series }) {
|
|
82
|
+
const xScale = useXAxisScale();
|
|
83
|
+
const yScale = useYAxisScale();
|
|
84
|
+
const plot = usePlotArea();
|
|
85
|
+
if (!xScale || !yScale || !plot) return null;
|
|
86
|
+
const lines = [];
|
|
87
|
+
for (const { key, label, color } of series) {
|
|
88
|
+
const row = data.findLast((candidate) => Number.isFinite(candidate[key]));
|
|
89
|
+
const x = row && xScale(row[xKey]);
|
|
90
|
+
const y = row && yScale(row[key]);
|
|
91
|
+
if (x !== void 0 && y !== void 0) lines.push({
|
|
92
|
+
id: String(key),
|
|
93
|
+
name: label,
|
|
94
|
+
color,
|
|
95
|
+
x,
|
|
96
|
+
y,
|
|
97
|
+
quiet: color === QUIET
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return /* @__PURE__ */ jsx("g", {
|
|
101
|
+
"data-slot": "end-labels",
|
|
102
|
+
children: placeEnds(lines, plot).map(({ gutter, ...end }) => /* @__PURE__ */ jsxs("g", { children: [
|
|
103
|
+
end.leader && /* @__PURE__ */ jsx("polyline", {
|
|
104
|
+
points: `${end.x + 4},${end.y} ${Math.max(end.x + 4, gutter) + 6},${end.at}`,
|
|
105
|
+
fill: "none",
|
|
106
|
+
stroke: end.color,
|
|
107
|
+
strokeOpacity: .55
|
|
108
|
+
}),
|
|
109
|
+
!end.quiet && /* @__PURE__ */ jsx("rect", {
|
|
110
|
+
x: gutter + 13,
|
|
111
|
+
y: end.at - 3.5,
|
|
112
|
+
width: 7,
|
|
113
|
+
height: 7,
|
|
114
|
+
fill: end.color
|
|
115
|
+
}),
|
|
116
|
+
/* @__PURE__ */ jsx("text", {
|
|
117
|
+
x: gutter + (end.quiet ? 13 : NAME_START),
|
|
118
|
+
y: end.at + 4,
|
|
119
|
+
className: end.quiet ? "fill-muted-foreground text-2xs" : "fill-foreground text-2xs font-medium",
|
|
120
|
+
children: end.name
|
|
121
|
+
})
|
|
122
|
+
] }, end.id))
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const MARK_LABEL = "stroke-card font-mono text-2xs uppercase";
|
|
126
|
+
const HALO = {
|
|
127
|
+
paintOrder: "stroke",
|
|
128
|
+
strokeWidth: 3,
|
|
129
|
+
strokeLinejoin: "round"
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* A forecast stretch, from forecastFrom to the last point: a faint hatched band labelled FORECAST behind the lines,
|
|
133
|
+
* and the clip paths that keep each line solid before it and dashed after. And a "today" rule: 1px in the link
|
|
134
|
+
* colour with a small mono capital label.
|
|
135
|
+
*/
|
|
136
|
+
function TimeMarks({ data, xKey, forecastFrom, today, clipId, hatch }) {
|
|
137
|
+
const xScale = useXAxisScale();
|
|
138
|
+
const plot = usePlotArea();
|
|
139
|
+
if (!xScale || !plot) return null;
|
|
140
|
+
const top = plot.y;
|
|
141
|
+
const bottom = plot.y + plot.height;
|
|
142
|
+
const cut = forecastFrom === void 0 ? void 0 : xScale(forecastFrom);
|
|
143
|
+
const last = data.at(-1);
|
|
144
|
+
const end = (last && xScale(last[xKey])) ?? plot.x + plot.width;
|
|
145
|
+
const now = today === void 0 ? void 0 : xScale(today);
|
|
146
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
147
|
+
"data-slot": "time-marks",
|
|
148
|
+
children: [cut !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
149
|
+
/* @__PURE__ */ jsxs("defs", { children: [
|
|
150
|
+
/* @__PURE__ */ jsx("clipPath", {
|
|
151
|
+
id: `${clipId}-actual`,
|
|
152
|
+
children: /* @__PURE__ */ jsx("rect", {
|
|
153
|
+
x: -1e4,
|
|
154
|
+
y: -1e4,
|
|
155
|
+
width: cut + 1e4,
|
|
156
|
+
height: 2e4
|
|
157
|
+
})
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ jsx("clipPath", {
|
|
160
|
+
id: `${clipId}-forecast`,
|
|
161
|
+
children: /* @__PURE__ */ jsx("rect", {
|
|
162
|
+
x: cut,
|
|
163
|
+
y: -1e4,
|
|
164
|
+
width: 2e4,
|
|
165
|
+
height: 2e4
|
|
166
|
+
})
|
|
167
|
+
}),
|
|
168
|
+
/* @__PURE__ */ jsx("pattern", {
|
|
169
|
+
id: `${clipId}-hatch`,
|
|
170
|
+
width: 7,
|
|
171
|
+
height: 7,
|
|
172
|
+
patternUnits: "userSpaceOnUse",
|
|
173
|
+
patternTransform: "rotate(45)",
|
|
174
|
+
children: /* @__PURE__ */ jsx("line", {
|
|
175
|
+
x1: 0,
|
|
176
|
+
y1: 0,
|
|
177
|
+
x2: 0,
|
|
178
|
+
y2: 7,
|
|
179
|
+
stroke: "var(--foreground)",
|
|
180
|
+
strokeOpacity: .16
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
] }),
|
|
184
|
+
hatch && /* @__PURE__ */ jsx("rect", {
|
|
185
|
+
x: cut,
|
|
186
|
+
y: top,
|
|
187
|
+
width: Math.max(0, end - cut),
|
|
188
|
+
height: plot.height,
|
|
189
|
+
fill: `url(#${clipId}-hatch)`
|
|
190
|
+
}),
|
|
191
|
+
/* @__PURE__ */ jsx("text", {
|
|
192
|
+
x: end - 4,
|
|
193
|
+
y: top + 12,
|
|
194
|
+
textAnchor: "end",
|
|
195
|
+
className: `${MARK_LABEL} fill-muted-foreground`,
|
|
196
|
+
style: HALO,
|
|
197
|
+
children: "Forecast"
|
|
198
|
+
})
|
|
199
|
+
] }), now !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("line", {
|
|
200
|
+
x1: now,
|
|
201
|
+
x2: now,
|
|
202
|
+
y1: top,
|
|
203
|
+
y2: bottom,
|
|
204
|
+
stroke: "var(--link)"
|
|
205
|
+
}), /* @__PURE__ */ jsx("text", {
|
|
206
|
+
x: now - 5,
|
|
207
|
+
y: top + 12,
|
|
208
|
+
textAnchor: "end",
|
|
209
|
+
className: `${MARK_LABEL} fill-link`,
|
|
210
|
+
style: HALO,
|
|
211
|
+
children: "Today"
|
|
212
|
+
})] })]
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { EndLabels, QUIET, TimeMarks, drawOrder, endLabelRoom, endLabelsFit, linesEndAtEdge, placeEnds };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AxisScale } from "./ticks.js";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
//#region src/lib/chart-series.d.ts
|
|
3
4
|
/**
|
|
@@ -6,7 +7,7 @@ import { ReactNode } from "react";
|
|
|
6
7
|
*/
|
|
7
8
|
export type ChartColor = "var(--chart-1)" | "var(--chart-2)" | "var(--chart-3)" | "var(--chart-4)" | "var(--chart-5)" | "var(--muted-foreground)" | "var(--positive)" | "var(--negative)";
|
|
8
9
|
/** The fields of a row that hold numbers: the ones a series can plot. */
|
|
9
|
-
type NumericKey<T> = { [K in keyof T]-?: T[K] extends number | null | undefined ? K : never; }[keyof T] & string;
|
|
10
|
+
export type NumericKey<T> = { [K in keyof T]-?: T[K] extends number | null | undefined ? K : never; }[keyof T] & string;
|
|
10
11
|
/** One series: the field it plots, its name in the legend and tooltip, and its colour. */
|
|
11
12
|
export interface ChartSeriesItem<T> {
|
|
12
13
|
key: NumericKey<T>;
|
|
@@ -49,21 +50,29 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
|
|
|
49
50
|
/** The field the x axis reads: a month, an hour of day, a price bucket. One row per value; a repeated value is hidden behind the first. */
|
|
50
51
|
xKey: K;
|
|
51
52
|
series: ChartSeries<NoInfer<T>>;
|
|
53
|
+
/** Categories to label, or tick positions with xDomain. Every named tick is drawn; other rows keep their marks and tooltips. */
|
|
54
|
+
xTicks?: readonly T[K][];
|
|
52
55
|
/** The x axis ticks, and the tooltip's heading unless xTooltipFormatter is set. Defaults to the value as text. */
|
|
53
56
|
xFormatter?: (value: T[K]) => string;
|
|
54
57
|
/** The tooltip's heading, when it should say more than a tick: "19:34:13 IST, 10 s average" against "19:34". */
|
|
55
58
|
xTooltipFormatter?: (value: T[K]) => string;
|
|
56
59
|
/** A series' value in the tooltip, and the y axis unless yTickFormatter is set. Defaults to the locale's numbers. */
|
|
57
60
|
valueFormatter?: (value: number) => string;
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The y axis, when its ticks should be shorter than the tooltip's figures: moneyCompact against money. It gets the
|
|
63
|
+
* axis' largest tick as `scale`, so moneyCompact writes every tick in one unit.
|
|
64
|
+
*/
|
|
65
|
+
yTickFormatter?: (value: number, scale: number) => string;
|
|
66
|
+
/**
|
|
67
|
+
* A fixed range for the value axis, such as [0, 100] for a percentage. By default it fits the data; a bar
|
|
68
|
+
* chart's always reaches zero.
|
|
69
|
+
*/
|
|
61
70
|
yDomain?: [number, number];
|
|
62
71
|
/** Where the value axis puts its ticks, such as [0, 25, 50, 75, 100]. */
|
|
63
72
|
yTicks?: readonly number[];
|
|
64
|
-
/** Height of the plot and legend, in pixels. */
|
|
73
|
+
/** Height of the plot and legend, in pixels. Sideways bars grow when wrapped tick details need more room. */
|
|
65
74
|
height: number;
|
|
66
|
-
/** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "
|
|
75
|
+
/** A second line under each x tick, so a row's figure reads without a hover: "FY24" then "+₹1.2L". */
|
|
67
76
|
xTickDetail?: (row: T) => string;
|
|
68
77
|
/** A title under the x axis, such as "Hour of day (IST)". Not drawn on sideways bars. */
|
|
69
78
|
xLabel?: string;
|
|
@@ -75,18 +84,41 @@ export interface SeriesChartProps<T extends object, K extends keyof T> {
|
|
|
75
84
|
placeholder?: ReactNode;
|
|
76
85
|
/** Lines under the chart, such as a caption or a takeaway, in the card's footer. */
|
|
77
86
|
footer?: ReactNode;
|
|
87
|
+
/**
|
|
88
|
+
* Marks the data as old, with since when, such as "3 Nov": the chart greys out and a small "Stale · 3 Nov" note
|
|
89
|
+
* sits under the title. The page decides; nothing is inferred from dates.
|
|
90
|
+
*/
|
|
91
|
+
stale?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
/** What a chart of lines takes, LineChartCard and AreaChartCard: a forecast stretch and today. */
|
|
94
|
+
export interface LineMarksProps<T extends object, K extends keyof T> {
|
|
95
|
+
/**
|
|
96
|
+
* The x value from which the data is a forecast, such as the last month-end: each line goes dashed from it, a faint
|
|
97
|
+
* hatched band labelled FORECAST covers the stretch, and the end dots are hollow.
|
|
98
|
+
*/
|
|
99
|
+
forecastFrom?: NoInfer<T[K]>;
|
|
100
|
+
/** The x value of today: a 1px rule in the link colour with a small TODAY label. */
|
|
101
|
+
today?: NoInfer<T[K]>;
|
|
78
102
|
}
|
|
79
103
|
/** A numeric x axis, for samples in time or any measure where a mark can sit between two rows. */
|
|
80
104
|
export interface NumericXProps {
|
|
81
105
|
/** The x axis runs over this range of the xKey field, which must hold numbers; without it the axis is one slot per row. */
|
|
82
106
|
xDomain?: [number, number];
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
}
|
|
108
|
+
/** A value axis that may run on a log scale: lines and areas, which need not start at zero as bars must. */
|
|
109
|
+
export interface ValueScaleProps {
|
|
110
|
+
/**
|
|
111
|
+
* "log" spreads values across powers of ten, for values that run from tiny to large; the first selection when
|
|
112
|
+
* yScaleToggle is set. Log draws only when every value, and yDomain, is above zero; otherwise the axis is linear.
|
|
113
|
+
* On log the axis ticks at 1, 2 and 5 inside yDomain, if given, and yTicks is not used.
|
|
114
|
+
*/
|
|
115
|
+
yScale?: AxisScale;
|
|
116
|
+
/** Linear and Log buttons for the value axis in the header, after the action, so a reader can switch. */
|
|
117
|
+
yScaleToggle?: boolean;
|
|
85
118
|
}
|
|
86
119
|
/**
|
|
87
120
|
* How a line or an area's edge joins its points: "linear" draws straight segments, as measured samples should
|
|
88
121
|
* be drawn; "monotone", the default, smooths between them without overshooting a point.
|
|
89
122
|
*/
|
|
90
123
|
export type ChartCurve = "monotone" | "linear";
|
|
91
|
-
//#endregion
|
|
92
|
-
export {};
|
|
124
|
+
//#endregion
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DataTableColumnDef } from "../components/registry/niko-table/types/index.js";
|
|
2
|
+
import { ColumnVisibilityState, RowData } from "@tanstack/react-table";
|
|
3
|
+
//#region src/lib/compact-columns.d.ts
|
|
4
|
+
/** Reports the element's inner width whenever it changes. */
|
|
5
|
+
export declare function useTableWidth(element: HTMLElement | null, onChange: (width: number) => void): void;
|
|
6
|
+
/** Narrower than 40rem, the width a phone gives a table: freezing is suspended and toolbars fold. */
|
|
7
|
+
export declare function isCompactWidth(width: number): boolean;
|
|
8
|
+
export interface FittedColumns {
|
|
9
|
+
/** The caller's visibility with the columns that do not fit turned off. */
|
|
10
|
+
visibility: ColumnVisibilityState;
|
|
11
|
+
/** The columns turned off to fit; empty when everything fits. */
|
|
12
|
+
hidden: ReadonlySet<string>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Which columns to hide so the table fits `width`: the rightmost columns without `meta.compact` go first. Null
|
|
16
|
+
* when no column sets `meta.compact`, so a table that never opted in keeps every column and scrolls.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fitColumns<TData extends RowData>(columns: DataTableColumnDef<TData>[], order: string[], visibility: ColumnVisibilityState, width: number): FittedColumns | null;
|
|
19
|
+
//#endregion
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
//#region src/lib/compact-columns.ts
|
|
3
|
+
const DEFAULT_SIZE = 150;
|
|
4
|
+
/** Reports the element's inner width whenever it changes. */
|
|
5
|
+
function useTableWidth(element, onChange) {
|
|
6
|
+
React.useLayoutEffect(() => {
|
|
7
|
+
if (!element) return;
|
|
8
|
+
const measure = () => {
|
|
9
|
+
if (element.clientWidth > 0) onChange(element.clientWidth);
|
|
10
|
+
};
|
|
11
|
+
measure();
|
|
12
|
+
const observer = new ResizeObserver(measure);
|
|
13
|
+
observer.observe(element);
|
|
14
|
+
return () => observer.disconnect();
|
|
15
|
+
}, [element, onChange]);
|
|
16
|
+
}
|
|
17
|
+
/** Narrower than 40rem, the width a phone gives a table: freezing is suspended and toolbars fold. */
|
|
18
|
+
function isCompactWidth(width) {
|
|
19
|
+
if (width <= 0) return false;
|
|
20
|
+
return width < 40 * Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
21
|
+
}
|
|
22
|
+
function columnId(column) {
|
|
23
|
+
if (column.id) return column.id;
|
|
24
|
+
const key = column.accessorKey;
|
|
25
|
+
return typeof key === "string" ? key.replaceAll(".", "_") : void 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which columns to hide so the table fits `width`: the rightmost columns without `meta.compact` go first. Null
|
|
29
|
+
* when no column sets `meta.compact`, so a table that never opted in keeps every column and scrolls.
|
|
30
|
+
*/
|
|
31
|
+
function fitColumns(columns, order, visibility, width) {
|
|
32
|
+
if (width <= 0 || !columns.some((column) => column.meta?.compact)) return null;
|
|
33
|
+
const byId = new Map(columns.flatMap((column) => {
|
|
34
|
+
const id = columnId(column);
|
|
35
|
+
return id ? [[id, column]] : [];
|
|
36
|
+
}));
|
|
37
|
+
const shown = [...order.filter((id) => byId.has(id)), ...[...byId.keys()].filter((id) => !order.includes(id))].filter((id) => visibility[id] !== false);
|
|
38
|
+
let total = shown.reduce((sum, id) => sum + (byId.get(id)?.size ?? DEFAULT_SIZE), 0);
|
|
39
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
40
|
+
for (const id of shown.toReversed()) {
|
|
41
|
+
if (total <= width) break;
|
|
42
|
+
const column = byId.get(id);
|
|
43
|
+
if (column?.meta?.compact) continue;
|
|
44
|
+
hidden.add(id);
|
|
45
|
+
total -= column?.size ?? DEFAULT_SIZE;
|
|
46
|
+
}
|
|
47
|
+
const fitted = { ...visibility };
|
|
48
|
+
for (const id of hidden) fitted[id] = false;
|
|
49
|
+
return {
|
|
50
|
+
visibility: fitted,
|
|
51
|
+
hidden
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { fitColumns, isCompactWidth, useTableWidth };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/lib/fit-text.d.ts
|
|
2
|
+
/** About how wide a text-xs label draws, in pixels. */
|
|
3
|
+
export declare const textWidth: (text: string) => number;
|
|
4
|
+
/** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
|
|
5
|
+
export declare function fit(text: string, width: number): string;
|
|
6
|
+
//#endregion
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/lib/fit-text.ts
|
|
2
|
+
const CHAR_PX = 6.6;
|
|
3
|
+
/** About how wide a text-xs label draws, in pixels. */
|
|
4
|
+
const textWidth = (text) => text.length * CHAR_PX;
|
|
5
|
+
/** The label, cut with an ellipsis when it would draw wider than `width` pixels at text-xs. */
|
|
6
|
+
function fit(text, width) {
|
|
7
|
+
const room = Math.floor(width / CHAR_PX);
|
|
8
|
+
return text.length <= room ? text : `${text.slice(0, Math.max(room - 1, 1)).trimEnd()}…`;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { fit, textWidth };
|
package/dist/lib/format.d.ts
CHANGED
|
@@ -6,9 +6,13 @@ export declare function money(value: number, locale?: SiyahiLocale, fractionDigi
|
|
|
6
6
|
* A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
|
|
7
7
|
* count in thousands up to a lakh, so in an Indian-region locale amounts under
|
|
8
8
|
* one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
|
|
9
|
-
* `Delta`, which adds the
|
|
9
|
+
* `Delta`, which adds the sign and a trend icon.
|
|
10
|
+
*
|
|
11
|
+
* Give `scale`, the largest amount on the same axis, and every tick takes that
|
|
12
|
+
* amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
|
|
13
|
+
* ₹0, rather than switching to ₹75,000 halfway up.
|
|
10
14
|
*/
|
|
11
|
-
export declare function moneyCompact(value: number, locale?: SiyahiLocale): string;
|
|
15
|
+
export declare function moneyCompact(value: number, locale?: SiyahiLocale, scale?: number): string;
|
|
12
16
|
/** A whole count with the locale's grouping: 1,53,110 in en-IN. */
|
|
13
17
|
export declare function count(value: number, locale?: SiyahiLocale): string;
|
|
14
18
|
/** A fraction as a percentage: 0.1234 is 12.3%. */
|
|
@@ -22,7 +26,8 @@ export declare function time(value: Date | number | string, locale?: SiyahiLocal
|
|
|
22
26
|
export interface Formatters {
|
|
23
27
|
count: (value: number) => string;
|
|
24
28
|
money: (value: number, fractionDigits?: number) => string;
|
|
25
|
-
|
|
29
|
+
/** Pass `scale` on an axis so every tick shares one unit; a chart's yTickFormatter gets it as its second argument. */
|
|
30
|
+
moneyCompact: (value: number, scale?: number) => string;
|
|
26
31
|
pct: (value: number, fractionDigits?: number) => string;
|
|
27
32
|
time: (value: Date | number | string, options?: TimeOptions) => string;
|
|
28
33
|
}
|
package/dist/lib/format.js
CHANGED
|
@@ -2,37 +2,69 @@ 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,
|
|
11
23
|
maximumFractionDigits: fractionDigits
|
|
12
24
|
}).format(value);
|
|
13
25
|
}
|
|
26
|
+
const DIGITS = /* @__PURE__ */ new Set(["integer", "fraction"]);
|
|
27
|
+
const NUMBER = /* @__PURE__ */ new Set([
|
|
28
|
+
"integer",
|
|
29
|
+
"group",
|
|
30
|
+
"decimal",
|
|
31
|
+
"fraction"
|
|
32
|
+
]);
|
|
14
33
|
/**
|
|
15
34
|
* A short amount: ₹2.4L and ₹40Cr in en-IN, $2.4M in en-US. Indian readers
|
|
16
35
|
* count in thousands up to a lakh, so in an Indian-region locale amounts under
|
|
17
36
|
* one lakh print in full (₹99,999), never as ₹99.9K. Signed figures belong in
|
|
18
|
-
* `Delta`, which adds the
|
|
37
|
+
* `Delta`, which adds the sign and a trend icon.
|
|
38
|
+
*
|
|
39
|
+
* Give `scale`, the largest amount on the same axis, and every tick takes that
|
|
40
|
+
* amount's unit, to two decimals so close ticks stay apart: ₹1.5L, ₹0.75L,
|
|
41
|
+
* ₹0, rather than switching to ₹75,000 halfway up.
|
|
19
42
|
*/
|
|
20
|
-
function moneyCompact(value, locale = defaultLocale) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
43
|
+
function moneyCompact(value, locale = defaultLocale, scale) {
|
|
44
|
+
if (!regions.has(locale.numberLocale)) regions.set(locale.numberLocale, new Intl.Locale(locale.numberLocale).region);
|
|
45
|
+
if (regions.get(locale.numberLocale) === "IN" && (scale ?? Math.abs(value)) < 1e5) return money(value, locale);
|
|
46
|
+
const compact = numberFormat(locale.numberLocale, {
|
|
23
47
|
style: "currency",
|
|
24
48
|
currency: locale.currency,
|
|
25
49
|
notation: "compact",
|
|
26
50
|
maximumFractionDigits: 1
|
|
27
|
-
})
|
|
51
|
+
});
|
|
52
|
+
if (scale === void 0 || value === 0) return compact.format(value);
|
|
53
|
+
const parts = compact.formatToParts(Math.sign(value) * scale);
|
|
54
|
+
const shown = Number(parts.flatMap((part) => part.type === "decimal" ? "." : DIGITS.has(part.type) ? part.value : []).join(""));
|
|
55
|
+
const unit = 10 ** Math.round(Math.log10(scale / shown));
|
|
56
|
+
if (unit === 1) return compact.format(value);
|
|
57
|
+
const digits = numberFormat(locale.numberLocale, { maximumFractionDigits: 2 }).format(Math.abs(value) / unit);
|
|
58
|
+
const first = parts.findIndex((part) => part.type === "integer");
|
|
59
|
+
return parts.flatMap((part, index) => index === first ? digits : NUMBER.has(part.type) ? [] : part.value).join("");
|
|
28
60
|
}
|
|
29
61
|
/** A whole count with the locale's grouping: 1,53,110 in en-IN. */
|
|
30
62
|
function count(value, locale = defaultLocale) {
|
|
31
|
-
return
|
|
63
|
+
return numberFormat(locale.numberLocale, { maximumFractionDigits: 0 }).format(value);
|
|
32
64
|
}
|
|
33
65
|
/** A fraction as a percentage: 0.1234 is 12.3%. */
|
|
34
66
|
function pct(value, locale = defaultLocale, fractionDigits = 1) {
|
|
35
|
-
return
|
|
67
|
+
return numberFormat(locale.numberLocale, {
|
|
36
68
|
style: "percent",
|
|
37
69
|
minimumFractionDigits: fractionDigits,
|
|
38
70
|
maximumFractionDigits: fractionDigits
|
|
@@ -40,19 +72,25 @@ function pct(value, locale = defaultLocale, fractionDigits = 1) {
|
|
|
40
72
|
}
|
|
41
73
|
/** A 24-hour time in the locale's zone, with the zone written: 14:30 IST. */
|
|
42
74
|
function time(value, locale = defaultLocale, { withDate = false } = {}) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
...date,
|
|
75
|
+
const options = {
|
|
76
|
+
...withDate ? {
|
|
77
|
+
day: "numeric",
|
|
78
|
+
month: "short",
|
|
79
|
+
year: "numeric"
|
|
80
|
+
} : {},
|
|
50
81
|
hour: "2-digit",
|
|
51
82
|
minute: "2-digit",
|
|
52
83
|
hourCycle: "h23",
|
|
53
84
|
timeZone: locale.timeZone,
|
|
54
85
|
timeZoneName: "short"
|
|
55
|
-
}
|
|
86
|
+
};
|
|
87
|
+
const key = JSON.stringify([locale.numberLocale, options]);
|
|
88
|
+
let formatter = clocks.get(key);
|
|
89
|
+
if (!formatter) {
|
|
90
|
+
formatter = new Intl.DateTimeFormat(locale.numberLocale, options);
|
|
91
|
+
clocks.set(key, formatter);
|
|
92
|
+
}
|
|
93
|
+
return formatter.format(new Date(value));
|
|
56
94
|
}
|
|
57
95
|
/** The formatters above, bound to the locale of the nearest `SiyahiProvider`. */
|
|
58
96
|
function useFormat() {
|
|
@@ -60,7 +98,7 @@ function useFormat() {
|
|
|
60
98
|
return useMemo(() => ({
|
|
61
99
|
count: (value) => count(value, locale),
|
|
62
100
|
money: (value, fractionDigits) => money(value, locale, fractionDigits),
|
|
63
|
-
moneyCompact: (value) => moneyCompact(value, locale),
|
|
101
|
+
moneyCompact: (value, scale) => moneyCompact(value, locale, scale),
|
|
64
102
|
pct: (value, fractionDigits) => pct(value, locale, fractionDigits),
|
|
65
103
|
time: (value, options) => time(value, locale, options)
|
|
66
104
|
}), [locale]);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/lib/heat.d.ts
|
|
2
|
+
export declare const HEAT_STEPS = 5;
|
|
3
|
+
/** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
|
|
4
|
+
export declare const heatRange: (range?: number) => number;
|
|
5
|
+
export declare const heatFill: (side: "positive" | "negative", step: number) => string;
|
|
6
|
+
export declare function heat(change: number | null, range?: number): string;
|
|
7
|
+
//#endregion
|
package/dist/lib/heat.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/lib/heat.ts
|
|
2
|
+
/** The signed heat scale a Treemap tile and a ChangeGrid cell share: five steps of gain or loss mixed into the card. */
|
|
3
|
+
const DEFAULT_RANGE = .05;
|
|
4
|
+
const HEAT_STEPS = 5;
|
|
5
|
+
/** The range the heat scale uses: one that is not a positive number falls back to ±5%. */
|
|
6
|
+
const heatRange = (range = DEFAULT_RANGE) => Number.isFinite(range) && range > 0 ? range : DEFAULT_RANGE;
|
|
7
|
+
const heatFill = (side, step) => `color-mix(in oklab, var(--${side}) var(--heat-${step}), var(--card))`;
|
|
8
|
+
function heat(change, range) {
|
|
9
|
+
if (!change || !Number.isFinite(change)) return "var(--muted)";
|
|
10
|
+
const step = Math.min(5, Math.ceil(Math.abs(change) / heatRange(range) * 5));
|
|
11
|
+
return heatFill(change > 0 ? "positive" : "negative", step);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { HEAT_STEPS, heat, heatFill, heatRange };
|