mango-ui-v3/components/account_page/PerformanceChart.tsx

383 lines
12 KiB
TypeScript

import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import cloneDeep from 'lodash/cloneDeep'
import dayjs from 'dayjs'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as ChartTooltip,
} from 'recharts'
import { InformationCircleIcon } from '@heroicons/react/outline'
import useDimensions from 'react-cool-dimensions'
import { useTranslation } from 'next-i18next'
import ButtonGroup from '../ButtonGroup'
import { formatUsdValue } from '../../utils'
import { numberCompacter } from '../SwapTokenInfo'
import Checkbox from '../Checkbox'
import Tooltip from '../Tooltip'
type PerformanceChart = {
hourlyPerformanceStats: any[]
performanceRange: '24hr' | '7d' | '30d' | '3m'
}
const defaultData = [
{ account_equity: 0, pnl: 0, time: '2022-01-01T00:00:00.000Z' },
{ account_equity: 0, pnl: 0, time: '2023-01-01T00:00:00.000Z' },
]
const PerformanceChart = ({
hourlyPerformanceStats,
performanceRange,
accountValue,
}) => {
const { theme } = useTheme()
const { t } = useTranslation('common')
const { observe, width, height } = useDimensions()
const [chartData, setChartData] = useState<any[]>([])
const [mouseData, setMouseData] = useState<string | null>(null)
const [chartToShow, setChartToShow] = useState('Value')
const [showSpotPnl, setShowSpotPnl] = useState(true)
const [showPerpPnl, setShowPerpPnl] = useState(true)
useEffect(() => {
if (hourlyPerformanceStats.length > 0) {
if (performanceRange === '3m') {
setChartData(hourlyPerformanceStats.slice().reverse())
}
if (performanceRange === '30d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(29, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '7d') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(7, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
if (performanceRange === '24h') {
const start = new Date(
// @ts-ignore
dayjs().utc().hour(0).minute(0).subtract(1, 'day')
).getTime()
const chartData = cloneDeep(hourlyPerformanceStats).filter(
(d) => new Date(d.time).getTime() > start
)
const pnlStart = chartData[chartData.length - 1].pnl
const perpPnlStart = chartData[chartData.length - 1].perp_pnl
for (let i = 0; i < chartData.length; i++) {
if (i === chartData.length - 1) {
chartData[i].pnl = 0
chartData[i].perp_pnl = 0
} else {
chartData[i].pnl = chartData[i].pnl - pnlStart
chartData[i].perp_pnl = chartData[i].perp_pnl - perpPnlStart
}
}
setChartData(chartData.reverse())
}
} else {
setChartData([])
}
}, [hourlyPerformanceStats, performanceRange])
useEffect(() => {
if (chartData.length > 0) {
for (const stat of chartData) {
stat.spot_pnl = stat.pnl - stat.perp_pnl
}
}
}, [chartData])
const handleMouseMove = (coords) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const renderPnlChartTitle = () => {
if (showPerpPnl && showSpotPnl) {
return t('pnl')
}
if (!showSpotPnl) {
return `${t('perp')} ${t('pnl')}`
}
if (!showPerpPnl) {
return `${t('spot')} ${t('pnl')}`
}
}
const formatDateAxis = (date) => {
if (['3m', '30d'].includes(performanceRange)) {
return dayjs(date).format('D MMM')
} else if (performanceRange === '7d') {
return dayjs(date).format('ddd, h:mma')
} else {
return dayjs(date).format('h:mma')
}
}
const pnlChartDataKey = () => {
if (!showPerpPnl && showSpotPnl) {
return 'spot_pnl'
} else if (!showSpotPnl && showPerpPnl) {
return 'perp_pnl'
} else {
return 'pnl'
}
}
const pnlChartColor =
chartToShow === 'PnL' &&
chartData.length > 0 &&
chartData[chartData.length - 1][pnlChartDataKey()] > 0
? theme === 'Mango'
? '#AFD803'
: '#5EBF4D'
: theme === 'Mango'
? '#F84638'
: '#CC2929'
return (
<div className="mt-4 h-64 w-full" ref={observe}>
<div className="flex justify-between pb-9">
<div>
<div className="flex items-center pb-0.5">
<div className="text-sm text-th-fgd-3">
{chartToShow === 'Value'
? t('account-value')
: renderPnlChartTitle()}{' '}
<span className="text-th-fgd-4">
{`(${t('timeframe-desc', {
timeframe: performanceRange,
})})`}
</span>
</div>
<Tooltip content={t('delayed-info')}>
<InformationCircleIcon className="ml-1.5 h-5 w-5 cursor-help text-th-fgd-3" />
</Tooltip>
</div>
{mouseData ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{formatUsdValue(
mouseData[
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
]
)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs(mouseData['time']).format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length === 0 ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">--</div>
<div className="text-xs font-normal text-th-fgd-4">
{dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : chartData.length > 0 ? (
<>
<div className="pb-1 text-xl font-bold text-th-fgd-1">
{chartToShow === 'PnL'
? formatUsdValue(
chartData[chartData.length - 1][pnlChartDataKey()]
)
: formatUsdValue(accountValue)}
</div>
<div className="text-xs font-normal text-th-fgd-4">
{chartToShow === 'PnL'
? dayjs(chartData[chartData.length - 1]['time']).format(
'ddd MMM D YYYY, h:mma'
)
: dayjs().format('ddd MMM D YYYY, h:mma')}
</div>
</>
) : (
<>
<div className="mt-1 h-8 w-48 animate-pulse rounded bg-th-bkg-3" />
<div className="mt-1 h-4 w-24 animate-pulse rounded bg-th-bkg-3" />
</>
)}
</div>
<div className="flex flex-col items-end">
<div className="w-36">
<ButtonGroup
activeValue={chartToShow}
className="pb-2 pt-2 text-sm"
onChange={(v) => setChartToShow(v)}
values={['Value', 'PnL']}
names={[t('value'), t('pnl')]}
/>
</div>
{chartToShow === 'PnL' ? (
<div className="flex space-x-3 pt-4">
<Checkbox
checked={showSpotPnl}
disabled={!showPerpPnl}
onChange={(e) => setShowSpotPnl(e.target.checked)}
>
{t('include-spot')}
</Checkbox>
<Checkbox
checked={showPerpPnl}
disabled={!showSpotPnl}
onChange={(e) => setShowPerpPnl(e.target.checked)}
>
{t('include-perp')}
</Checkbox>
</div>
) : null}
</div>
</div>
{chartData.length > 0 ? (
<AreaChart
width={width}
height={height}
data={chartData?.length ? chartData : defaultData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<ChartTooltip
cursor={{
strokeOpacity: 0,
}}
content={<></>}
/>
<defs>
<linearGradient
id="defaultGradientArea"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor="#ffba24" stopOpacity={0.9} />
<stop offset="80%" stopColor="#ffba24" stopOpacity={0} />
</linearGradient>
<linearGradient id="greenGradientArea" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#AFD803' : '#5EBF4D'}
stopOpacity={0}
/>
</linearGradient>
<linearGradient id="redGradientArea" x1="0" y1="1" x2="0" y2="0">
<stop
offset="0%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0.9}
/>
<stop
offset="80%"
stopColor={theme === 'Mango' ? '#F84638' : '#CC2929'}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={true}
type="monotone"
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
stroke={chartToShow === 'PnL' ? pnlChartColor : '#ffba24'}
fill={
chartToShow === 'PnL'
? chartData[chartData.length - 1][pnlChartDataKey()] > 0
? 'url(#greenGradientArea)'
: 'url(#redGradientArea)'
: 'url(#defaultGradientArea)'
}
fillOpacity={0.3}
/>
<YAxis
dataKey={
chartToShow === 'PnL' ? pnlChartDataKey() : 'account_equity'
}
type="number"
domain={['dataMin', 'dataMax']}
axisLine={false}
dx={-10}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => numberCompacter.format(v)}
/>
<XAxis
dataKey="time"
axisLine={false}
dy={10}
minTickGap={20}
tick={{
fill:
theme === 'Light'
? 'rgba(0,0,0,0.4)'
: 'rgba(255,255,255,0.35)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) => formatDateAxis(v)}
/>
</AreaChart>
) : null}
</div>
)
}
export default PerformanceChart