feat(02-02): create IncomeBarChart and SpendBarChart components
- IncomeBarChart: vertical grouped bars (budgeted muted, actual vivid) - SpendBarChart: horizontal bars via layout="vertical" with swapped axes - Both use per-cell conditional fill for over-budget red accent - Both use ChartContainer + ChartConfig for CSS variable theming - Both handle empty data with ChartEmptyState placeholder - ChartLegend and ChartTooltip with formatted currency values
This commit is contained in:
74
src/components/dashboard/charts/IncomeBarChart.tsx
Normal file
74
src/components/dashboard/charts/IncomeBarChart.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
} from "recharts"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
} from "@/components/ui/chart"
|
||||
import type { ChartConfig } from "@/components/ui/chart"
|
||||
import { ChartEmptyState } from "./ChartEmptyState"
|
||||
import { formatCurrency } from "@/lib/format"
|
||||
|
||||
interface IncomeBarChartProps {
|
||||
data: Array<{ label: string; budgeted: number; actual: number }>
|
||||
currency: string
|
||||
emptyMessage: string
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
|
||||
actual: { label: "Actual", color: "var(--color-income-fill)" },
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function IncomeBarChart({
|
||||
data,
|
||||
currency,
|
||||
emptyMessage,
|
||||
}: IncomeBarChartProps) {
|
||||
if (data.length === 0) {
|
||||
return <ChartEmptyState message={emptyMessage} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="min-h-[250px] w-full">
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
formatter={(value) => formatCurrency(Number(value), currency)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="budgeted"
|
||||
fill="var(--color-budgeted)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar dataKey="actual" radius={[4, 4, 0, 0]}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={index}
|
||||
fill={
|
||||
entry.actual > entry.budgeted
|
||||
? "var(--color-over-budget)"
|
||||
: "var(--color-income-fill)"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
84
src/components/dashboard/charts/SpendBarChart.tsx
Normal file
84
src/components/dashboard/charts/SpendBarChart.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
} from "recharts"
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
} from "@/components/ui/chart"
|
||||
import type { ChartConfig } from "@/components/ui/chart"
|
||||
import { ChartEmptyState } from "./ChartEmptyState"
|
||||
import { formatCurrency } from "@/lib/format"
|
||||
|
||||
interface SpendBarChartProps {
|
||||
data: Array<{
|
||||
type: string
|
||||
label: string
|
||||
budgeted: number
|
||||
actual: number
|
||||
}>
|
||||
currency: string
|
||||
emptyMessage: string
|
||||
}
|
||||
|
||||
const chartConfig = {
|
||||
budgeted: { label: "Budgeted", color: "var(--color-budget-bar-bg)" },
|
||||
actual: { label: "Actual", color: "var(--color-muted-foreground)" },
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function SpendBarChart({
|
||||
data,
|
||||
currency,
|
||||
emptyMessage,
|
||||
}: SpendBarChartProps) {
|
||||
if (data.length === 0) {
|
||||
return <ChartEmptyState message={emptyMessage} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="min-h-[250px] w-full">
|
||||
<BarChart layout="vertical" data={data}>
|
||||
<CartesianGrid horizontal={false} />
|
||||
<XAxis type="number" hide />
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="label"
|
||||
width={120}
|
||||
tick={{ fontSize: 12 }}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
formatter={(value) => formatCurrency(Number(value), currency)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
<Bar
|
||||
dataKey="budgeted"
|
||||
fill="var(--color-budgeted)"
|
||||
radius={4}
|
||||
/>
|
||||
<Bar dataKey="actual" radius={4}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={index}
|
||||
fill={
|
||||
entry.actual > entry.budgeted
|
||||
? "var(--color-over-budget)"
|
||||
: `var(--color-${entry.type}-fill)`
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user