@wealthx/shadcn 1.0.2 → 1.1.0
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/.turbo/turbo-build.log +154 -138
- package/CHANGELOG.md +6 -0
- package/README.md +82 -0
- package/dist/chunk-3EQP72AW.mjs +58 -0
- package/dist/chunk-5JGQAAQV.mjs +212 -0
- package/dist/chunk-GLW2UO6O.mjs +212 -0
- package/dist/chunk-RN67642N.mjs +171 -0
- package/dist/chunk-UEL4RD5P.mjs +272 -0
- package/dist/chunk-YBXCIF5Q.mjs +198 -0
- package/dist/components/ui/cashflow-bar-chart.js +596 -0
- package/dist/components/ui/cashflow-bar-chart.mjs +16 -0
- package/dist/components/ui/combobox.js +261 -0
- package/dist/components/ui/combobox.mjs +28 -0
- package/dist/components/ui/data-table.mjs +3 -3
- package/dist/components/ui/expense-bar-chart.js +543 -0
- package/dist/components/ui/expense-bar-chart.mjs +16 -0
- package/dist/components/ui/field.mjs +2 -2
- package/dist/components/ui/income-bar-chart.js +543 -0
- package/dist/components/ui/income-bar-chart.mjs +16 -0
- package/dist/components/ui/transactions-income-expense-bar-chart.js +478 -0
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +16 -0
- package/dist/index.js +1685 -725
- package/dist/index.mjs +152 -111
- package/dist/styles.css +1 -1
- package/package.json +30 -2
- package/src/components/index.tsx +56 -0
- package/src/components/ui/cashflow-bar-chart.tsx +336 -0
- package/src/components/ui/chart-shared.tsx +100 -0
- package/src/components/ui/combobox.tsx +217 -0
- package/src/components/ui/expense-bar-chart.tsx +278 -0
- package/src/components/ui/income-bar-chart.tsx +278 -0
- package/src/components/ui/transactions-income-expense-bar-chart.tsx +198 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +5 -0
- package/dist/{chunk-K76E2TQU.mjs → chunk-CJ46PDXE.mjs} +5 -5
- package/dist/{chunk-HUVTPUV2.mjs → chunk-NLLKTU4B.mjs} +3 -3
package/src/components/index.tsx
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
export { TransactionsIncomeExpenseBarChart } from "./ui/transactions-income-expense-bar-chart";
|
|
2
|
+
export type { TransactionsIncomeExpenseBarChartProps } from "./ui/transactions-income-expense-bar-chart";
|
|
3
|
+
|
|
4
|
+
export { CashflowBarChart } from "./ui/cashflow-bar-chart";
|
|
5
|
+
export type {
|
|
6
|
+
CashflowBarChartProps,
|
|
7
|
+
CashflowChartData,
|
|
8
|
+
CashflowDataPoint,
|
|
9
|
+
CashflowPeriod,
|
|
10
|
+
} from "./ui/cashflow-bar-chart";
|
|
11
|
+
|
|
12
|
+
export { ExpenseBarChart } from "./ui/expense-bar-chart";
|
|
13
|
+
export type {
|
|
14
|
+
ExpenseBarChartProps,
|
|
15
|
+
ExpenseBarChartData,
|
|
16
|
+
ExpenseDataset,
|
|
17
|
+
ExpensePeriod,
|
|
18
|
+
ExpenseGranularity,
|
|
19
|
+
} from "./ui/expense-bar-chart";
|
|
20
|
+
|
|
21
|
+
export { IncomeBarChart } from "./ui/income-bar-chart";
|
|
22
|
+
export type {
|
|
23
|
+
IncomeBarChartProps,
|
|
24
|
+
IncomeBarChartData,
|
|
25
|
+
IncomeDataset,
|
|
26
|
+
IncomePeriod,
|
|
27
|
+
IncomeGranularity,
|
|
28
|
+
} from "./ui/income-bar-chart";
|
|
29
|
+
|
|
1
30
|
export {
|
|
2
31
|
Accordion,
|
|
3
32
|
AccordionItem,
|
|
@@ -98,6 +127,33 @@ export type { CheckboxProps, CheckboxCardProps } from "./ui/checkbox";
|
|
|
98
127
|
export { Chip } from "./ui/chip";
|
|
99
128
|
export type { ChipProps } from "./ui/chip";
|
|
100
129
|
|
|
130
|
+
export {
|
|
131
|
+
Combobox,
|
|
132
|
+
ComboboxTrigger,
|
|
133
|
+
ComboboxValue,
|
|
134
|
+
ComboboxInput,
|
|
135
|
+
ComboboxContent,
|
|
136
|
+
ComboboxList,
|
|
137
|
+
ComboboxItem,
|
|
138
|
+
ComboboxEmpty,
|
|
139
|
+
ComboboxGroup,
|
|
140
|
+
ComboboxGroupLabel,
|
|
141
|
+
ComboboxSeparator,
|
|
142
|
+
} from "./ui/combobox";
|
|
143
|
+
export type {
|
|
144
|
+
ComboboxProps,
|
|
145
|
+
ComboboxTriggerProps,
|
|
146
|
+
ComboboxValueProps,
|
|
147
|
+
ComboboxInputProps,
|
|
148
|
+
ComboboxContentProps,
|
|
149
|
+
ComboboxListProps,
|
|
150
|
+
ComboboxItemProps,
|
|
151
|
+
ComboboxEmptyProps,
|
|
152
|
+
ComboboxGroupProps,
|
|
153
|
+
ComboboxGroupLabelProps,
|
|
154
|
+
ComboboxSeparatorProps,
|
|
155
|
+
} from "./ui/combobox";
|
|
156
|
+
|
|
101
157
|
export {
|
|
102
158
|
DataTable,
|
|
103
159
|
DataTableToolbar,
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Chart as ChartJS,
|
|
4
|
+
CategoryScale,
|
|
5
|
+
LinearScale,
|
|
6
|
+
BarElement,
|
|
7
|
+
Tooltip,
|
|
8
|
+
Legend,
|
|
9
|
+
type ChartOptions,
|
|
10
|
+
type ChartData,
|
|
11
|
+
} from "chart.js";
|
|
12
|
+
import { Chart } from "react-chartjs-2";
|
|
13
|
+
import { useThemeVars } from "@/lib/theme-provider";
|
|
14
|
+
import { Card, CardContent, CardHeader, CardTitle, CardAction } from "./card";
|
|
15
|
+
import { Empty, EmptyDescription } from "./empty";
|
|
16
|
+
import { Skeleton } from "./skeleton";
|
|
17
|
+
import { cn } from "@/lib/utils";
|
|
18
|
+
import {
|
|
19
|
+
hexToRgba,
|
|
20
|
+
FALLBACK_TICK,
|
|
21
|
+
formatTooltipDate,
|
|
22
|
+
ChartPeriodButton,
|
|
23
|
+
} from "./chart-shared";
|
|
24
|
+
|
|
25
|
+
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Types
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
export interface CashflowDataPoint {
|
|
32
|
+
/** ISO date string e.g. "2024-01-01" */
|
|
33
|
+
date: string;
|
|
34
|
+
income: number;
|
|
35
|
+
expenses: number;
|
|
36
|
+
/** Positive value — ignored when overspending > 0 */
|
|
37
|
+
surplus: number;
|
|
38
|
+
/** Positive value — ignored when surplus > 0 */
|
|
39
|
+
overspending: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CashflowChartData {
|
|
43
|
+
months: string[];
|
|
44
|
+
data: CashflowDataPoint[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CashflowPeriod = 3 | 6 | 12;
|
|
48
|
+
|
|
49
|
+
export interface CashflowBarChartProps {
|
|
50
|
+
/** Full dataset — sliced to the selected period */
|
|
51
|
+
cashflowData: CashflowChartData | null;
|
|
52
|
+
title?: string;
|
|
53
|
+
/** Show or hide the chart legend */
|
|
54
|
+
showLegend?: boolean;
|
|
55
|
+
/** Show or hide X axis labels */
|
|
56
|
+
showXAxis?: boolean;
|
|
57
|
+
/** Show or hide Y axis labels */
|
|
58
|
+
showYAxis?: boolean;
|
|
59
|
+
/** Legend placement relative to chart */
|
|
60
|
+
legendPosition?: "top" | "bottom";
|
|
61
|
+
/** Default period selector value */
|
|
62
|
+
defaultPeriod?: CashflowPeriod;
|
|
63
|
+
/** Chart canvas height in pixels */
|
|
64
|
+
height?: number;
|
|
65
|
+
/** Width of the card in pixels */
|
|
66
|
+
width?: number | string;
|
|
67
|
+
className?: string;
|
|
68
|
+
/** Show skeleton loading state instead of the chart */
|
|
69
|
+
isLoading?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// HTML legend (outside canvas — full spacing control)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
interface LegendItemProps {
|
|
77
|
+
label: string;
|
|
78
|
+
fillColor: string;
|
|
79
|
+
strokeColor: string;
|
|
80
|
+
strokeWidth?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function LegendItem({ label, fillColor, strokeColor, strokeWidth = 1.5 }: LegendItemProps) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex items-center gap-1.5">
|
|
86
|
+
<div
|
|
87
|
+
style={{
|
|
88
|
+
width: 10,
|
|
89
|
+
height: 10,
|
|
90
|
+
backgroundColor: fillColor,
|
|
91
|
+
border: strokeWidth > 0 ? `${strokeWidth}px solid ${strokeColor}` : "none",
|
|
92
|
+
flexShrink: 0,
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
<span className="text-[11px] text-muted-foreground leading-none">{label}</span>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ChartLegend({ primary, secondary }: { primary: string; secondary: string }) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
|
|
103
|
+
<LegendItem
|
|
104
|
+
label="Income"
|
|
105
|
+
fillColor={hexToRgba(primary, 0.2)}
|
|
106
|
+
strokeColor={primary}
|
|
107
|
+
/>
|
|
108
|
+
<LegendItem
|
|
109
|
+
label="Expenses and Liabilities"
|
|
110
|
+
fillColor={hexToRgba(secondary, 0.2)}
|
|
111
|
+
strokeColor={secondary}
|
|
112
|
+
/>
|
|
113
|
+
<LegendItem
|
|
114
|
+
label="Surplus Income"
|
|
115
|
+
fillColor={primary}
|
|
116
|
+
strokeColor={primary}
|
|
117
|
+
strokeWidth={0}
|
|
118
|
+
/>
|
|
119
|
+
<LegendItem
|
|
120
|
+
label="Over Spending"
|
|
121
|
+
fillColor={secondary}
|
|
122
|
+
strokeColor={secondary}
|
|
123
|
+
strokeWidth={0}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Constants
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
const PERIODS: CashflowPeriod[] = [3, 6, 12];
|
|
134
|
+
|
|
135
|
+
const FALLBACK_PRIMARY = "#33FF99";
|
|
136
|
+
const FALLBACK_SECONDARY = "#162029";
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Component
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export function CashflowBarChart({
|
|
143
|
+
cashflowData,
|
|
144
|
+
title = "Cashflow",
|
|
145
|
+
showLegend = true,
|
|
146
|
+
showXAxis = true,
|
|
147
|
+
showYAxis = true,
|
|
148
|
+
legendPosition = "top",
|
|
149
|
+
defaultPeriod = 6,
|
|
150
|
+
height = 280,
|
|
151
|
+
width = "100%",
|
|
152
|
+
className,
|
|
153
|
+
isLoading = false,
|
|
154
|
+
}: CashflowBarChartProps) {
|
|
155
|
+
const [period, setPeriod] = useState<CashflowPeriod>(defaultPeriod);
|
|
156
|
+
|
|
157
|
+
// Read theme colors from ThemeProvider context.
|
|
158
|
+
// Falls back to WealthX defaults when no ThemeProvider is present.
|
|
159
|
+
const themeVars = useThemeVars();
|
|
160
|
+
const brandPrimary: string =
|
|
161
|
+
(themeVars["--theme-primary"] as string | undefined) || FALLBACK_PRIMARY;
|
|
162
|
+
const brandSecondary: string =
|
|
163
|
+
(themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY;
|
|
164
|
+
|
|
165
|
+
const sliced = useMemo<CashflowChartData | null>(() => {
|
|
166
|
+
if (!cashflowData?.data?.length) return null;
|
|
167
|
+
const count = Math.min(period, cashflowData.data.length);
|
|
168
|
+
const start = cashflowData.data.length - count;
|
|
169
|
+
return {
|
|
170
|
+
months: cashflowData.months.slice(start),
|
|
171
|
+
data: cashflowData.data.slice(start),
|
|
172
|
+
};
|
|
173
|
+
}, [cashflowData, period]);
|
|
174
|
+
|
|
175
|
+
const chartData = useMemo<ChartData<"bar">>(() => {
|
|
176
|
+
if (!sliced) return { labels: [], datasets: [] };
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
labels: sliced.months,
|
|
180
|
+
datasets: [
|
|
181
|
+
{
|
|
182
|
+
label: "Income",
|
|
183
|
+
data: sliced.data.map((d) => d.income),
|
|
184
|
+
backgroundColor: hexToRgba(brandPrimary, 0.2),
|
|
185
|
+
hoverBackgroundColor: hexToRgba(brandPrimary, 0.35),
|
|
186
|
+
borderColor: brandPrimary,
|
|
187
|
+
borderWidth: 1.5,
|
|
188
|
+
borderRadius: 0,
|
|
189
|
+
borderSkipped: false,
|
|
190
|
+
barPercentage: 0.75,
|
|
191
|
+
categoryPercentage: 0.7,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: "Expenses and Liabilities",
|
|
195
|
+
data: sliced.data.map((d) => d.expenses),
|
|
196
|
+
backgroundColor: hexToRgba(brandSecondary, 0.2),
|
|
197
|
+
hoverBackgroundColor: hexToRgba(brandSecondary, 0.35),
|
|
198
|
+
borderColor: brandSecondary,
|
|
199
|
+
borderWidth: 1.5,
|
|
200
|
+
borderRadius: 0,
|
|
201
|
+
borderSkipped: false,
|
|
202
|
+
barPercentage: 0.75,
|
|
203
|
+
categoryPercentage: 0.7,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
label: "_thirdBar",
|
|
207
|
+
data: sliced.data.map((d) =>
|
|
208
|
+
d.overspending > 0 ? d.overspending : d.surplus
|
|
209
|
+
),
|
|
210
|
+
backgroundColor: sliced.data.map((d) =>
|
|
211
|
+
d.overspending > 0 ? brandSecondary : brandPrimary
|
|
212
|
+
),
|
|
213
|
+
hoverBackgroundColor: sliced.data.map((d) =>
|
|
214
|
+
d.overspending > 0
|
|
215
|
+
? hexToRgba(brandSecondary, 0.8)
|
|
216
|
+
: hexToRgba(brandPrimary, 0.8)
|
|
217
|
+
),
|
|
218
|
+
borderWidth: 0,
|
|
219
|
+
borderRadius: 0,
|
|
220
|
+
borderSkipped: false,
|
|
221
|
+
barPercentage: 0.75,
|
|
222
|
+
categoryPercentage: 0.7,
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
}, [sliced, brandPrimary, brandSecondary]);
|
|
227
|
+
|
|
228
|
+
const options = useMemo<ChartOptions<"bar">>(() => ({
|
|
229
|
+
responsive: true,
|
|
230
|
+
maintainAspectRatio: false,
|
|
231
|
+
animation: { duration: 800, easing: "easeOutQuart" },
|
|
232
|
+
layout: { padding: 0 },
|
|
233
|
+
plugins: {
|
|
234
|
+
legend: { display: false },
|
|
235
|
+
tooltip: {
|
|
236
|
+
mode: "index",
|
|
237
|
+
intersect: false,
|
|
238
|
+
padding: 12,
|
|
239
|
+
cornerRadius: 0,
|
|
240
|
+
titleFont: { size: 11, weight: "600" },
|
|
241
|
+
bodyFont: { size: 12, weight: "500" },
|
|
242
|
+
callbacks: {
|
|
243
|
+
title: (tooltipItems) => {
|
|
244
|
+
const idx = tooltipItems[0]?.dataIndex;
|
|
245
|
+
if (idx != null && sliced?.data[idx]?.date) {
|
|
246
|
+
return formatTooltipDate(sliced.data[idx].date, "monthly");
|
|
247
|
+
}
|
|
248
|
+
return tooltipItems[0]?.label ?? "";
|
|
249
|
+
},
|
|
250
|
+
label: (ctx) => {
|
|
251
|
+
const val = ctx.raw as number;
|
|
252
|
+
if (val === 0) return null;
|
|
253
|
+
if (ctx.datasetIndex === 2) {
|
|
254
|
+
const d = sliced?.data[ctx.dataIndex];
|
|
255
|
+
if (!d) return null;
|
|
256
|
+
const lbl = d.overspending > 0 ? "Over Spending" : "Surplus Income";
|
|
257
|
+
return ` ${lbl}: $${val.toLocaleString()}`;
|
|
258
|
+
}
|
|
259
|
+
return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
scales: {
|
|
265
|
+
x: {
|
|
266
|
+
display: showXAxis,
|
|
267
|
+
grid: { display: false },
|
|
268
|
+
border: { display: false },
|
|
269
|
+
ticks: { font: { size: 10 }, color: FALLBACK_TICK },
|
|
270
|
+
},
|
|
271
|
+
y: {
|
|
272
|
+
display: showYAxis,
|
|
273
|
+
grid: { display: false },
|
|
274
|
+
border: { display: false },
|
|
275
|
+
ticks: {
|
|
276
|
+
font: { size: 10 },
|
|
277
|
+
color: FALLBACK_TICK,
|
|
278
|
+
maxTicksLimit: 5,
|
|
279
|
+
padding: 8,
|
|
280
|
+
callback: (v) => `$${Number(v).toLocaleString()}`,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
}), [showXAxis, showYAxis, sliced]);
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<Card
|
|
288
|
+
className={cn("w-full py-4 sm:py-6 gap-2", className)}
|
|
289
|
+
style={{ maxWidth: width }}
|
|
290
|
+
>
|
|
291
|
+
<CardHeader className="px-3 sm:px-6">
|
|
292
|
+
<CardTitle className="text-sm sm:text-base">{title}</CardTitle>
|
|
293
|
+
<CardAction>
|
|
294
|
+
<div className="flex gap-0.5 sm:gap-1">
|
|
295
|
+
{PERIODS.map((p) => (
|
|
296
|
+
<ChartPeriodButton
|
|
297
|
+
key={p}
|
|
298
|
+
period={p}
|
|
299
|
+
active={period === p}
|
|
300
|
+
onClick={() => setPeriod(p)}
|
|
301
|
+
/>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
</CardAction>
|
|
305
|
+
</CardHeader>
|
|
306
|
+
|
|
307
|
+
<CardContent className="px-3 sm:px-6">
|
|
308
|
+
{isLoading ? (
|
|
309
|
+
<Skeleton style={{ height, width: "100%" }} />
|
|
310
|
+
) : !sliced ? (
|
|
311
|
+
<Empty className="flex-none p-4" style={{ height }}>
|
|
312
|
+
<EmptyDescription>No data available</EmptyDescription>
|
|
313
|
+
</Empty>
|
|
314
|
+
) : (
|
|
315
|
+
<div className="flex flex-col gap-2">
|
|
316
|
+
{showLegend && legendPosition === "top" && (
|
|
317
|
+
<ChartLegend primary={brandPrimary} secondary={brandSecondary} />
|
|
318
|
+
)}
|
|
319
|
+
<div style={{ height, width: "100%", position: "relative" }}>
|
|
320
|
+
<Chart
|
|
321
|
+
key={`${brandPrimary}__${brandSecondary}`}
|
|
322
|
+
type="bar"
|
|
323
|
+
data={chartData}
|
|
324
|
+
options={options}
|
|
325
|
+
aria-label={title}
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
{showLegend && legendPosition === "bottom" && (
|
|
329
|
+
<ChartLegend primary={brandPrimary} secondary={brandSecondary} />
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
</CardContent>
|
|
334
|
+
</Card>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities and sub-components for IncomeBarChart and ExpenseBarChart.
|
|
3
|
+
* Not part of the public package API — internal use only.
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Button } from "./button";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export type ChartPeriod = 1 | 3 | 6 | 12;
|
|
13
|
+
export type ChartGranularity = "monthly" | "daily";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Period / slice config
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How many data points to slice per period × granularity combination.
|
|
21
|
+
* monthly: slice by calendar month count.
|
|
22
|
+
* daily: slice by approximate day count (1M=30d, 3M=90d, 6M=180d, 12M=365d).
|
|
23
|
+
*/
|
|
24
|
+
export const CHART_SLICE_COUNT: Record<ChartGranularity, Record<ChartPeriod, number>> = {
|
|
25
|
+
monthly: { 1: 1, 3: 3, 6: 6, 12: 12 },
|
|
26
|
+
daily: { 1: 30, 3: 90, 6: 180, 12: 365 },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Period buttons shown per granularity. monthly hides 1M; daily shows all four. */
|
|
30
|
+
export const CHART_PERIODS: Record<ChartGranularity, ChartPeriod[]> = {
|
|
31
|
+
monthly: [3, 6, 12],
|
|
32
|
+
daily: [1, 3, 6, 12],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Chart.js helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export function hexToRgba(hex: string, alpha: number): string {
|
|
40
|
+
const clean = hex.replace("#", "");
|
|
41
|
+
const full =
|
|
42
|
+
clean.length === 3
|
|
43
|
+
? clean.split("").map((c) => c + c).join("")
|
|
44
|
+
: clean;
|
|
45
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
46
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
47
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
48
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Opacity steps to derive distinct shades from a single brand color (up to 6 datasets). */
|
|
52
|
+
export const DATASET_ALPHAS = [1, 0.72, 0.52, 0.36, 0.24, 0.15];
|
|
53
|
+
|
|
54
|
+
export const FALLBACK_TICK = "#9EAAB5";
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Tooltip date format
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format an ISO date string for the chart tooltip title.
|
|
62
|
+
* Daily granularity includes the day; monthly shows only month + year.
|
|
63
|
+
*/
|
|
64
|
+
export function formatTooltipDate(iso: string, granularity: ChartGranularity): string {
|
|
65
|
+
const d = new Date(iso);
|
|
66
|
+
return d.toLocaleDateString("en-US",
|
|
67
|
+
granularity === "daily"
|
|
68
|
+
? { month: "short", day: "numeric", year: "numeric" }
|
|
69
|
+
: { month: "short", year: "numeric" }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Sub-components
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
export function ChartLegendItem({ label, color }: { label: string; color: string }) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex items-center gap-1.5">
|
|
80
|
+
<div style={{ width: 10, height: 10, backgroundColor: color, flexShrink: 0 }} />
|
|
81
|
+
<span className="text-[11px] text-muted-foreground leading-none">{label}</span>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function ChartPeriodButton({
|
|
87
|
+
period,
|
|
88
|
+
active,
|
|
89
|
+
onClick,
|
|
90
|
+
}: {
|
|
91
|
+
period: ChartPeriod;
|
|
92
|
+
active: boolean;
|
|
93
|
+
onClick: () => void;
|
|
94
|
+
}) {
|
|
95
|
+
return (
|
|
96
|
+
<Button variant={active ? "default" : "outline"} size="xs" onClick={onClick}>
|
|
97
|
+
{period}M
|
|
98
|
+
</Button>
|
|
99
|
+
);
|
|
100
|
+
}
|