245 lines
8.1 KiB
TypeScript
245 lines
8.1 KiB
TypeScript
import * as React from "react";
|
|
import {
|
|
Box,
|
|
Typography,
|
|
ToggleButtonGroup,
|
|
ToggleButton,
|
|
Paper
|
|
} from "@mui/material";
|
|
import { useTheme, alpha } from "@mui/material/styles";
|
|
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 theme = useTheme();
|
|
const isDark = theme.palette.mode === "dark";
|
|
|
|
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.5, sm: 4 },
|
|
borderRadius: 4,
|
|
width: "100%",
|
|
boxShadow: "none",
|
|
border: "1px solid",
|
|
borderColor: "divider",
|
|
bgcolor: isDark ? "background.paper" : colorScheme.light,
|
|
transition: 'background-color 0.3s ease, border-color 0.3s ease'
|
|
}}
|
|
>
|
|
<Typography variant="h6" fontWeight={700} gutterBottom sx={{ color: isDark ? 'text.primary' : 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 ? 1 : 0.5, 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",
|
|
color: 'text.secondary',
|
|
fontWeight: 600
|
|
}}
|
|
>
|
|
{formatDisplay(point, activeTab.toLowerCase(), comparison)}
|
|
</Typography>
|
|
|
|
{comparison && (
|
|
<Box sx={{ width: 8, height: `${compareHeight}%`, bgcolor: isDark ? alpha(colorScheme.primary, 0.3) : alpha(colorScheme.primary, 0.4), borderRadius: '4px 4px 0 0' }} />
|
|
)}
|
|
|
|
<Box
|
|
sx={{
|
|
width: comparison ? 10 : 16,
|
|
height: `${currentHeight}%`,
|
|
bgcolor: point.highlighted ? colorScheme.primary : isDark ? alpha(colorScheme.primary, 0.8) : alpha(colorScheme.primary, 0.9),
|
|
borderRadius: '4px 4px 0 0',
|
|
boxShadow: point.highlighted ? `0 0 10px ${alpha(colorScheme.primary, 0.5)}` : 'none'
|
|
}}
|
|
/>
|
|
</Box>
|
|
|
|
<Box sx={{ mt: 1.5, textAlign: "center", display: "flex", flexDirection: "column", alignItems: "center", lineHeight: 1.1 }}>
|
|
<Typography variant="caption" sx={{ fontSize: "0.7rem", opacity: 0.8, color: 'text.primary', fontWeight: 500 }}>
|
|
{formatLabel(point.id, activeDataKey)}
|
|
</Typography>
|
|
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
fontSize: "0.65rem",
|
|
color: "text.disabled",
|
|
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>
|
|
);
|
|
}
|