refactored HistoryChart to component
This commit is contained in:
@@ -13,7 +13,7 @@ import LatestItemsList, { LatestItem } from "./components/LatestItemsList";
|
|||||||
import HistoryChart from "./components/HistoryChart";
|
import HistoryChart from "./components/HistoryChart";
|
||||||
import {
|
import {
|
||||||
AggregatedDashboardData
|
AggregatedDashboardData
|
||||||
} from "./types/historyChart";
|
} from "./components/HistoryChart";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchLatestTransactions,
|
fetchLatestTransactions,
|
||||||
|
|||||||
@@ -1,391 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
ToggleButtonGroup,
|
|
||||||
ToggleButton,
|
|
||||||
Paper
|
|
||||||
} from "@mui/material";
|
|
||||||
import {
|
|
||||||
ChartDataPoint,
|
|
||||||
HistoryChartProps,
|
|
||||||
ChartData,
|
|
||||||
} from "../types/historyChart";
|
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
||||||
|
|
||||||
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,
|
|
||||||
colorScheme,
|
|
||||||
}: 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;
|
|
||||||
|
|
||||||
const [startIndex, setStartIndex] = React.useState(0);
|
|
||||||
const visibleCountDataTabMapping = {
|
|
||||||
daily: 7,
|
|
||||||
weekly: 6,
|
|
||||||
monthly: 4,
|
|
||||||
}
|
|
||||||
const visibleCount = visibleCountDataTabMapping[activeDataKey];
|
|
||||||
const total = currentData.length;
|
|
||||||
|
|
||||||
// clamp startIndex so we always show full 5 (when possible)
|
|
||||||
const clampedStartIndex = Math.min(
|
|
||||||
startIndex,
|
|
||||||
Math.max(total - visibleCount, 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
const visibleData = currentData.slice(
|
|
||||||
clampedStartIndex,
|
|
||||||
clampedStartIndex + visibleCount
|
|
||||||
);
|
|
||||||
|
|
||||||
const canGoLeft = startIndex > 0;
|
|
||||||
const canGoRight = startIndex + visibleCount < currentData.length;
|
|
||||||
|
|
||||||
const handlePrev = () => {
|
|
||||||
if (canGoLeft) setStartIndex((prev) => prev - visibleCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
if (canGoRight) setStartIndex((prev) => prev + visibleCount);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
p: { xs: 2, sm: 4 },
|
|
||||||
borderRadius: 4,
|
|
||||||
width: "100%",
|
|
||||||
boxShadow: "none",
|
|
||||||
border: "1px solid",
|
|
||||||
borderColor: "divider",
|
|
||||||
bgcolor: colorScheme.light,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography variant="h6" fontWeight={700} gutterBottom color={colorScheme.text}>
|
|
||||||
{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>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
mb: 3
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Rolling / Calendar */}
|
|
||||||
<ToggleButtonGroup
|
|
||||||
value={period}
|
|
||||||
exclusive
|
|
||||||
onChange={(_, v) => v && onPeriodChange(v)}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<ToggleButton value="rolling">Rolling</ToggleButton>
|
|
||||||
<ToggleButton
|
|
||||||
value="calendar"
|
|
||||||
disabled={activeDataKey === "daily"}
|
|
||||||
>
|
|
||||||
Calendar
|
|
||||||
</ToggleButton>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
|
|
||||||
{/* Compare toggle */}
|
|
||||||
<ToggleButton
|
|
||||||
value="compare"
|
|
||||||
selected={comparison}
|
|
||||||
onChange={() => setComparison(!comparison)}
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
textTransform: "none",
|
|
||||||
borderRadius: 2,
|
|
||||||
px: 2,
|
|
||||||
|
|
||||||
// OFF
|
|
||||||
color: "text.secondary",
|
|
||||||
border: "1px solid",
|
|
||||||
borderColor: "divider",
|
|
||||||
|
|
||||||
// ON
|
|
||||||
"&.Mui-selected": {
|
|
||||||
color: "white",
|
|
||||||
bgcolor: "success.main",
|
|
||||||
borderColor: "success.main"
|
|
||||||
},
|
|
||||||
"&.Mui-selected:hover": {
|
|
||||||
bgcolor: "success.dark"
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Compare
|
|
||||||
</ToggleButton>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{currentData.length > 0 ? (
|
|
||||||
<Box sx={{ position: "relative", mt: 4 }}>
|
|
||||||
|
|
||||||
{/* LEFT ARROW */}
|
|
||||||
{canGoLeft && (
|
|
||||||
<IconButton
|
|
||||||
onClick={handlePrev}
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: "50%",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
zIndex: 2,
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
boxShadow: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CHART */}
|
|
||||||
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
|
|
||||||
{visibleData.map((point) => {
|
|
||||||
const currentHeight = (point.amount / maxAmount) * 100;
|
|
||||||
const compareHeight = comparison
|
|
||||||
? ((point.compareAmount || 0) / maxAmount) * 100
|
|
||||||
: 0;
|
|
||||||
const labelHeight = Math.max(currentHeight, compareHeight);
|
|
||||||
|
|
||||||
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"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: `${labelHeight}%`,
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -6px)",
|
|
||||||
fontSize: "0.65rem",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
pointerEvents: "none"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{formatDisplay(point, activeTab.toLowerCase(), comparison)}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{/* Compare */}
|
|
||||||
{comparison && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 6,
|
|
||||||
height: `${compareHeight}%`,
|
|
||||||
bgcolor: `${colorScheme.primary}55`,
|
|
||||||
borderRadius: 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Spacer */}
|
|
||||||
<Box sx={{ width: 4 }} />
|
|
||||||
|
|
||||||
{/* Current */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: 10,
|
|
||||||
height: `${currentHeight}%`,
|
|
||||||
bgcolor: point.highlighted
|
|
||||||
? colorScheme.primary
|
|
||||||
: `${colorScheme.primary}99`,
|
|
||||||
borderRadius: 2
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* RIGHT ARROW */}
|
|
||||||
{canGoRight && (
|
|
||||||
<IconButton
|
|
||||||
onClick={handleNext}
|
|
||||||
size="small"
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
right: 0,
|
|
||||||
top: "50%",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
zIndex: 2,
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
boxShadow: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
||||||
<Typography color="text.secondary">No Data Available</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,17 @@
|
|||||||
|
export interface _ChartDataPoint {
|
||||||
export interface ChartDataPoint {
|
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
compareAmount?: number;
|
|
||||||
compareLabel?: string;
|
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChartSeries {
|
export interface ChartDataPoint extends _ChartDataPoint {
|
||||||
rolling: ChartDataPoint[];
|
compare?: _ChartDataPoint;
|
||||||
calendar: ChartDataPoint[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChartData {
|
export interface ChartData {
|
||||||
daily: ChartDataPoint[];
|
daily?: ChartDataPoint[];
|
||||||
weekly: ChartSeries;
|
weekly?: Record<string, ChartDataPoint[]>;
|
||||||
monthly: ChartSeries;
|
monthly?: Record<string, ChartDataPoint[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AggregatedDashboardData {
|
export interface AggregatedDashboardData {
|
||||||
@@ -30,8 +26,12 @@ export interface HistoryChartProps {
|
|||||||
tabs: string[];
|
tabs: string[];
|
||||||
data: ChartData;
|
data: ChartData;
|
||||||
period: "rolling" | "calendar";
|
period: "rolling" | "calendar";
|
||||||
onPeriodChange: (mode: "rolling" | "calendar") => void;
|
onPeriodChange: (p: "rolling" | "calendar") => void;
|
||||||
comparison: boolean;
|
comparison: boolean;
|
||||||
setComparison: (mode: boolean) => void;
|
setComparison: (v: boolean) => void;
|
||||||
colorScheme: any;
|
colorScheme: {
|
||||||
|
primary: string;
|
||||||
|
light: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
60
src/components/HistoryChart/HistoryChart.tsx
Normal file
60
src/components/HistoryChart/HistoryChart.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { ChartDataPoint, HistoryChartProps, ChartData } from "./HistoryChart.models";
|
||||||
|
import HistoryChartView from "./HistoryChart.view";
|
||||||
|
|
||||||
|
export default function HistoryChart(props: HistoryChartProps) {
|
||||||
|
const { tabs, data, period, comparison } = props;
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = React.useState<string>(tabs[0] || "");
|
||||||
|
const [startIndex, setStartIndex] = React.useState(0);
|
||||||
|
|
||||||
|
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.compare?.amount ?? 0] : [d.amount]
|
||||||
|
),
|
||||||
|
1
|
||||||
|
)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const visibleCountMap = { daily: 7, weekly: 6, monthly: 4 };
|
||||||
|
const visibleCount = visibleCountMap[activeDataKey];
|
||||||
|
|
||||||
|
const total = currentData.length;
|
||||||
|
|
||||||
|
const clampedStartIndex = Math.min(startIndex, Math.max(total - visibleCount, 0));
|
||||||
|
|
||||||
|
const visibleData = currentData.slice(
|
||||||
|
clampedStartIndex,
|
||||||
|
clampedStartIndex + visibleCount
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HistoryChartView
|
||||||
|
{...props}
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
currentData={currentData}
|
||||||
|
visibleData={visibleData}
|
||||||
|
maxAmount={maxAmount}
|
||||||
|
visibleCount={visibleCount}
|
||||||
|
startIndex={startIndex}
|
||||||
|
setStartIndex={setStartIndex}
|
||||||
|
activeDataKey={activeDataKey}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/HistoryChart/HistoryChart.utils.ts
Normal file
43
src/components/HistoryChart/HistoryChart.utils.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { ChartDataPoint } from "./HistoryChart.models";
|
||||||
|
|
||||||
|
export const formatDisplay = (
|
||||||
|
point: ChartDataPoint,
|
||||||
|
tab: string,
|
||||||
|
comparison: boolean
|
||||||
|
) => {
|
||||||
|
const base = point.amount;
|
||||||
|
const cmp = point.compare?.amount ?? 0;
|
||||||
|
|
||||||
|
const formatShort = (val: number) => {
|
||||||
|
if (tab === "monthly" && val >= 100000) {
|
||||||
|
return `${(val / 100000).toFixed(2)}L`;
|
||||||
|
}
|
||||||
|
if (tab === "weekly" && val >= 1000) {
|
||||||
|
return `${(val / 1000).toFixed(1)}K`;
|
||||||
|
}
|
||||||
|
return val.toLocaleString("en-IN");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!comparison) return `₹ ${formatShort(base)}`;
|
||||||
|
|
||||||
|
const diff = base - cmp;
|
||||||
|
const sign = diff >= 0 ? "+" : "-";
|
||||||
|
|
||||||
|
return `₹ ${formatShort(base)} (${sign}${formatShort(Math.abs(diff))})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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 [endDay, month] = end.split(" ");
|
||||||
|
return `${startDay}–${endDay} ${month}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
238
src/components/HistoryChart/HistoryChart.view.tsx
Normal file
238
src/components/HistoryChart/HistoryChart.view.tsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
ToggleButton,
|
||||||
|
Paper
|
||||||
|
} from "@mui/material";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
|
||||||
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
|
import {
|
||||||
|
ChartDataPoint,
|
||||||
|
HistoryChartProps,
|
||||||
|
} from "./HistoryChart.models";
|
||||||
|
import { formatDisplay, formatLabel } from "./HistoryChart.utils";
|
||||||
|
|
||||||
|
interface ViewProps extends HistoryChartProps {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (v: string) => void;
|
||||||
|
currentData: ChartDataPoint[];
|
||||||
|
visibleData: ChartDataPoint[];
|
||||||
|
maxAmount: number;
|
||||||
|
visibleCount: number;
|
||||||
|
startIndex: number;
|
||||||
|
setStartIndex: React.Dispatch<React.SetStateAction<number>>;
|
||||||
|
activeDataKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryChartView(props: ViewProps) {
|
||||||
|
const {
|
||||||
|
header,
|
||||||
|
summary,
|
||||||
|
tabs,
|
||||||
|
period,
|
||||||
|
onPeriodChange,
|
||||||
|
comparison,
|
||||||
|
setComparison,
|
||||||
|
colorScheme,
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
currentData,
|
||||||
|
visibleData,
|
||||||
|
maxAmount,
|
||||||
|
visibleCount,
|
||||||
|
startIndex,
|
||||||
|
setStartIndex,
|
||||||
|
activeDataKey,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const handleTabChange = (_: React.MouseEvent<HTMLElement>, newTab: string | null) => {
|
||||||
|
if (newTab !== null) setActiveTab(newTab);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canGoLeft = startIndex > 0;
|
||||||
|
const canGoRight = startIndex + visibleCount < currentData.length;
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (canGoLeft) setStartIndex((prev) => prev - visibleCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (canGoRight) setStartIndex((prev) => prev + visibleCount);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: { xs: 2, sm: 4 },
|
||||||
|
borderRadius: 4,
|
||||||
|
width: "100%",
|
||||||
|
boxShadow: "none",
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: "divider",
|
||||||
|
bgcolor: colorScheme.light,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" fontWeight={700} gutterBottom color={colorScheme.text}>
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mb: 3 }}>
|
||||||
|
<ToggleButtonGroup value={period} exclusive onChange={(_, v) => v && onPeriodChange(v)} size="small">
|
||||||
|
<ToggleButton value="rolling">Rolling</ToggleButton>
|
||||||
|
<ToggleButton value="calendar" disabled={activeDataKey === "daily"}>
|
||||||
|
Calendar
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
value="compare"
|
||||||
|
selected={comparison}
|
||||||
|
onChange={() => setComparison(!comparison)}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
textTransform: "none",
|
||||||
|
borderRadius: 2,
|
||||||
|
px: 2,
|
||||||
|
color: "text.secondary",
|
||||||
|
border: "1px solid",
|
||||||
|
borderColor: "divider",
|
||||||
|
"&.Mui-selected": {
|
||||||
|
color: "white",
|
||||||
|
bgcolor: "success.main",
|
||||||
|
borderColor: "success.main"
|
||||||
|
},
|
||||||
|
"&.Mui-selected:hover": {
|
||||||
|
bgcolor: "success.dark"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</ToggleButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{currentData.length > 0 ? (
|
||||||
|
<Box sx={{ position: "relative", mt: 4 }}>
|
||||||
|
{canGoLeft && (
|
||||||
|
<IconButton
|
||||||
|
onClick={handlePrev}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
zIndex: 2,
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
boxShadow: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", alignItems: "flex-end", height: 220, mt: 4 }}>
|
||||||
|
{visibleData.map((point) => {
|
||||||
|
const currentHeight = (point.amount / maxAmount) * 100;
|
||||||
|
const compareHeight = comparison
|
||||||
|
? ((point.compare?.amount ?? 0) / maxAmount) * 100
|
||||||
|
: 0;
|
||||||
|
const labelHeight = Math.max(currentHeight, compareHeight);
|
||||||
|
|
||||||
|
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" }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: `${labelHeight}%`,
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -6px)",
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
pointerEvents: "none"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatDisplay(point, activeTab.toLowerCase(), comparison)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{comparison && (
|
||||||
|
<Box sx={{ width: 6, height: `${compareHeight}%`, bgcolor: `${colorScheme.primary}55`, borderRadius: 2 }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ width: 4 }} />
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 10,
|
||||||
|
height: `${currentHeight}%`,
|
||||||
|
bgcolor: point.highlighted ? colorScheme.primary : `${colorScheme.primary}99`,
|
||||||
|
borderRadius: 2
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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 }}>
|
||||||
|
{formatLabel(point.id, activeDataKey)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontSize: "0.65rem",
|
||||||
|
color: "grey.400",
|
||||||
|
visibility: comparison && point.compare && activeDataKey !== "daily" ? "visible" : "hidden"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{point.compare ? formatLabel(point.compare.id, activeDataKey) : "placeholder"}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{canGoRight && (
|
||||||
|
<IconButton
|
||||||
|
onClick={handleNext}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
right: 0,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
zIndex: 2,
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
boxShadow: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ height: 200, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Typography color="text.secondary">No Data Available</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
src/components/HistoryChart/index.ts
Normal file
2
src/components/HistoryChart/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./HistoryChart";
|
||||||
|
export * from "./HistoryChart.models";
|
||||||
Reference in New Issue
Block a user