2022-06-24 17:14:20 +00:00
|
|
|
import { ResponsiveLine } from '@nivo/line'
|
|
|
|
import { PortfolioMetrics } from 'common/user'
|
2022-09-26 23:01:13 +00:00
|
|
|
import { filterDefined } from 'common/util/array'
|
2022-06-24 17:14:20 +00:00
|
|
|
import { formatMoney } from 'common/util/format'
|
2022-09-26 23:01:13 +00:00
|
|
|
import dayjs from 'dayjs'
|
2022-06-24 17:14:20 +00:00
|
|
|
import { last } from 'lodash'
|
|
|
|
import { memo } from 'react'
|
|
|
|
import { useWindowSize } from 'web/hooks/use-window-size'
|
2022-09-26 23:01:13 +00:00
|
|
|
import { Col } from '../layout/col'
|
2022-06-24 17:14:20 +00:00
|
|
|
|
|
|
|
export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: {
|
|
|
|
portfolioHistory: PortfolioMetrics[]
|
2022-09-07 04:24:56 +00:00
|
|
|
mode: 'value' | 'profit'
|
2022-09-26 23:01:13 +00:00
|
|
|
handleGraphDisplayChange: (arg0: string | number | null) => void
|
2022-06-24 17:14:20 +00:00
|
|
|
height?: number
|
|
|
|
}) {
|
2022-09-26 23:01:13 +00:00
|
|
|
const { portfolioHistory, height, mode, handleGraphDisplayChange } = props
|
2022-06-24 17:14:20 +00:00
|
|
|
const { width } = useWindowSize()
|
|
|
|
|
2022-09-26 23:01:13 +00:00
|
|
|
const valuePoints = getPoints('value', portfolioHistory)
|
|
|
|
const posProfitPoints = getPoints('posProfit', portfolioHistory)
|
|
|
|
const negProfitPoints = getPoints('negProfit', portfolioHistory)
|
|
|
|
|
|
|
|
const valuePointsY = valuePoints.map((p) => p.y)
|
|
|
|
const posProfitPointsY = posProfitPoints.map((p) => p.y)
|
|
|
|
const negProfitPointsY = negProfitPoints.map((p) => p.y)
|
|
|
|
|
|
|
|
let data
|
|
|
|
|
|
|
|
if (mode === 'value') {
|
|
|
|
data = [{ id: 'value', data: valuePoints, color: '#4f46e5' }]
|
|
|
|
} else {
|
|
|
|
data = [
|
|
|
|
{
|
|
|
|
id: 'negProfit',
|
|
|
|
data: negProfitPoints,
|
|
|
|
color: '#dc2626',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'posProfit',
|
|
|
|
data: posProfitPoints,
|
|
|
|
color: '#14b8a6',
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|
|
|
|
const numYTickValues = 2
|
|
|
|
const endDate = last(data[0].data)?.x
|
|
|
|
|
|
|
|
const yMin =
|
|
|
|
mode === 'value'
|
|
|
|
? Math.min(...filterDefined(valuePointsY))
|
|
|
|
: Math.min(
|
|
|
|
...filterDefined(negProfitPointsY),
|
|
|
|
...filterDefined(posProfitPointsY)
|
|
|
|
)
|
|
|
|
|
|
|
|
const yMax =
|
|
|
|
mode === 'value'
|
|
|
|
? Math.max(...filterDefined(valuePointsY))
|
|
|
|
: Math.max(
|
|
|
|
...filterDefined(negProfitPointsY),
|
|
|
|
...filterDefined(posProfitPointsY)
|
|
|
|
)
|
2022-09-07 04:24:56 +00:00
|
|
|
|
2022-06-24 17:14:20 +00:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className="w-full overflow-hidden"
|
2022-09-26 23:01:13 +00:00
|
|
|
style={{ height: height ?? (!width || width >= 800 ? 200 : 100) }}
|
|
|
|
onMouseLeave={() => handleGraphDisplayChange(null)}
|
2022-06-24 17:14:20 +00:00
|
|
|
>
|
|
|
|
<ResponsiveLine
|
2022-09-26 23:01:13 +00:00
|
|
|
margin={{ top: 10, right: 0, left: 40, bottom: 10 }}
|
2022-06-24 17:14:20 +00:00
|
|
|
data={data}
|
|
|
|
xScale={{
|
|
|
|
type: 'time',
|
2022-09-26 23:01:13 +00:00
|
|
|
min: valuePoints[0]?.x,
|
2022-06-24 17:14:20 +00:00
|
|
|
max: endDate,
|
|
|
|
}}
|
|
|
|
yScale={{
|
|
|
|
type: 'linear',
|
|
|
|
stacked: false,
|
2022-09-26 23:01:13 +00:00
|
|
|
min: yMin,
|
|
|
|
max: yMax,
|
2022-06-24 17:14:20 +00:00
|
|
|
}}
|
2022-08-09 17:08:14 +00:00
|
|
|
curve="stepAfter"
|
|
|
|
enablePoints={false}
|
2022-06-24 17:14:20 +00:00
|
|
|
colors={{ datum: 'color' }}
|
|
|
|
axisBottom={{
|
2022-09-26 23:01:13 +00:00
|
|
|
tickValues: 0,
|
2022-06-24 17:14:20 +00:00
|
|
|
}}
|
|
|
|
pointBorderColor="#fff"
|
2022-09-26 23:01:13 +00:00
|
|
|
pointSize={valuePoints.length > 100 ? 0 : 6}
|
2022-06-24 17:14:20 +00:00
|
|
|
axisLeft={{
|
|
|
|
tickValues: numYTickValues,
|
2022-09-26 23:01:13 +00:00
|
|
|
format: '.3s',
|
2022-06-24 17:14:20 +00:00
|
|
|
}}
|
2022-09-26 23:01:13 +00:00
|
|
|
enableGridX={false}
|
2022-06-24 17:14:20 +00:00
|
|
|
enableGridY={true}
|
2022-09-26 23:01:13 +00:00
|
|
|
gridYValues={numYTickValues}
|
2022-06-24 17:14:20 +00:00
|
|
|
enableSlices="x"
|
|
|
|
animate={false}
|
2022-07-03 19:18:12 +00:00
|
|
|
yFormat={(value) => formatMoney(+value)}
|
2022-09-26 23:01:13 +00:00
|
|
|
enableArea={true}
|
|
|
|
areaOpacity={0.1}
|
|
|
|
sliceTooltip={({ slice }) => {
|
|
|
|
handleGraphDisplayChange(slice.points[0].data.yFormatted)
|
|
|
|
return (
|
2022-09-30 07:03:31 +00:00
|
|
|
<div className="rounded border border-gray-200 bg-white px-4 py-2 opacity-80">
|
2022-09-26 23:01:13 +00:00
|
|
|
<div
|
|
|
|
key={slice.points[0].id}
|
|
|
|
className="text-xs font-semibold sm:text-sm"
|
|
|
|
>
|
|
|
|
<Col>
|
|
|
|
<div>
|
|
|
|
{dayjs(slice.points[0].data.xFormatted).format('MMM/D/YY')}
|
|
|
|
</div>
|
|
|
|
<div className="text-greyscale-6 text-2xs font-normal sm:text-xs">
|
|
|
|
{dayjs(slice.points[0].data.xFormatted).format('h:mm A')}
|
|
|
|
</div>
|
|
|
|
</Col>
|
|
|
|
</div>
|
|
|
|
{/* ))} */}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}}
|
2022-06-24 17:14:20 +00:00
|
|
|
></ResponsiveLine>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
})
|
2022-09-26 23:01:13 +00:00
|
|
|
|
|
|
|
export function getPoints(
|
|
|
|
line: 'value' | 'posProfit' | 'negProfit',
|
|
|
|
portfolioHistory: PortfolioMetrics[]
|
|
|
|
) {
|
|
|
|
const points = portfolioHistory.map((p) => {
|
|
|
|
const { timestamp, balance, investmentValue, totalDeposits } = p
|
|
|
|
const value = balance + investmentValue
|
|
|
|
|
|
|
|
const profit = value - totalDeposits
|
|
|
|
let posProfit = null
|
|
|
|
let negProfit = null
|
|
|
|
if (profit < 0) {
|
|
|
|
negProfit = profit
|
|
|
|
} else {
|
|
|
|
posProfit = profit
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
x: new Date(timestamp),
|
|
|
|
y:
|
|
|
|
line === 'value' ? value : line === 'posProfit' ? posProfit : negProfit,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return points
|
|
|
|
}
|