wormhole/bridge_ui/src/components/Stats/VolumeStats.tsx

326 lines
9.5 KiB
TypeScript

import { ChainId } from "@certusone/wormhole-sdk";
import {
Checkbox,
CircularProgress,
FormControl,
ListItemText,
makeStyles,
MenuItem,
Paper,
Select,
TextField,
Tooltip,
Typography,
withStyles,
} from "@material-ui/core";
import { InfoOutlined } from "@material-ui/icons";
import { ToggleButton, ToggleButtonGroup } from "@material-ui/lab";
import { useCallback, useMemo, useState } from "react";
import useNotionalTransferred from "../../hooks/useNotionalTransferred";
import { COLORS } from "../../muiTheme";
import { CHAINS_BY_ID } from "../../utils/consts";
import { TIME_FRAMES } from "./Charts/TimeFrame";
import {
createCumulativeTransferChartData,
createCumulativeTransactionData,
formatTransactionCount,
} from "./Charts/utils";
import VolumeAreaChart from "./Charts/VolumeAreaChart";
import VolumeStackedBarChart from "./Charts/VolumeStackedBarChart";
import VolumeLineChart from "./Charts/VolumeLineChart";
import TransactionsAreaChart from "./Charts/TransactionsAreaChart";
import TransactionsLineChart from "./Charts/TransactionsLineChart";
import useTransactionTotals from "../../hooks/useTransactionTotals";
const DISPLAY_BY_VALUES = ["Dollar", "Percent", "Transactions"];
const useStyles = makeStyles((theme) => ({
description: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
flexDirection: "column",
},
},
displayBy: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: "wrap",
marginBottom: "16px",
[theme.breakpoints.down("xs")]: {
justifyContent: "center",
columnGap: 8,
rowGap: 8,
},
},
mainPaper: {
display: "flex",
flexDirection: "column",
backgroundColor: COLORS.whiteWithTransparency,
padding: "2rem",
marginBottom: theme.spacing(8),
borderRadius: 8,
},
toggleButton: {
textTransform: "none",
},
tooltip: {
margin: 8,
},
alignCenter: {
margin: "0 auto",
display: "block",
},
}));
const tooltipStyles = {
tooltip: {
minWidth: "max-content",
borderRadius: "4px",
backgroundColor: "#5EA1EC",
color: "#0F0C48",
fontSize: "14px",
},
};
const StyledTooltip = withStyles(tooltipStyles)(Tooltip);
const VolumeStats = () => {
const classes = useStyles();
const [displayBy, setDisplayBy] = useState(DISPLAY_BY_VALUES[0]);
const [timeFrame, setTimeFrame] = useState("All time");
const [selectedChains, setSelectedChains] = useState<ChainId[]>([]);
const notionalTransferred = useNotionalTransferred();
const transferData = useMemo(() => {
return notionalTransferred.data
? createCumulativeTransferChartData(
notionalTransferred.data,
TIME_FRAMES[timeFrame]
)
: [];
}, [notionalTransferred, timeFrame]);
const transferredAllTime = useMemo(() => {
return notionalTransferred.data
? new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(notionalTransferred.data.Total || 0)
: "";
}, [notionalTransferred]);
const transactionTotals = useTransactionTotals();
const transactionData = useMemo(() => {
return transactionTotals.data
? createCumulativeTransactionData(
transactionTotals.data,
TIME_FRAMES[timeFrame]
)
: [];
}, [transactionTotals, timeFrame]);
const transactionsAllTime = useMemo(() => {
const totalTransactions =
transactionData[transactionData.length - 1]?.totalTransactions;
return totalTransactions !== undefined
? formatTransactionCount(totalTransactions)
: "";
}, [transactionData]);
const availableChains = useMemo(() => {
const chainIds = notionalTransferred.data
? Object.keys(
Object.values(notionalTransferred.data.Daily)[0] || {}
).reduce<ChainId[]>((chainIds, key) => {
if (key !== "*") {
const chainId = parseInt(key) as ChainId;
if (CHAINS_BY_ID[chainId] !== undefined) {
chainIds.push(chainId);
}
}
return chainIds;
}, [])
: [];
setSelectedChains(chainIds);
return chainIds;
}, [notionalTransferred]);
const handleDisplayByChange = useCallback((event, nextValue) => {
if (nextValue !== null) {
setDisplayBy(nextValue);
}
}, []);
const handleTimeFrameChange = useCallback(
(event) => setTimeFrame(event.target.value),
[]
);
const handleSelectedChainsChange = useCallback(
(event) => {
const value = event.target.value;
if (value[value.length - 1] === "all") {
setSelectedChains((prevValue) =>
prevValue.length === availableChains.length ? [] : availableChains
);
} else {
setSelectedChains(value);
}
},
[availableChains]
);
const allChainsSelected = selectedChains.length === availableChains.length;
return (
<>
<div className={classes.description}>
<Typography variant="h3">
{displayBy === "Transactions"
? "Transaction Count"
: "Outbound Volume"}
<StyledTooltip
title={
displayBy === "Transactions"
? "Total number of transactions the Token Bridge has processed"
: "Amount of assets bridged through Portal in the outbound direction"
}
className={classes.tooltip}
>
<InfoOutlined />
</StyledTooltip>
</Typography>
<Typography variant="h3">
{displayBy === "Transactions"
? transactionsAllTime
: transferredAllTime}
</Typography>
</div>
<div className={classes.displayBy}>
<div>
<Typography display="inline" style={{ marginRight: "8px" }}>
Display by
</Typography>
<ToggleButtonGroup
value={displayBy}
exclusive
onChange={handleDisplayByChange}
>
{DISPLAY_BY_VALUES.map((value) => (
<ToggleButton
key={value}
value={value}
className={classes.toggleButton}
>
{value}
</ToggleButton>
))}
</ToggleButtonGroup>
</div>
<div>
<FormControl>
<Select
multiple
variant="outlined"
value={selectedChains}
onChange={handleSelectedChainsChange}
renderValue={(selected: any) =>
selected.length === availableChains.length
? "All chains"
: selected.length > 1
? `${selected.length} chains`
: //@ts-ignore
CHAINS_BY_ID[selected[0]]?.name
}
MenuProps={{ getContentAnchorEl: null }} // hack to prevent popup menu from moving
style={{ minWidth: 128 }}
>
<MenuItem value="all">
<Checkbox
checked={availableChains.length > 0 && allChainsSelected}
indeterminate={
selectedChains.length > 0 &&
selectedChains.length < availableChains.length
}
/>
<ListItemText primary="All chains" />
</MenuItem>
{availableChains.map((option) => (
<MenuItem key={option} value={option}>
<Checkbox checked={selectedChains.indexOf(option) > -1} />
<ListItemText primary={CHAINS_BY_ID[option]?.name} />
</MenuItem>
))}
</Select>
</FormControl>
<TextField
select
variant="outlined"
value={timeFrame}
onChange={handleTimeFrameChange}
style={{ marginLeft: 8 }}
>
{Object.keys(TIME_FRAMES).map((timeFrame) => (
<MenuItem key={timeFrame} value={timeFrame}>
{timeFrame}
</MenuItem>
))}
</TextField>
</div>
</div>
<Paper className={classes.mainPaper}>
{displayBy === "Dollar" ? (
notionalTransferred.data ? (
allChainsSelected ? (
<VolumeAreaChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<VolumeLineChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)
) : displayBy === "Percent" ? (
<VolumeStackedBarChart
transferData={transferData}
timeFrame={TIME_FRAMES[timeFrame]}
selectedChains={selectedChains}
/>
) : transactionTotals.data ? (
allChainsSelected ? (
<TransactionsAreaChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
/>
) : (
<TransactionsLineChart
transactionData={transactionData}
timeFrame={TIME_FRAMES[timeFrame]}
chains={selectedChains}
/>
)
) : (
<CircularProgress className={classes.alignCenter} />
)}
</Paper>
</>
);
};
export default VolumeStats;