diff --git a/web/components/portfolio/portfolio-value-graph.tsx b/web/components/portfolio/portfolio-value-graph.tsx index e329457d..62692ae5 100644 --- a/web/components/portfolio/portfolio-value-graph.tsx +++ b/web/components/portfolio/portfolio-value-graph.tsx @@ -1,155 +1,84 @@ -import { ResponsiveLine } from '@nivo/line' -import { PortfolioMetrics } from 'common/user' -import { filterDefined } from 'common/util/array' -import { formatMoney } from 'common/util/format' +import { useMemo } from 'react' +import { scaleTime, scaleLinear } from 'd3-scale' +import { curveStepAfter } from 'd3-shape' +import { min, max } from 'lodash' import dayjs from 'dayjs' -import { last } from 'lodash' -import { memo } from 'react' -import { useWindowSize } from 'web/hooks/use-window-size' +import { PortfolioMetrics } from 'common/user' import { Col } from '../layout/col' +import { TooltipProps } from 'web/components/charts/helpers' +import { + HistoryPoint, + SingleValueHistoryChart, +} from 'web/components/charts/generic-charts' -export const PortfolioValueGraph = memo(function PortfolioValueGraph(props: { - portfolioHistory: PortfolioMetrics[] - mode: 'value' | 'profit' - handleGraphDisplayChange: (arg0: string | number | null) => void - height?: number -}) { - const { portfolioHistory, height, mode, handleGraphDisplayChange } = props - const { width } = useWindowSize() +const MARGIN = { top: 20, right: 10, bottom: 20, left: 70 } +const MARGIN_X = MARGIN.left + MARGIN.right +const MARGIN_Y = MARGIN.top + MARGIN.bottom - const valuePoints = getPoints('value', portfolioHistory) - const posProfitPoints = getPoints('posProfit', portfolioHistory) - const negProfitPoints = getPoints('negProfit', portfolioHistory) +export type GraphMode = 'profit' | 'value' - const valuePointsY = valuePoints.map((p) => p.y) - const posProfitPointsY = posProfitPoints.map((p) => p.y) - const negProfitPointsY = negProfitPoints.map((p) => p.y) +export const PortfolioTooltip = (props: TooltipProps) => { + const { mouseX, xScale } = props + const d = dayjs(xScale.invert(mouseX)) + return ( + +
{d.format('MMM/D/YY')}
+
+ {d.format('h:mm A')} +
+ + ) +} - let data +const getY = (mode: GraphMode, p: PortfolioMetrics) => + p.balance + p.investmentValue - (mode === 'profit' ? p.totalDeposits : 0) - 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 +export function getPoints(mode: GraphMode, history: PortfolioMetrics[]) { + return history.map((p) => ({ + x: new Date(p.timestamp), + y: getY(mode, p), + obj: p, + })) +} - 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) - ) +export const PortfolioGraph = (props: { + mode: 'profit' | 'value' + history: PortfolioMetrics[] + width: number + height: number + onMouseOver?: (p: HistoryPoint | undefined) => void +}) => { + const { mode, history, onMouseOver, width, height } = props + const { data, minDate, maxDate, minValue, maxValue } = useMemo(() => { + const data = getPoints(mode, history) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const minDate = min(data.map((d) => d.x))! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const maxDate = max(data.map((d) => d.x))! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const minValue = min(data.map((d) => d.y))! + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const maxValue = max(data.map((d) => d.y))! + return { data, minDate, maxDate, minValue, maxValue } + }, [mode, history]) return ( -
= 800 ? 200 : 100) }} - onMouseLeave={() => handleGraphDisplayChange(null)} - > - 100 ? 0 : 6} - axisLeft={{ - tickValues: numYTickValues, - format: '.3s', - }} - enableGridX={false} - enableGridY={true} - gridYValues={numYTickValues} - enableSlices="x" - animate={false} - yFormat={(value) => formatMoney(+value)} - enableArea={true} - areaOpacity={0.1} - sliceTooltip={({ slice }) => { - handleGraphDisplayChange(slice.points[0].data.yFormatted) - return ( -
-
- -
- {dayjs(slice.points[0].data.xFormatted).format('MMM/D/YY')} -
-
- {dayjs(slice.points[0].data.xFormatted).format('h:mm A')} -
- -
- {/* ))} */} -
- ) - }} - >
-
+ (p.y >= 0 ? '#14b8a6' : '#f00') + } + /> ) -}) - -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 } diff --git a/web/components/portfolio/portfolio-value-section.tsx b/web/components/portfolio/portfolio-value-section.tsx index d7fff6ef..cc9fb197 100644 --- a/web/components/portfolio/portfolio-value-section.tsx +++ b/web/components/portfolio/portfolio-value-section.tsx @@ -1,34 +1,29 @@ import clsx from 'clsx' import { formatMoney } from 'common/util/format' import { last } from 'lodash' -import { memo, useRef, useState } from 'react' +import { memo, useState } from 'react' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' -import { Period } from 'web/lib/firebase/users' import { Col } from '../layout/col' import { Row } from '../layout/row' -import { PortfolioValueGraph } from './portfolio-value-graph' +import { GraphMode, PortfolioGraph } from './portfolio-value-graph' +import { SizedContainer } from 'web/components/sized-container' export const PortfolioValueSection = memo( function PortfolioValueSection(props: { userId: string }) { const { userId } = props - const [portfolioPeriod, setPortfolioPeriod] = useState('weekly') - const portfolioHistory = usePortfolioHistory(userId, portfolioPeriod) - const [graphMode, setGraphMode] = useState<'profit' | 'value'>('profit') + const portfolioHistory = usePortfolioHistory(userId, 'allTime') + const [graphMode, setGraphMode] = useState('profit') const [graphDisplayNumber, setGraphDisplayNumber] = useState< number | string | null >(null) - const handleGraphDisplayChange = (num: string | number | null) => { - setGraphDisplayNumber(num) + const handleGraphDisplayChange = (p: { y: number } | undefined) => { + console.log(p) + setGraphDisplayNumber(p != null ? formatMoney(p.y) : null) } - // Remember the last defined portfolio history. - const portfolioRef = useRef(portfolioHistory) - if (portfolioHistory) portfolioRef.current = portfolioHistory - const currPortfolioHistory = portfolioRef.current - - const lastPortfolioMetrics = last(currPortfolioHistory) - if (!currPortfolioHistory || !lastPortfolioMetrics) { + const lastPortfolioMetrics = last(portfolioHistory) + if (!portfolioHistory || !lastPortfolioMetrics) { return <> } @@ -46,7 +41,10 @@ export const PortfolioValueSection = memo( ? 'cursor-pointer opacity-40 hover:opacity-80' : '' )} - onClick={() => setGraphMode('profit')} + onClick={() => { + setGraphMode('profit') + setGraphDisplayNumber(null) + }} >
Profit
setGraphMode('value')} + onClick={() => { + setGraphMode('value') + setGraphDisplayNumber(null) + }} >
Portfolio value @@ -93,56 +94,18 @@ export const PortfolioValueSection = memo( - - + + {(width, height) => ( + + )} + ) } ) - -export function PortfolioPeriodSelection(props: { - setPortfolioPeriod: (string: any) => void - portfolioPeriod: string - className?: string - selectClassName?: string -}) { - const { setPortfolioPeriod, portfolioPeriod, className, selectClassName } = - props - return ( - - - - - - - ) -}