mango-v4-ui/components/trade/DepthChart.tsx

521 lines
16 KiB
TypeScript
Raw Normal View History

2023-07-12 07:37:18 -07:00
import Slider from '@components/forms/Slider'
import useMarkPrice from 'hooks/useMarkPrice'
2023-07-02 22:55:25 -07:00
import useSelectedMarket from 'hooks/useSelectedMarket'
2023-07-13 03:37:41 -07:00
import { useViewport } from 'hooks/useViewport'
2023-07-12 07:37:18 -07:00
import { useCallback, useMemo, useState } from 'react'
import {
XAxis,
YAxis,
ResponsiveContainer,
AreaChart,
Area,
ReferenceLine,
Label,
2023-07-13 15:53:50 -07:00
LabelProps,
2023-07-12 07:37:18 -07:00
} from 'recharts'
import { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart'
2023-07-02 22:55:25 -07:00
import { COLORS } from 'styles/colors'
2023-07-12 20:59:03 -07:00
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
2023-07-13 15:53:50 -07:00
import { CartesianViewBox } from 'recharts/types/util/types'
import { cumOrderbookSide } from 'types'
import mangoStore from '@store/mangoStore'
2023-07-20 03:40:27 -07:00
import { breakpoints } from 'utils/theme'
2023-07-21 11:50:06 -07:00
import useThemeWrapper from 'hooks/useThemeWrapper'
2023-07-12 20:59:03 -07:00
type LabelPosition =
| 'left'
| 'right'
| 'center'
| 'bottom'
| 'insideLeft'
| 'insideRight'
| 'insideTop'
| 'insideBottom'
| 'insideTopLeft'
| 'insideTopRight'
| 'insideBottomLeft'
| 'insideBottomRight'
| 'top'
2023-06-29 20:05:41 -07:00
2023-07-13 15:53:50 -07:00
const Y_TICK_COUNT = 10
interface CustomLabel extends LabelProps {
viewBox?: CartesianViewBox
}
const MarkPriceLabel = ({ value, viewBox }: CustomLabel) => {
if (typeof value === 'string' && viewBox?.x && viewBox?.y) {
const { x, y } = viewBox
const valueLength = value.length
const valueWidth = valueLength * 6
return (
<g>
<foreignObject x={x - valueWidth} y={y - 10} width="100%" height={20}>
<div className="w-max rounded bg-th-bkg-3 p-1 font-mono text-[9px] leading-none">
{value}
</div>
</foreignObject>
</g>
)
} else return null
}
type RawOrderbook = number[][]
type DepthOrderbookSide = {
price: number
size: number
cumulativeSize: number
}
const DepthChart = () => {
2023-07-21 11:50:06 -07:00
const { theme } = useThemeWrapper()
2023-07-02 22:55:25 -07:00
const { serumOrPerpMarket } = useSelectedMarket()
2023-07-12 07:37:18 -07:00
const [mouseData, setMouseData] = useState<cumOrderbookSide | null>(null)
const markPrice = useMarkPrice()
const orderbook = mangoStore((s) => s.selectedMarket.orderbook)
const [priceRangePercent, setPriceRangePercentPercent] = useState('10')
2023-08-31 20:22:50 -07:00
const { isTablet, width } = useViewport()
2023-07-20 03:40:27 -07:00
const increaseHeight = width ? width > breakpoints['3xl'] : false
2023-06-29 20:05:41 -07:00
const formatOrderbookData = (orderbook: RawOrderbook, markPrice: number) => {
const maxPrice = markPrice * 4
const minPrice = markPrice / 4
const formattedBook = []
let cumulativeSize = 0
for (let i = 0; i < orderbook.length; i++) {
const [price, size] = orderbook[i]
cumulativeSize += size
const object = {
price: price,
size: size,
cumulativeSize: cumulativeSize,
}
if (price >= minPrice && price <= maxPrice) {
formattedBook.push(object)
}
}
return formattedBook
}
2023-07-13 15:53:50 -07:00
// format chart data for the bids and asks series
2023-07-02 22:55:25 -07:00
const mergeCumulativeData = (
bids: DepthOrderbookSide[],
2023-07-21 11:47:53 -07:00
asks: DepthOrderbookSide[],
2023-07-02 22:55:25 -07:00
) => {
const bidsWithSide = bids.map((b) => ({ ...b, bids: b.cumulativeSize }))
const asksWithSide = asks.map((a) => ({ ...a, asks: a.cumulativeSize }))
return [...bidsWithSide, ...asksWithSide].sort((a, b) => a.price - b.price)
2023-06-29 20:05:41 -07:00
}
2023-07-02 22:55:25 -07:00
const chartData = useMemo(() => {
if (!orderbook || !serumOrPerpMarket || !markPrice) return []
const formattedBids = formatOrderbookData(orderbook.bids, markPrice)
const formattedAsks = formatOrderbookData(orderbook.asks, markPrice)
return mergeCumulativeData(formattedBids, formattedAsks)
}, [markPrice, orderbook, serumOrPerpMarket])
2023-07-02 22:55:25 -07:00
2023-07-13 15:53:50 -07:00
// find the max value for the x-axis
2023-07-12 07:37:18 -07:00
const findXDomainMax = (
data: DepthOrderbookSide[],
2023-07-12 07:37:18 -07:00
yMin: number,
2023-07-21 11:47:53 -07:00
yMax: number,
2023-07-12 07:37:18 -07:00
) => {
let closestItemForYMin = 0
let minDifferenceForYMin = Infinity
let closestItemForYMax = 0
let minDifferenceForYMax = Infinity
for (const item of data) {
const differenceForYMin = Math.abs(item.price - yMin)
const differenceForYMax = Math.abs(item.price - yMax)
if (differenceForYMin < minDifferenceForYMin) {
minDifferenceForYMin = differenceForYMin
closestItemForYMin = item.cumulativeSize
}
if (differenceForYMax < minDifferenceForYMax) {
minDifferenceForYMax = differenceForYMax
closestItemForYMax = item.cumulativeSize
}
}
return Math.max(closestItemForYMin, closestItemForYMax)
}
2023-07-13 15:53:50 -07:00
// calc axis domains
2023-07-12 07:37:18 -07:00
const [xMax, yMin, yMax] = useMemo(() => {
let xMax = 100
let yMin = 0
let yMax = 100
if (markPrice) {
yMin = markPrice / (1 + parseFloat(priceRangePercent) / 100)
yMax = markPrice * (1 + parseFloat(priceRangePercent) / 100)
}
if (chartData.length) {
xMax = findXDomainMax(chartData, yMin, yMax)
}
return [xMax, yMin, yMax]
}, [chartData, markPrice, priceRangePercent])
2023-07-02 22:55:25 -07:00
2023-07-13 15:53:50 -07:00
// get nearest data on the opposing side to the mouse
const opposingMouseReference = useMemo(() => {
if (!markPrice || !mouseData) return null
const mousePrice = mouseData.price
const difference = Math.abs(mousePrice - markPrice) / markPrice
if (mousePrice >= markPrice) {
const price = markPrice / (1 + difference)
let closestItemBelow = null
let minDifference = Infinity
for (const item of chartData) {
const difference = Math.abs(item.price - price)
if (difference < minDifference) {
minDifference = difference
closestItemBelow = item
}
}
return closestItemBelow
} else {
const price = markPrice * (1 + difference)
let closestItemAbove = null
let minDifference = Infinity
for (const item of chartData) {
const difference = Math.abs(item.price - price)
if (difference < minDifference) {
minDifference = difference
closestItemAbove = item
}
}
return closestItemAbove
}
}, [markPrice, mouseData])
const priceFormatter = useCallback(
(price: number) => {
if (!serumOrPerpMarket) return price.toFixed()
2023-07-12 07:37:18 -07:00
const tickDecimals = getDecimalCount(serumOrPerpMarket.tickSize)
if (tickDecimals >= 7) {
2023-07-13 15:53:50 -07:00
return price.toExponential(3)
} else return price.toFixed(tickDecimals)
2023-07-12 07:37:18 -07:00
},
2023-07-21 11:47:53 -07:00
[serumOrPerpMarket],
2023-07-12 07:37:18 -07:00
)
2023-07-02 22:55:25 -07:00
const xTickFormatter = useCallback(
(size: number) => {
if (!serumOrPerpMarket) return size.toFixed()
const minOrderDecimals = getDecimalCount(serumOrPerpMarket.minOrderSize)
return size.toFixed(minOrderDecimals)
},
2023-07-21 11:47:53 -07:00
[serumOrPerpMarket],
)
2023-07-13 15:53:50 -07:00
const isWithinRangeOfTick = useCallback(
(value: number, baseValue: number) => {
const difference = Math.abs(value - baseValue)
const range = (yMax - yMin) / Y_TICK_COUNT
return difference <= range
},
2023-07-21 11:47:53 -07:00
[yMin, yMax],
2023-07-13 15:53:50 -07:00
)
const yTickFormatter = useCallback(
(tick: number) => {
if ((markPrice && isWithinRangeOfTick(markPrice, tick)) || mouseData) {
return ''
}
return priceFormatter(tick)
},
2023-07-21 11:47:53 -07:00
[markPrice, mouseData],
2023-07-13 15:53:50 -07:00
)
2023-07-12 20:59:03 -07:00
const getChartReferenceColor = (price: number | undefined) => {
if (!price || !markPrice) return 'var(--fgd-2)'
return price > markPrice ? 'var(--down)' : 'var(--up)'
}
const getPercentFromMarkPrice = (price: number | undefined) => {
if (!price || !markPrice) return
const percentDif = ((price - markPrice) / markPrice) * 100
return `${percentDif.toFixed(2)}%`
}
const getSizeFromMouseData = useCallback(
(size: number | undefined) => {
if (!size || !serumOrPerpMarket) return
return floorToDecimal(
size,
2023-07-21 11:47:53 -07:00
getDecimalCount(serumOrPerpMarket.tickSize),
2023-07-12 20:59:03 -07:00
).toString()
},
2023-07-21 11:47:53 -07:00
[serumOrPerpMarket],
2023-07-12 20:59:03 -07:00
)
const getSizeLabelPosition = useCallback(
(size: number | undefined, price: number | undefined) => {
if (!xMax || !size || !price || !markPrice) return `insideRight`
const yPosition = price > markPrice ? 'Top' : 'Bottom'
const midPoint = xMax / 2
const xPosition = size > midPoint ? 'Left' : 'Right'
return `inside${yPosition}${xPosition}` as LabelPosition
},
2023-07-21 11:47:53 -07:00
[xMax, markPrice],
2023-07-12 20:59:03 -07:00
)
const getPercentLabelPosition = useCallback(
(price: number | undefined) => {
if (!markPrice || !price || !yMax || !yMin) return 'bottom'
const upperMidPoint = (markPrice + yMax) / 2
const lowerMidPoint = (markPrice + yMin) / 2
return price > markPrice
? price > upperMidPoint
? 'bottom'
: 'top'
: price > lowerMidPoint
? 'bottom'
: 'top'
},
2023-07-21 11:47:53 -07:00
[markPrice, yMax, yMin],
2023-07-12 20:59:03 -07:00
)
2023-07-12 07:37:18 -07:00
const handleMouseMove: CategoricalChartFunc = (coords) => {
if (coords?.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
2023-07-02 22:55:25 -07:00
2023-07-12 07:37:18 -07:00
const handleMouseLeave = () => {
setMouseData(null)
}
return chartData.length ? (
2023-07-12 07:37:18 -07:00
<>
<div className="flex h-10 items-center border-b border-th-bkg-3 px-2 py-1">
2023-07-20 17:09:34 -07:00
<div className="flex w-full items-center">
2023-07-12 07:37:18 -07:00
<span className="w-16 font-mono text-xs text-th-fgd-3">
{priceRangePercent}%
</span>
<Slider
amount={parseFloat(priceRangePercent)}
max="100"
2023-07-12 07:37:18 -07:00
min="0.5"
onChange={(p) => setPriceRangePercentPercent(p)}
step={0.5}
2023-07-02 22:55:25 -07:00
/>
2023-07-12 07:37:18 -07:00
</div>
</div>
2023-08-17 03:56:38 -07:00
<div
className={
2023-08-31 20:22:50 -07:00
increaseHeight ? 'h-[570px]' : isTablet ? 'h-[538px]' : 'h-[482px]'
2023-08-17 03:56:38 -07:00
}
>
2023-07-12 07:37:18 -07:00
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={chartData}
layout="vertical"
margin={{
2023-07-12 20:59:03 -07:00
top: 8,
2023-07-13 15:53:50 -07:00
left: -8,
2023-07-02 22:55:25 -07:00
}}
2023-07-12 07:37:18 -07:00
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<XAxis
axisLine={false}
reversed={true}
domain={[() => 0, () => xMax]}
type="number"
tick={false}
tickLine={false}
tickFormatter={(tick) => xTickFormatter(tick)}
2023-07-12 07:37:18 -07:00
/>
<YAxis
dataKey="price"
reversed={true}
domain={[() => yMin, () => yMax]}
axisLine={false}
tick={{
2023-07-12 20:59:03 -07:00
fill: 'var(--fgd-4)',
2023-07-12 07:37:18 -07:00
fontSize: 8,
}}
2023-07-13 15:53:50 -07:00
tickCount={Y_TICK_COUNT}
2023-07-12 07:37:18 -07:00
tickLine={false}
2023-07-13 15:53:50 -07:00
tickFormatter={(tick) => yTickFormatter(tick)}
2023-07-12 20:59:03 -07:00
/>
<Area
type="stepBefore"
2023-07-12 20:59:03 -07:00
dataKey="bids"
stroke={COLORS.UP[theme]}
fill="url(#bidsGradient)"
isAnimationActive={false}
strokeWidth={1}
2023-07-12 20:59:03 -07:00
/>
<Area
type="stepBefore"
2023-07-12 20:59:03 -07:00
dataKey="asks"
stroke={COLORS.DOWN[theme]}
fill="url(#asksGradient)"
isAnimationActive={false}
strokeWidth={1}
2023-07-12 07:37:18 -07:00
/>
2023-07-12 17:32:22 -07:00
<ReferenceLine
y={mouseData?.price}
2023-07-12 20:59:03 -07:00
stroke={getChartReferenceColor(mouseData?.price)}
2023-07-12 17:32:22 -07:00
strokeDasharray="3, 3"
2023-07-12 20:59:03 -07:00
>
<Label
2023-07-13 15:53:50 -07:00
value={mouseData ? priceFormatter(mouseData.price) : ''}
fontSize={9}
2023-07-12 20:59:03 -07:00
fill={getChartReferenceColor(mouseData?.price)}
position="left"
2023-07-13 15:53:50 -07:00
offset={5}
2023-07-12 20:59:03 -07:00
/>
<Label
value={getPercentFromMarkPrice(mouseData?.price)}
2023-07-13 15:53:50 -07:00
fontSize={9}
2023-07-12 20:59:03 -07:00
fill={getChartReferenceColor(opposingMouseReference?.price)}
position={getPercentLabelPosition(mouseData?.price)}
2023-07-13 15:53:50 -07:00
offset={6}
2023-07-12 20:59:03 -07:00
/>
</ReferenceLine>
2023-07-12 07:37:18 -07:00
<ReferenceLine
y={opposingMouseReference?.price}
2023-07-12 20:59:03 -07:00
stroke={getChartReferenceColor(opposingMouseReference?.price)}
2023-07-12 17:32:22 -07:00
strokeDasharray="3, 3"
2023-07-12 20:59:03 -07:00
>
<Label
2023-07-13 15:53:50 -07:00
value={
opposingMouseReference
? priceFormatter(opposingMouseReference.price)
: ''
}
fontSize={9}
2023-07-12 20:59:03 -07:00
fill={getChartReferenceColor(opposingMouseReference?.price)}
position="left"
2023-07-13 15:53:50 -07:00
offset={5}
2023-07-12 20:59:03 -07:00
/>
<Label
value={getPercentFromMarkPrice(opposingMouseReference?.price)}
2023-07-13 15:53:50 -07:00
fontSize={9}
2023-07-12 20:59:03 -07:00
fill={getChartReferenceColor(mouseData?.price)}
position={getPercentLabelPosition(
2023-07-21 11:47:53 -07:00
opposingMouseReference?.price,
2023-07-12 20:59:03 -07:00
)}
2023-07-13 15:53:50 -07:00
offset={6}
2023-07-12 20:59:03 -07:00
/>
</ReferenceLine>
2023-07-12 17:32:22 -07:00
<ReferenceLine
2023-07-12 20:59:03 -07:00
stroke={getChartReferenceColor(mouseData?.price)}
strokeDasharray="3, 3"
segment={
mouseData && mouseData?.price >= markPrice
? [
{ x: mouseData?.cumulativeSize, y: markPrice },
{ x: mouseData?.cumulativeSize, y: yMax },
]
: [
{ x: mouseData?.cumulativeSize, y: yMin },
{ x: mouseData?.cumulativeSize, y: markPrice },
]
}
>
<Label
value={getSizeFromMouseData(mouseData?.cumulativeSize)}
2023-07-13 15:53:50 -07:00
fontSize={9}
fill={getChartReferenceColor(mouseData?.price)}
2023-07-12 20:59:03 -07:00
position={getSizeLabelPosition(
mouseData?.cumulativeSize,
2023-07-21 11:47:53 -07:00
mouseData?.price,
2023-07-12 20:59:03 -07:00
)}
2023-07-13 15:53:50 -07:00
offset={6}
2023-07-12 20:59:03 -07:00
/>
</ReferenceLine>
<ReferenceLine
stroke={getChartReferenceColor(opposingMouseReference?.price)}
2023-07-12 17:32:22 -07:00
strokeDasharray="3, 3"
segment={
opposingMouseReference &&
opposingMouseReference?.price >= markPrice
? [
{
x: opposingMouseReference?.cumulativeSize,
y: markPrice,
},
{ x: opposingMouseReference?.cumulativeSize, y: yMax },
]
: [
{ x: opposingMouseReference?.cumulativeSize, y: yMin },
{
x: opposingMouseReference?.cumulativeSize,
y: markPrice,
},
]
}
2023-07-12 20:59:03 -07:00
>
<Label
value={getSizeFromMouseData(
2023-07-21 11:47:53 -07:00
opposingMouseReference?.cumulativeSize,
2023-07-12 20:59:03 -07:00
)}
2023-07-13 15:53:50 -07:00
fontSize={9}
fill={getChartReferenceColor(opposingMouseReference?.price)}
2023-07-12 20:59:03 -07:00
position={getSizeLabelPosition(
opposingMouseReference?.cumulativeSize,
2023-07-21 11:47:53 -07:00
opposingMouseReference?.price,
2023-07-12 20:59:03 -07:00
)}
2023-07-13 15:53:50 -07:00
offset={6}
2023-07-12 20:59:03 -07:00
/>
</ReferenceLine>
2023-07-13 15:53:50 -07:00
{markPrice ? (
<ReferenceLine y={markPrice} stroke="var(--bkg-4)">
<Label
value={priceFormatter(markPrice)}
content={<MarkPriceLabel />}
/>
</ReferenceLine>
) : null}
2023-07-12 07:37:18 -07:00
<defs>
2023-07-13 15:53:50 -07:00
<linearGradient id="bidsGradient" x1="0" y1="0" x2="1" y2="0">
2023-07-12 07:37:18 -07:00
<stop
offset="0%"
stopColor={COLORS.UP[theme]}
stopOpacity={0.15}
/>
<stop
offset="99%"
stopColor={COLORS.UP[theme]}
stopOpacity={0}
/>
</linearGradient>
2023-07-13 15:53:50 -07:00
<linearGradient id="asksGradient" x1="0" y1="0" x2="1" y2="0">
2023-07-12 07:37:18 -07:00
<stop
offset="0%"
stopColor={COLORS.DOWN[theme]}
stopOpacity={0.15}
/>
<stop
offset="99%"
stopColor={COLORS.DOWN[theme]}
stopOpacity={0}
/>
</linearGradient>
</defs>
</AreaChart>
</ResponsiveContainer>
</div>
</>
) : null
2023-06-29 20:05:41 -07:00
}
export default DepthChart