@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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox — WealthX Design System
|
|
3
|
+
* Base: @base-ui/react/combobox
|
|
4
|
+
* WealthX overrides: square corners (rounded-none), design token colors,
|
|
5
|
+
* consistent styling with Select component, built-in search filtering.
|
|
6
|
+
*/
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react"
|
|
9
|
+
import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"
|
|
10
|
+
|
|
11
|
+
import { cn } from "@/lib/utils"
|
|
12
|
+
|
|
13
|
+
export type ComboboxProps<Value = string, Multiple extends boolean | undefined = false> =
|
|
14
|
+
React.ComponentProps<typeof ComboboxPrimitive.Root<Value, Multiple>>
|
|
15
|
+
|
|
16
|
+
function Combobox<Value, Multiple extends boolean | undefined = false>({
|
|
17
|
+
...props
|
|
18
|
+
}: React.ComponentProps<typeof ComboboxPrimitive.Root<Value, Multiple>>) {
|
|
19
|
+
return <ComboboxPrimitive.Root data-slot="combobox" {...props} />
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ComboboxTriggerProps = React.ComponentProps<typeof ComboboxPrimitive.Trigger> & {
|
|
23
|
+
size?: "sm" | "default"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ComboboxTrigger({
|
|
27
|
+
className,
|
|
28
|
+
size = "default",
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: ComboboxTriggerProps) {
|
|
32
|
+
return (
|
|
33
|
+
<ComboboxPrimitive.Trigger
|
|
34
|
+
data-slot="combobox-trigger"
|
|
35
|
+
data-size={size}
|
|
36
|
+
className={cn(
|
|
37
|
+
"flex w-fit items-center justify-between gap-2 border border-input bg-transparent px-3 py-2 text-body-small whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary/20 data-[popup-open]:border-primary data-[popup-open]:ring-[3px] data-[popup-open]:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=combobox-value]:line-clamp-1 *:data-[slot=combobox-value]:flex *:data-[slot=combobox-value]:items-center *:data-[slot=combobox-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
38
|
+
className
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
<ComboboxPrimitive.Icon className="transition-transform duration-200 data-[popup-open]:rotate-180">
|
|
44
|
+
<ChevronDownIcon className="size-4 opacity-50" />
|
|
45
|
+
</ComboboxPrimitive.Icon>
|
|
46
|
+
</ComboboxPrimitive.Trigger>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ComboboxValueProps = React.ComponentProps<typeof ComboboxPrimitive.Value>
|
|
51
|
+
|
|
52
|
+
function ComboboxValue({
|
|
53
|
+
...props
|
|
54
|
+
}: ComboboxValueProps) {
|
|
55
|
+
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ComboboxInputProps = React.ComponentProps<typeof ComboboxPrimitive.Input>
|
|
59
|
+
|
|
60
|
+
function ComboboxInput({
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}: ComboboxInputProps) {
|
|
64
|
+
return (
|
|
65
|
+
<div data-slot="combobox-input-wrapper" className="flex items-center gap-2 border-b border-border px-3">
|
|
66
|
+
<SearchIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
67
|
+
<ComboboxPrimitive.Input
|
|
68
|
+
data-slot="combobox-input"
|
|
69
|
+
className={cn(
|
|
70
|
+
"h-9 w-full min-w-0 bg-transparent py-1 text-body-small outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type ComboboxContentProps = React.ComponentProps<typeof ComboboxPrimitive.Popup>
|
|
80
|
+
|
|
81
|
+
function ComboboxContent({
|
|
82
|
+
className,
|
|
83
|
+
children,
|
|
84
|
+
...props
|
|
85
|
+
}: ComboboxContentProps) {
|
|
86
|
+
return (
|
|
87
|
+
<ComboboxPrimitive.Portal>
|
|
88
|
+
<ComboboxPrimitive.Positioner sideOffset={4} align="start">
|
|
89
|
+
<ComboboxPrimitive.Popup
|
|
90
|
+
data-slot="combobox-content"
|
|
91
|
+
className={cn(
|
|
92
|
+
"relative z-50 max-h-[var(--available-height)] min-w-[8rem] overflow-hidden border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[ending-style]:animate-out data-[ending-style]:fade-out-0 data-[ending-style]:zoom-out-95 data-[open]:animate-in data-[open]:fade-in-0 data-[open]:zoom-in-95",
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
{...props}
|
|
96
|
+
>
|
|
97
|
+
{children}
|
|
98
|
+
</ComboboxPrimitive.Popup>
|
|
99
|
+
</ComboboxPrimitive.Positioner>
|
|
100
|
+
</ComboboxPrimitive.Portal>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type ComboboxListProps = React.ComponentProps<typeof ComboboxPrimitive.List>
|
|
105
|
+
|
|
106
|
+
function ComboboxList({
|
|
107
|
+
className,
|
|
108
|
+
...props
|
|
109
|
+
}: ComboboxListProps) {
|
|
110
|
+
return (
|
|
111
|
+
<ComboboxPrimitive.List
|
|
112
|
+
data-slot="combobox-list"
|
|
113
|
+
className={cn(
|
|
114
|
+
"max-h-[min(var(--available-height),18rem)] overflow-y-auto p-1",
|
|
115
|
+
className
|
|
116
|
+
)}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ComboboxItemProps = React.ComponentProps<typeof ComboboxPrimitive.Item>
|
|
123
|
+
|
|
124
|
+
function ComboboxItem({
|
|
125
|
+
className,
|
|
126
|
+
children,
|
|
127
|
+
...props
|
|
128
|
+
}: ComboboxItemProps) {
|
|
129
|
+
return (
|
|
130
|
+
<ComboboxPrimitive.Item
|
|
131
|
+
data-slot="combobox-item"
|
|
132
|
+
className={cn(
|
|
133
|
+
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-body-small outline-hidden select-none data-[highlighted]:bg-primary/5 data-[highlighted]:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
>
|
|
138
|
+
<span
|
|
139
|
+
data-slot="combobox-item-indicator"
|
|
140
|
+
className="absolute right-2 flex size-3.5 items-center justify-center"
|
|
141
|
+
>
|
|
142
|
+
<ComboboxPrimitive.ItemIndicator>
|
|
143
|
+
<CheckIcon className="size-4" />
|
|
144
|
+
</ComboboxPrimitive.ItemIndicator>
|
|
145
|
+
</span>
|
|
146
|
+
{children}
|
|
147
|
+
</ComboboxPrimitive.Item>
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type ComboboxEmptyProps = React.ComponentProps<typeof ComboboxPrimitive.Empty>
|
|
152
|
+
|
|
153
|
+
function ComboboxEmpty({
|
|
154
|
+
className,
|
|
155
|
+
...props
|
|
156
|
+
}: ComboboxEmptyProps) {
|
|
157
|
+
return (
|
|
158
|
+
<ComboboxPrimitive.Empty
|
|
159
|
+
data-slot="combobox-empty"
|
|
160
|
+
className={`text-body-small ${cn("py-6 text-center text-muted-foreground empty:hidden", className)}`}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type ComboboxGroupProps = React.ComponentProps<typeof ComboboxPrimitive.Group>
|
|
167
|
+
|
|
168
|
+
function ComboboxGroup({
|
|
169
|
+
...props
|
|
170
|
+
}: ComboboxGroupProps) {
|
|
171
|
+
return <ComboboxPrimitive.Group data-slot="combobox-group" {...props} />
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type ComboboxGroupLabelProps = React.ComponentProps<typeof ComboboxPrimitive.GroupLabel>
|
|
175
|
+
|
|
176
|
+
function ComboboxGroupLabel({
|
|
177
|
+
className,
|
|
178
|
+
...props
|
|
179
|
+
}: ComboboxGroupLabelProps) {
|
|
180
|
+
return (
|
|
181
|
+
<ComboboxPrimitive.GroupLabel
|
|
182
|
+
data-slot="combobox-group-label"
|
|
183
|
+
className={`text-label-small ${cn("px-2 py-1.5 uppercase text-muted-foreground", className)}`}
|
|
184
|
+
{...props}
|
|
185
|
+
/>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type ComboboxSeparatorProps = React.ComponentProps<"div">
|
|
190
|
+
|
|
191
|
+
function ComboboxSeparator({
|
|
192
|
+
className,
|
|
193
|
+
...props
|
|
194
|
+
}: ComboboxSeparatorProps) {
|
|
195
|
+
return (
|
|
196
|
+
<div
|
|
197
|
+
role="separator"
|
|
198
|
+
data-slot="combobox-separator"
|
|
199
|
+
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
|
200
|
+
{...props}
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export {
|
|
206
|
+
Combobox,
|
|
207
|
+
ComboboxTrigger,
|
|
208
|
+
ComboboxValue,
|
|
209
|
+
ComboboxInput,
|
|
210
|
+
ComboboxContent,
|
|
211
|
+
ComboboxList,
|
|
212
|
+
ComboboxItem,
|
|
213
|
+
ComboboxEmpty,
|
|
214
|
+
ComboboxGroup,
|
|
215
|
+
ComboboxGroupLabel,
|
|
216
|
+
ComboboxSeparator,
|
|
217
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef, 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
|
+
DATASET_ALPHAS,
|
|
21
|
+
FALLBACK_TICK,
|
|
22
|
+
CHART_SLICE_COUNT,
|
|
23
|
+
CHART_PERIODS,
|
|
24
|
+
formatTooltipDate,
|
|
25
|
+
ChartLegendItem,
|
|
26
|
+
ChartPeriodButton,
|
|
27
|
+
type ChartPeriod,
|
|
28
|
+
type ChartGranularity,
|
|
29
|
+
} from "./chart-shared";
|
|
30
|
+
|
|
31
|
+
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Types
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export interface ExpenseDataset {
|
|
38
|
+
/** Expense category label, e.g. "Housing", "Food", "Transport" */
|
|
39
|
+
label: string;
|
|
40
|
+
/** One value per data point, aligned to the months array */
|
|
41
|
+
data: number[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ExpenseBarChartData {
|
|
45
|
+
/** Display labels, e.g. ["Jul", "Aug"] for monthly or ["Mar 8", "Mar 9"] for daily */
|
|
46
|
+
months: string[];
|
|
47
|
+
/** Optional ISO date strings per point — used for the tooltip title */
|
|
48
|
+
dates?: string[];
|
|
49
|
+
datasets: ExpenseDataset[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ExpensePeriod = ChartPeriod;
|
|
53
|
+
export type ExpenseGranularity = ChartGranularity;
|
|
54
|
+
|
|
55
|
+
export interface ExpenseBarChartProps {
|
|
56
|
+
/** Full dataset — sliced to the selected period */
|
|
57
|
+
expenseData: ExpenseBarChartData | null;
|
|
58
|
+
title?: string;
|
|
59
|
+
/** Show or hide the chart legend */
|
|
60
|
+
showLegend?: boolean;
|
|
61
|
+
/** Show or hide X axis labels */
|
|
62
|
+
showXAxis?: boolean;
|
|
63
|
+
/** Show or hide Y axis labels */
|
|
64
|
+
showYAxis?: boolean;
|
|
65
|
+
/** Legend placement relative to chart */
|
|
66
|
+
legendPosition?: "top" | "bottom";
|
|
67
|
+
/** Default period selector value */
|
|
68
|
+
defaultPeriod?: ExpensePeriod;
|
|
69
|
+
/**
|
|
70
|
+
* Data granularity — controls available period buttons and slice counts.
|
|
71
|
+
* "monthly" (default): shows 3M / 6M / 12M, slices by month count.
|
|
72
|
+
* "daily": shows 1M / 3M / 6M / 12M, slices by day count (1M=30, 3M=90, etc.).
|
|
73
|
+
*/
|
|
74
|
+
granularity?: ExpenseGranularity;
|
|
75
|
+
/** Chart canvas height in pixels */
|
|
76
|
+
height?: number;
|
|
77
|
+
/** Width of the card in pixels */
|
|
78
|
+
width?: number | string;
|
|
79
|
+
className?: string;
|
|
80
|
+
/** Show skeleton loading state instead of the chart */
|
|
81
|
+
isLoading?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Constants
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const FALLBACK_SECONDARY = "#162029";
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Component
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export function ExpenseBarChart({
|
|
95
|
+
expenseData,
|
|
96
|
+
title = "Expenses",
|
|
97
|
+
showLegend = true,
|
|
98
|
+
showXAxis = true,
|
|
99
|
+
showYAxis = true,
|
|
100
|
+
legendPosition = "top",
|
|
101
|
+
defaultPeriod = 6,
|
|
102
|
+
granularity = "monthly",
|
|
103
|
+
height = 280,
|
|
104
|
+
width = "100%",
|
|
105
|
+
className,
|
|
106
|
+
isLoading = false,
|
|
107
|
+
}: ExpenseBarChartProps) {
|
|
108
|
+
const periods = CHART_PERIODS[granularity];
|
|
109
|
+
const [period, setPeriod] = useState<ExpensePeriod>(defaultPeriod);
|
|
110
|
+
|
|
111
|
+
// Reset period when granularity changes, but not on initial mount
|
|
112
|
+
// (defaultPeriod handles the initial value).
|
|
113
|
+
const isFirstRender = useRef(true);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (isFirstRender.current) { isFirstRender.current = false; return; }
|
|
116
|
+
setPeriod(CHART_PERIODS[granularity][0]);
|
|
117
|
+
}, [granularity]);
|
|
118
|
+
|
|
119
|
+
const themeVars = useThemeVars();
|
|
120
|
+
const brandSecondary: string =
|
|
121
|
+
(themeVars["--theme-secondary"] as string | undefined) || FALLBACK_SECONDARY;
|
|
122
|
+
|
|
123
|
+
const sliced = useMemo<ExpenseBarChartData | null>(() => {
|
|
124
|
+
if (!expenseData?.months?.length || !expenseData.datasets.length) return null;
|
|
125
|
+
const count = Math.min(CHART_SLICE_COUNT[granularity][period], expenseData.months.length);
|
|
126
|
+
const start = expenseData.months.length - count;
|
|
127
|
+
return {
|
|
128
|
+
months: expenseData.months.slice(start),
|
|
129
|
+
dates: expenseData.dates?.slice(start),
|
|
130
|
+
datasets: expenseData.datasets.map((ds) => ({
|
|
131
|
+
...ds,
|
|
132
|
+
data: ds.data.slice(start),
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}, [expenseData, period, granularity]);
|
|
136
|
+
|
|
137
|
+
const datasetColors = useMemo(
|
|
138
|
+
() => sliced?.datasets.map((_, i) =>
|
|
139
|
+
hexToRgba(brandSecondary, DATASET_ALPHAS[i % DATASET_ALPHAS.length])
|
|
140
|
+
) ?? [],
|
|
141
|
+
[sliced, brandSecondary]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const chartData = useMemo<ChartData<"bar">>(() => {
|
|
145
|
+
if (!sliced) return { labels: [], datasets: [] };
|
|
146
|
+
return {
|
|
147
|
+
labels: sliced.months,
|
|
148
|
+
datasets: sliced.datasets.map((ds, i) => ({
|
|
149
|
+
label: ds.label,
|
|
150
|
+
data: ds.data,
|
|
151
|
+
backgroundColor: datasetColors[i],
|
|
152
|
+
hoverBackgroundColor: hexToRgba(
|
|
153
|
+
brandSecondary,
|
|
154
|
+
Math.min(DATASET_ALPHAS[i % DATASET_ALPHAS.length] + 0.15, 1)
|
|
155
|
+
),
|
|
156
|
+
borderWidth: 0,
|
|
157
|
+
borderRadius: 0,
|
|
158
|
+
borderSkipped: false,
|
|
159
|
+
barPercentage: 0.75,
|
|
160
|
+
categoryPercentage: 0.7,
|
|
161
|
+
stack: "expense",
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
164
|
+
}, [sliced, datasetColors, brandSecondary]);
|
|
165
|
+
|
|
166
|
+
const options = useMemo<ChartOptions<"bar">>(() => ({
|
|
167
|
+
responsive: true,
|
|
168
|
+
maintainAspectRatio: false,
|
|
169
|
+
animation: { duration: 800, easing: "easeOutQuart" },
|
|
170
|
+
layout: { padding: 0 },
|
|
171
|
+
plugins: {
|
|
172
|
+
legend: { display: false },
|
|
173
|
+
tooltip: {
|
|
174
|
+
mode: "index",
|
|
175
|
+
intersect: false,
|
|
176
|
+
padding: 12,
|
|
177
|
+
cornerRadius: 0,
|
|
178
|
+
titleFont: { size: 11, weight: "600" },
|
|
179
|
+
bodyFont: { size: 12, weight: "500" },
|
|
180
|
+
callbacks: {
|
|
181
|
+
title: (tooltipItems) => {
|
|
182
|
+
const idx = tooltipItems[0]?.dataIndex;
|
|
183
|
+
if (idx != null && sliced?.dates?.[idx]) {
|
|
184
|
+
return formatTooltipDate(sliced.dates[idx], granularity);
|
|
185
|
+
}
|
|
186
|
+
return tooltipItems[0]?.label ?? "";
|
|
187
|
+
},
|
|
188
|
+
label: (ctx) => {
|
|
189
|
+
const val = ctx.raw as number;
|
|
190
|
+
if (val === 0) return null;
|
|
191
|
+
return ` ${ctx.dataset.label}: $${val.toLocaleString()}`;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
scales: {
|
|
197
|
+
x: {
|
|
198
|
+
display: showXAxis,
|
|
199
|
+
stacked: true,
|
|
200
|
+
grid: { display: false },
|
|
201
|
+
border: { display: false },
|
|
202
|
+
ticks: { font: { size: 10 }, color: FALLBACK_TICK },
|
|
203
|
+
},
|
|
204
|
+
y: {
|
|
205
|
+
display: showYAxis,
|
|
206
|
+
stacked: true,
|
|
207
|
+
grid: { display: false },
|
|
208
|
+
border: { display: false },
|
|
209
|
+
ticks: {
|
|
210
|
+
font: { size: 10 },
|
|
211
|
+
color: FALLBACK_TICK,
|
|
212
|
+
maxTicksLimit: 5,
|
|
213
|
+
padding: 8,
|
|
214
|
+
callback: (v) => `$${Number(v).toLocaleString()}`,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
}), [showXAxis, showYAxis, sliced, granularity]);
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Card
|
|
222
|
+
className={cn("w-full py-4 sm:py-6 gap-2", className)}
|
|
223
|
+
style={{ maxWidth: width }}
|
|
224
|
+
>
|
|
225
|
+
<CardHeader className="px-3 sm:px-6">
|
|
226
|
+
<CardTitle className="text-sm sm:text-base">{title}</CardTitle>
|
|
227
|
+
<CardAction>
|
|
228
|
+
<div className="flex gap-0.5 sm:gap-1">
|
|
229
|
+
{periods.map((p) => (
|
|
230
|
+
<ChartPeriodButton
|
|
231
|
+
key={p}
|
|
232
|
+
period={p}
|
|
233
|
+
active={period === p}
|
|
234
|
+
onClick={() => setPeriod(p)}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
</CardAction>
|
|
239
|
+
</CardHeader>
|
|
240
|
+
|
|
241
|
+
<CardContent className="px-3 sm:px-6">
|
|
242
|
+
{isLoading ? (
|
|
243
|
+
<Skeleton style={{ height, width: "100%" }} />
|
|
244
|
+
) : !sliced ? (
|
|
245
|
+
<Empty className="flex-none p-4" style={{ height }}>
|
|
246
|
+
<EmptyDescription>No data available</EmptyDescription>
|
|
247
|
+
</Empty>
|
|
248
|
+
) : (
|
|
249
|
+
<div className="flex flex-col gap-2">
|
|
250
|
+
{showLegend && legendPosition === "top" && (
|
|
251
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1.5 pb-2">
|
|
252
|
+
{sliced.datasets.map((ds, i) => (
|
|
253
|
+
<ChartLegendItem key={ds.label} label={ds.label} color={datasetColors[i]} />
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
<div style={{ height, width: "100%", position: "relative" }}>
|
|
258
|
+
<Chart
|
|
259
|
+
key={brandSecondary}
|
|
260
|
+
type="bar"
|
|
261
|
+
data={chartData}
|
|
262
|
+
options={options}
|
|
263
|
+
aria-label={title}
|
|
264
|
+
/>
|
|
265
|
+
</div>
|
|
266
|
+
{showLegend && legendPosition === "bottom" && (
|
|
267
|
+
<div className="flex flex-wrap gap-x-3 gap-y-1.5 pt-2">
|
|
268
|
+
{sliced.datasets.map((ds, i) => (
|
|
269
|
+
<ChartLegendItem key={ds.label} label={ds.label} color={datasetColors[i]} />
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
</CardContent>
|
|
276
|
+
</Card>
|
|
277
|
+
);
|
|
278
|
+
}
|