292 lines
7.9 KiB
TypeScript
292 lines
7.9 KiB
TypeScript
import * as React from "react";
|
||
import { Box, Typography, ToggleButtonGroup, ToggleButton, Paper } from "@mui/material";
|
||
|
||
export interface ChartDataPoint {
|
||
id: string;
|
||
amount: number;
|
||
compareAmount?: number;
|
||
compareLabel?: string;
|
||
highlighted?: boolean;
|
||
}
|
||
|
||
export interface ChartSeries {
|
||
rolling: ChartDataPoint[];
|
||
calendar: ChartDataPoint[];
|
||
}
|
||
|
||
export interface ChartData {
|
||
daily: ChartDataPoint[];
|
||
weekly: ChartSeries;
|
||
monthly: ChartSeries;
|
||
}
|
||
|
||
export interface HistoryChartProps {
|
||
header: string;
|
||
summary?: string;
|
||
tabs: string[];
|
||
data: ChartData;
|
||
period: "rolling" | "calendar";
|
||
onPeriodChange: (mode: "rolling" | "calendar") => void;
|
||
comparison: boolean;
|
||
setComparison: (mode: boolean) => void;
|
||
}
|
||
|
||
const formatDisplay = (
|
||
point: ChartDataPoint,
|
||
tab: string,
|
||
comparison: boolean
|
||
) => {
|
||
const base = point.amount;
|
||
const cmp = point.compareAmount ?? 0;
|
||
|
||
const formatShort = (val: number) => {
|
||
if (tab === "monthly") {
|
||
if (val >= 100000) return `${(val / 100000).toFixed(2)}L`;
|
||
}
|
||
if (tab === "weekly") {
|
||
if (val >= 1000) return `${(val / 1000).toFixed(1)}K`;
|
||
}
|
||
return val.toLocaleString("en-IN");
|
||
};
|
||
|
||
// Only hide diff when comparison OFF or compare is undefined
|
||
if (!comparison) {
|
||
return `₹ ${formatShort(base)}`;
|
||
}
|
||
|
||
const diff = base - cmp;
|
||
const sign = diff >= 0 ? "+" : "-";
|
||
const absDiff = Math.abs(diff);
|
||
|
||
return `₹ ${formatShort(base)} (${sign}${formatShort(absDiff)})`;
|
||
};
|
||
|
||
const formatLabel = (label: string, type: string) => {
|
||
if (type === "monthly") return label;
|
||
|
||
if (type === "weekly") {
|
||
const parts = label.split(" - ");
|
||
if (parts.length === 2) {
|
||
const [start, end] = parts;
|
||
const startDay = start.split(" ")[0];
|
||
const endParts = end.split(" ");
|
||
const endDay = endParts[0];
|
||
const month = endParts[1];
|
||
return `${startDay}–${endDay} ${month}`;
|
||
}
|
||
}
|
||
|
||
return label;
|
||
};
|
||
|
||
export default function HistoryChart({
|
||
header,
|
||
summary,
|
||
tabs,
|
||
data,
|
||
period,
|
||
onPeriodChange,
|
||
comparison,
|
||
setComparison,
|
||
}: HistoryChartProps) {
|
||
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
||
|
||
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
||
if (newTab !== null) setActiveTab(newTab);
|
||
};
|
||
|
||
const activeDataKey = activeTab.toLowerCase() as keyof ChartData;
|
||
|
||
let rawData: ChartDataPoint[] = [];
|
||
|
||
if (activeDataKey === "daily") {
|
||
rawData = data.daily || [];
|
||
} else {
|
||
const section = data[activeDataKey];
|
||
rawData = section?.[period] || [];
|
||
}
|
||
|
||
const currentData = rawData;
|
||
|
||
const maxAmount =
|
||
currentData.length > 0
|
||
? Math.max(
|
||
...currentData.flatMap((d) =>
|
||
comparison ? [d.amount, d.compareAmount || 0] : [d.amount]
|
||
),
|
||
1
|
||
)
|
||
: 1;
|
||
|
||
return (
|
||
<Paper
|
||
sx={{
|
||
p: { xs: 2, sm: 4 },
|
||
borderRadius: 4,
|
||
width: "100%",
|
||
boxShadow: "none",
|
||
border: "1px solid",
|
||
borderColor: "divider"
|
||
}}
|
||
>
|
||
<Typography variant="h6" fontWeight={700} gutterBottom>
|
||
{header}
|
||
</Typography>
|
||
|
||
{summary && (
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||
{summary}
|
||
</Typography>
|
||
)}
|
||
|
||
<ToggleButtonGroup
|
||
value={activeTab}
|
||
exclusive
|
||
onChange={handleTabChange}
|
||
fullWidth
|
||
sx={{ mb: 4 }}
|
||
>
|
||
{tabs.map((tab) => (
|
||
<ToggleButton key={tab} value={tab}>
|
||
{tab}
|
||
</ToggleButton>
|
||
))}
|
||
</ToggleButtonGroup>
|
||
|
||
<ToggleButtonGroup
|
||
value={period}
|
||
exclusive
|
||
onChange={(_, v) => v && onPeriodChange(v)}
|
||
size="small"
|
||
sx={{ mb: 2 }}
|
||
>
|
||
<ToggleButton value="rolling">Rolling</ToggleButton>
|
||
<ToggleButton value="calendar" disabled={activeDataKey === "daily"}>
|
||
Calendar
|
||
</ToggleButton>
|
||
</ToggleButtonGroup>
|
||
|
||
<ToggleButtonGroup
|
||
value={comparison ? "on" : "off"}
|
||
exclusive
|
||
onChange={(_, v) => setComparison(v === "on")}
|
||
size="small"
|
||
sx={{ mb: 2 }}
|
||
>
|
||
<ToggleButton value="off">Single</ToggleButton>
|
||
<ToggleButton value="on">Compare</ToggleButton>
|
||
</ToggleButtonGroup>
|
||
|
||
{currentData.length > 0 ? (
|
||
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
|
||
{currentData.map((point) => {
|
||
const currentHeight = (point.amount / maxAmount) * 100;
|
||
const compareHeight = ((point.compareAmount || 0) / maxAmount) * 100;
|
||
|
||
return (
|
||
<Box
|
||
key={point.id}
|
||
sx={{
|
||
flex: 1,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
alignItems: "center",
|
||
justifyContent: "flex-end",
|
||
height: "100%"
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
display: "flex",
|
||
alignItems: "flex-end",
|
||
gap: comparison ? 0.5 : 0,
|
||
height: "100%",
|
||
position: "relative"
|
||
}}
|
||
>
|
||
{comparison && (
|
||
<Box
|
||
sx={{
|
||
width: 6,
|
||
height: `${compareHeight}%`,
|
||
bgcolor: "grey.400",
|
||
borderRadius: 2
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
<Box
|
||
sx={{
|
||
width: 10,
|
||
height: `${currentHeight}%`,
|
||
bgcolor: point.highlighted ? "error.main" : "primary.main",
|
||
borderRadius: 2,
|
||
position: "relative"
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
position: "absolute",
|
||
top: -18,
|
||
left: "50%",
|
||
transform: "translateX(-50%)",
|
||
fontSize: "0.65rem",
|
||
whiteSpace: "nowrap"
|
||
}}
|
||
>
|
||
{formatDisplay(point, activeTab.toLowerCase(), comparison)}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
<Box
|
||
sx={{
|
||
mt: 1,
|
||
textAlign: "center",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
alignItems: "center",
|
||
lineHeight: 1.1
|
||
}}
|
||
>
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontSize: "0.7rem",
|
||
opacity: 0.7,
|
||
color: "text.primary",
|
||
}}
|
||
>
|
||
{formatLabel(point.id, activeDataKey)}
|
||
</Typography>
|
||
|
||
<Typography
|
||
variant="caption"
|
||
sx={{
|
||
fontSize: "0.65rem",
|
||
color: "grey.400",
|
||
visibility:
|
||
comparison && point.compareLabel && activeDataKey !== "daily"
|
||
? "visible"
|
||
: "hidden"
|
||
}}
|
||
>
|
||
{point.compareLabel
|
||
? formatLabel(point.compareLabel, activeDataKey)
|
||
: "placeholder"}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Box>
|
||
) : (
|
||
<Box sx={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||
<Typography color="text.secondary">No Data Available</Typography>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
);
|
||
}
|