Commit aea3c1f6 authored by Justin Domingue's avatar Justin Domingue Committed by GitHub

feat(pools): add liquidity chart range input primitives (#1990)

* first iteration of liquidity chart primitives

* add tickprocessed type

* clean up
parent e0c62567
...@@ -20,11 +20,11 @@ ...@@ -20,11 +20,11 @@
"@react-hook/window-scroll": "^1.3.0", "@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.0", "@reduxjs/toolkit": "^1.6.0",
"@typechain/ethers-v5": "^7.0.0", "@typechain/ethers-v5": "^7.0.0",
"@types/d3": "^6.7.1",
"@types/jest": "^25.2.1", "@types/jest": "^25.2.1",
"@types/lingui__core": "^2.7.1", "@types/lingui__core": "^2.7.1",
"@types/lingui__macro": "^2.7.4", "@types/lingui__macro": "^2.7.4",
"@types/lingui__react": "^2.8.3", "@types/lingui__react": "^2.8.3",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lodash.flatmap": "^4.5.6", "@types/lodash.flatmap": "^4.5.6",
"@types/luxon": "^1.24.4", "@types/luxon": "^1.24.4",
"@types/ms.macro": "^2.0.0", "@types/ms.macro": "^2.0.0",
...@@ -66,6 +66,7 @@ ...@@ -66,6 +66,7 @@
"copy-to-clipboard": "^3.2.0", "copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"cypress": "^4.11.0", "cypress": "^4.11.0",
"d3": "^7.0.0",
"eslint": "^7.11.0", "eslint": "^7.11.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
...@@ -76,7 +77,6 @@ ...@@ -76,7 +77,6 @@
"graphql-request": "^3.4.0", "graphql-request": "^3.4.0",
"inter-ui": "^3.13.1", "inter-ui": "^3.13.1",
"lightweight-charts": "^3.3.0", "lightweight-charts": "^3.3.0",
"lodash.clonedeep": "^4.5.0",
"lodash.flatmap": "^4.5.0", "lodash.flatmap": "^4.5.0",
"luxon": "^1.25.0", "luxon": "^1.25.0",
"ms.macro": "^2.0.0", "ms.macro": "^2.0.0",
......
import React, { useMemo } from 'react'
import { area, curveStepAfter, ScaleLinear } from 'd3'
import styled from 'styled-components/macro'
import { ChartEntry } from './types'
import inRange from 'lodash/inRange'
const Path = styled.path<{ fill: string | undefined }>`
opacity: 0.5;
stroke: ${({ fill, theme }) => fill ?? theme.blue2};
fill: ${({ fill, theme }) => fill ?? theme.blue2};
`
export const Area = ({
series,
xScale,
yScale,
xValue,
yValue,
fill,
}: {
series: ChartEntry[]
xScale: ScaleLinear<number, number>
yScale: ScaleLinear<number, number>
xValue: (d: ChartEntry) => number
yValue: (d: ChartEntry) => number
fill?: string | undefined
}) =>
useMemo(
() => (
<Path
fill={fill}
d={
area()
.curve(curveStepAfter)
.x((d: unknown) => xScale(xValue(d as ChartEntry)))
.y1((d: unknown) => yScale(yValue(d as ChartEntry)))
.y0(yScale(0))(
series.filter((d) => inRange(xScale(xValue(d)), 0, innerWidth)) as Iterable<[number, number]>
) ?? undefined
}
/>
),
[fill, series, xScale, xValue, yScale, yValue]
)
import React, { useMemo } from 'react'
import { Axis as d3Axis, axisBottom, NumberValue, ScaleLinear, select } from 'd3'
import styled from 'styled-components/macro'
const StyledGroup = styled.g`
line {
display: none;
}
text {
color: ${({ theme }) => theme.text2};
transform: translateY(5px);
}
`
const Axis = ({ axisGenerator }: { axisGenerator: d3Axis<NumberValue> }) => {
const axisRef = (axis: SVGGElement) => {
axis &&
select(axis)
.call(axisGenerator)
.call((g) => g.select('.domain').remove())
}
return <g ref={axisRef} />
}
export const AxisBottom = ({
xScale,
innerHeight,
offset = 5,
}: {
xScale: ScaleLinear<number, number>
innerHeight: number
offset?: number
}) =>
useMemo(
() => (
<StyledGroup transform={`translate(0, ${innerHeight + offset})`}>
<Axis axisGenerator={axisBottom(xScale).ticks(6)} />
</StyledGroup>
),
[innerHeight, offset, xScale]
)
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BrushBehavior, brushX, D3BrushEvent, ScaleLinear, select } from 'd3'
import styled from 'styled-components/macro'
import { brushHandleAccentPath, brushHandlePath } from 'components/LiquidityChartRangeInput/svg'
import usePrevious from 'hooks/usePrevious'
const Handle = styled.path<{ color: string }>`
cursor: ew-resize;
pointer-events: none;
stroke-width: 4;
stroke: ${({ color }) => color};
fill: ${({ color }) => color};
`
const HandleAccent = styled.path`
cursor: ew-resize;
pointer-events: none;
stroke-width: 1.5;
stroke: ${({ theme }) => theme.white};
opacity: 0.6;
`
const LabelGroup = styled.g<{ visible: boolean }>`
opacity: ${({ visible }) => (visible ? '1' : '0')};
transition: opacity 300ms;
`
const TooltipBackground = styled.rect`
fill: ${({ theme }) => theme.bg2};
`
const Tooltip = styled.text`
text-anchor: middle;
font-size: 13px;
fill: ${({ theme }) => theme.text1};
`
export const Brush = ({
id,
xScale,
interactive,
brushLabelValue,
brushExtent,
setBrushExtent,
innerWidth,
innerHeight,
colors,
}: {
id: string
xScale: ScaleLinear<number, number>
interactive: boolean
brushLabelValue: (d: 'w' | 'e', x: number) => string
brushExtent: [number, number]
setBrushExtent: (extent: [number, number]) => void
innerWidth: number
innerHeight: number
colors: {
west: string
east: string
}
}) => {
const brushRef = useRef<SVGGElement | null>(null)
const brushBehavior = useRef<BrushBehavior<SVGGElement> | null>(null)
// only used to drag the handles on brush for performance
const [localBrushExtent, setLocalBrushExtent] = useState<[number, number] | null>(brushExtent)
const [showLabels, setShowLabels] = useState(false)
const [hovering, setHovering] = useState(false)
const previousBrushExtent = usePrevious(brushExtent)
const brushed = useCallback(
({ mode, type, selection }: D3BrushEvent<unknown>) => {
if (!selection) {
setLocalBrushExtent(null)
return
}
const scaled = (selection as [number, number]).map(xScale.invert) as [number, number]
// undefined `mode` means brush was programatically moved
// skip calling the handler to avoid a loop
if (type === 'end' && mode !== undefined) {
setBrushExtent(scaled)
}
setLocalBrushExtent(scaled)
},
[xScale.invert, setBrushExtent]
)
// keep local and external brush extent in sync
// i.e. snap to ticks on bruhs end
useEffect(() => {
setLocalBrushExtent(brushExtent)
}, [brushExtent])
// initialize the brush
useEffect(() => {
if (!brushRef.current) return
brushBehavior.current = brushX<SVGGElement>()
.extent([
[Math.max(0, xScale(0)), 0],
[innerWidth, innerHeight],
])
.handleSize(30)
.filter(() => interactive)
.on('brush end', brushed)
brushBehavior.current(select(brushRef.current))
if (
previousBrushExtent &&
(brushExtent[0] !== previousBrushExtent[0] || brushExtent[1] !== previousBrushExtent[1])
) {
select(brushRef.current)
.transition()
.call(brushBehavior.current.move as any, brushExtent.map(xScale))
}
// brush linear gradient
select(brushRef.current)
.selectAll('.selection')
.attr('stroke', 'none')
.attr('fill-opacity', '0.1')
.attr('fill', `url(#${id}-gradient-selection)`)
}, [brushExtent, brushed, id, innerHeight, innerWidth, interactive, previousBrushExtent, xScale])
// respond to xScale changes only
useEffect(() => {
if (!brushRef.current || !brushBehavior.current) return
brushBehavior.current.move(select(brushRef.current) as any, brushExtent.map(xScale) as any)
// dependency on brushExtent would start an update loop
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [xScale])
useEffect(() => {
setShowLabels(true)
const timeout = setTimeout(() => setShowLabels(false), 1500)
return () => clearTimeout(timeout)
}, [localBrushExtent])
const flipWestHandle = localBrushExtent && xScale(localBrushExtent[0]) > 15
const flipEastHandle = localBrushExtent && xScale(localBrushExtent[1]) > innerWidth - 15
return useMemo(
() => (
<>
<defs>
<linearGradient id={`${id}-gradient-selection`} x1="0%" y1="100%" x2="100%" y2="100%">
<stop stopColor={colors.west} />
<stop stopColor={colors.east} offset="1" />
</linearGradient>
{/* clips at exactly the svg area */}
<clipPath id={`${id}-brush-clip`}>
<rect x="0" y="0" width={innerWidth} height="100%" />
</clipPath>
<clipPath id={`${id}-handles-clip`}>
<rect x="0" y="0" width="100%" height="100%" />
</clipPath>
</defs>
{/* will host the d3 brush */}
<g
ref={brushRef}
clipPath={`url(#${id}-brush-clip)`}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
/>
{/* custom brush handles */}
{localBrushExtent && (
<>
{/* west handle */}
<g
transform={`translate(${Math.max(0, xScale(localBrushExtent[0]))}, 0), scale(${
flipWestHandle ? '-1' : '1'
}, 1)`}
>
<g clipPath={`url(#${id}-handles-clip)`}>
<Handle color={colors.west} d={brushHandlePath(innerHeight)} />
<HandleAccent d={brushHandleAccentPath()} />
</g>
<LabelGroup
transform={`translate(50,0), scale(${flipWestHandle ? '1' : '-1'}, 1)`}
visible={showLabels || hovering}
>
<TooltipBackground y="0" x="-30" height="30" width="60" rx="8" />
<Tooltip transform={`scale(-1, 1)`} y="15" dominantBaseline="middle">
{brushLabelValue('w', localBrushExtent[0])}
</Tooltip>
</LabelGroup>
</g>
{/* east handle */}
<g
transform={`translate(${Math.min(xScale(localBrushExtent[1]), innerWidth)}, 0), scale(${
flipEastHandle ? '-1' : '1'
}, 1)`}
>
<g clipPath={`url(#${id}-handles-clip)`}>
<Handle color={colors.east} d={brushHandlePath(innerHeight)} />
<HandleAccent d={brushHandleAccentPath()} />
</g>
<LabelGroup
transform={`translate(50,0), scale(${flipEastHandle ? '-1' : '1'}, 1)`}
visible={showLabels || hovering}
>
<TooltipBackground y="0" x="-30" height="30" width="60" rx="8" />
<Tooltip y="15" dominantBaseline="middle">
{brushLabelValue('e', localBrushExtent[1])}
</Tooltip>
</LabelGroup>
</g>
</>
)}
</>
),
[
brushLabelValue,
colors.east,
colors.west,
flipEastHandle,
flipWestHandle,
hovering,
id,
innerHeight,
innerWidth,
localBrushExtent,
showLabels,
xScale,
]
)
}
import { max, scaleLinear, ZoomTransform } from 'd3'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Area } from './Area'
import { AxisBottom } from './AxisBottom'
import { Brush } from './Brush'
import { Line } from './Line'
import { ChartEntry, LiquidityChartRangeInputProps } from './types'
import Zoom from './Zoom'
export const xAccessor = (d: ChartEntry) => d.price0
export const yAccessor = (d: ChartEntry) => d.activeLiquidity
export function Chart({
id = 'liquidityChartRangeInput',
data: { series, current },
styles,
dimensions: { width, height },
margins,
interactive = true,
brushDomain,
brushLabels,
onBrushDomainChange,
initialZoom,
}: LiquidityChartRangeInputProps) {
const svgRef = useRef<SVGSVGElement | null>(null)
const [zoom, setZoom] = useState<ZoomTransform>()
const [innerHeight, innerWidth] = useMemo(
() => [height - margins.top - margins.bottom, width - margins.left - margins.right],
[width, height, margins]
)
const { xScale, yScale } = useMemo(() => {
const scales = {
xScale: scaleLinear()
.domain([(1 - initialZoom) * current, (1 + initialZoom) * current] as number[])
.range([0, innerWidth]),
yScale: scaleLinear()
.domain([0, max(series, yAccessor)] as number[])
.range([innerHeight, 0]),
}
if (zoom) {
const newXscale = zoom.rescaleX(scales.xScale)
scales.xScale.domain(newXscale.domain())
}
return scales
}, [initialZoom, current, innerWidth, series, innerHeight, zoom])
useEffect(() => {
if (!brushDomain) {
onBrushDomainChange(xScale.domain() as [number, number])
}
}, [brushDomain, onBrushDomainChange, xScale])
// ensures the brush remains in view and adapts to zooms
xScale.clamp(true)
return (
<>
<Zoom
svg={svgRef.current}
xScale={xScale}
setZoom={setZoom}
innerWidth={innerWidth}
innerHeight={innerHeight}
showClear={Boolean(zoom && zoom.k !== 1)}
/>
<svg ref={svgRef} style={{ overflow: 'visible' }} width={width} height={height}>
<defs>
<clipPath id={`${id}-chart-clip`}>
<rect x="0" y="0" width={innerWidth} height={height} />
</clipPath>
{brushDomain && (
// mask to highlight selected area
<mask id={`${id}-chart-area-mask`}>
<rect
fill="white"
x={xScale(brushDomain[0])}
y="0"
width={xScale(brushDomain[1]) - xScale(brushDomain[0])}
height={innerHeight}
/>
</mask>
)}
</defs>
<g transform={`translate(${margins.left},${margins.top})`}>
<g clipPath={`url(#${id}-chart-clip)`}>
<Area series={series} xScale={xScale} yScale={yScale} xValue={xAccessor} yValue={yAccessor} />
{brushDomain && (
// duplicate area chart with mask for selected area
<g mask={`url(#${id}-chart-area-mask)`}>
<Area
series={series}
xScale={xScale}
yScale={yScale}
xValue={xAccessor}
yValue={yAccessor}
fill={styles.area.selection}
/>
</g>
)}
<Line value={current} xScale={xScale} innerHeight={innerHeight} />
<AxisBottom xScale={xScale} innerHeight={innerHeight} />
</g>
<Brush
id={id}
xScale={xScale}
interactive={interactive}
brushLabelValue={brushLabels}
brushExtent={brushDomain ?? (xScale.domain() as [number, number])}
innerWidth={innerWidth}
innerHeight={innerHeight}
setBrushExtent={onBrushDomainChange}
colors={{
west: styles.brush.handle.west,
east: styles.brush.handle.east,
}}
/>
</g>
</svg>
</>
)
}
import React, { useMemo } from 'react'
import { ScaleLinear } from 'd3'
import styled from 'styled-components/macro'
const StyledLine = styled.line`
opacity: 0.5;
stroke-width: 2;
stroke: ${({ theme }) => theme.text1};
fill: none;
`
export const Line = ({
value,
xScale,
innerHeight,
}: {
value: number
xScale: ScaleLinear<number, number>
innerHeight: number
}) =>
useMemo(
() => <StyledLine x1={xScale(value)} y1="0" x2={xScale(value)} y2={innerHeight} />,
[value, xScale, innerHeight]
)
import React, { useEffect, useMemo, useRef } from 'react'
import { ButtonGray } from 'components/Button'
import styled from 'styled-components/macro'
import { ScaleLinear, select, ZoomBehavior, zoom, ZoomTransform } from 'd3'
import { RefreshCcw, ZoomIn, ZoomOut } from 'react-feather'
const Wrapper = styled.div<{ count: number }>`
display: grid;
grid-template-columns: repeat(${({ count }) => count.toString()}, 1fr);
grid-gap: 6px;
position: absolute;
top: -75px;
right: 0;
`
const Button = styled(ButtonGray)`
&:hover {
background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text1};
}
width: 28px;
height: 28px;
padding: 4px;
`
export default function Zoom({
svg,
xScale,
setZoom,
innerWidth,
innerHeight,
showClear,
}: {
svg: SVGSVGElement | null
xScale: ScaleLinear<number, number>
setZoom: (transform: ZoomTransform) => void
innerWidth: number
innerHeight: number
showClear: boolean
}) {
const zoomBehavior = useRef<ZoomBehavior<Element, unknown>>()
const [zoomIn, zoomOut, reset] = useMemo(
() => [
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleBy, 2),
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleBy, 0.5),
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleTo, 1),
],
[svg, zoomBehavior]
)
useEffect(() => {
if (!svg) return
// zoom
zoomBehavior.current = zoom()
.scaleExtent([0.3, 10])
.translateExtent([
[0, 0],
[innerWidth, innerHeight],
])
.extent([
[0, 0],
[innerWidth, innerHeight],
])
.on('zoom', ({ transform }: { transform: ZoomTransform }) => setZoom(transform))
select(svg as Element)
.call(zoomBehavior.current)
.on('mousedown.zoom', null)
}, [innerHeight, innerWidth, setZoom, svg, xScale, zoomBehavior])
return (
<Wrapper count={showClear ? 3 : 2}>
{showClear && (
<Button onClick={reset} disabled={false}>
<RefreshCcw size={14} />
</Button>
)}
<Button onClick={zoomIn} disabled={false}>
<ZoomIn size={14} />
</Button>
<Button onClick={zoomOut} disabled={false}>
<ZoomOut size={14} />
</Button>
</Wrapper>
)
}
import { useEffect, useState } from 'react'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import { ChartEntry } from './types'
import JSBI from 'jsbi'
// Tick with fields parsed to JSBIs, and active liquidity computed.
export interface TickProcessed {
tickIdx: number
liquidityActive: JSBI
liquidityNet: JSBI
price0: string
}
export function useDensityChartData({
currencyA,
currencyB,
feeAmount,
}: {
currencyA: Currency | undefined
currencyB: Currency | undefined
feeAmount: FeeAmount | undefined
}) {
const [formattedData, setFormattedData] = useState<ChartEntry[] | undefined>()
const { isLoading, isUninitialized, isError, error, activeTick, data } = usePoolActiveLiquidity(
currencyA,
currencyB,
feeAmount
)
useEffect(() => {
// clear data when inputs are cleared
setFormattedData(undefined)
}, [currencyA, currencyB, feeAmount])
useEffect(() => {
function formatData() {
if (!data?.length) {
return
}
const newData: ChartEntry[] = []
for (let i = 0; i < data.length; i++) {
const t: TickProcessed = data[i]
const chartEntry = {
activeLiquidity: parseFloat(t.liquidityActive.toString()),
price0: parseFloat(t.price0),
}
newData.push(chartEntry)
}
if (newData) {
setFormattedData(newData)
}
}
if (!isLoading) {
formatData()
}
}, [isLoading, activeTick, data])
return {
isLoading,
isUninitialized,
isError,
error,
formattedData,
}
}
import React, { ReactNode, useCallback, useMemo } from 'react'
import { Trans } from '@lingui/macro'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import { AutoColumn, ColumnCenter } from 'components/Column'
import Loader from 'components/Loader'
import { useColor } from 'hooks/useColor'
import useTheme from 'hooks/useTheme'
import { saturate } from 'polished'
import { BarChart2, Inbox, CloudOff } from 'react-feather'
import { batch } from 'react-redux'
import styled from 'styled-components/macro'
import { TYPE } from '../../theme'
import { Chart } from './Chart'
import { useDensityChartData } from './hooks'
import { format } from 'd3'
import { Bound } from 'state/mint/v3/actions'
import { FeeAmount } from '@uniswap/v3-sdk'
import ReactGA from 'react-ga'
const ChartWrapper = styled.div`
display: grid;
position: relative;
justify-content: center;
align-content: center;
`
function InfoBox({ message, icon }: { message?: ReactNode; icon: ReactNode }) {
return (
<ColumnCenter style={{ height: '100%', justifyContent: 'center' }}>
{icon}
{message && (
<TYPE.mediumHeader padding={10} marginTop="20px">
{message}
</TYPE.mediumHeader>
)}
</ColumnCenter>
)
}
export default function LiquidityChartRangeInput({
currencyA,
currencyB,
feeAmount,
ticksAtLimit,
price,
priceLower,
priceUpper,
onLeftRangeInput,
onRightRangeInput,
interactive,
}: {
currencyA: Currency | undefined
currencyB: Currency | undefined
feeAmount?: number
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
price: number | undefined
priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token>
onLeftRangeInput: (typedValue: string) => void
onRightRangeInput: (typedValue: string) => void
interactive: boolean
}) {
const theme = useTheme()
const tokenAColor = useColor(currencyA?.wrapped)
const tokenBColor = useColor(currencyB?.wrapped)
const { isLoading, isUninitialized, isError, error, formattedData } = useDensityChartData({
currencyA,
currencyB,
feeAmount,
})
const onBrushDomainChangeEnded = useCallback(
(domain) => {
const leftRangeValue = Number(domain[0])
const rightRangeValue = Number(domain[1])
ReactGA.event({
category: 'Liquidity',
action: 'Chart brushed',
})
batch(() => {
// simulate user input for auto-formatting and other validations
leftRangeValue > 0 && onLeftRangeInput(leftRangeValue.toFixed(6))
rightRangeValue > 0 && onRightRangeInput(rightRangeValue.toFixed(6))
})
},
[onLeftRangeInput, onRightRangeInput]
)
interactive = interactive && Boolean(formattedData?.length)
const brushDomain: [number, number] | undefined = useMemo(() => {
const isSorted = currencyA && currencyB && currencyA?.wrapped.sortsBefore(currencyB?.wrapped)
const leftPrice = isSorted ? priceLower : priceUpper?.invert()
const rightPrice = isSorted ? priceUpper : priceLower?.invert()
return leftPrice && rightPrice
? [parseFloat(leftPrice?.toSignificant(5)), parseFloat(rightPrice?.toSignificant(5))]
: undefined
}, [currencyA, currencyB, priceLower, priceUpper])
const brushLabelValue = useCallback(
(d: 'w' | 'e', x: number) => {
if (!price) return ''
if (d === 'w' && ticksAtLimit[Bound.LOWER]) return '0'
if (d === 'e' && ticksAtLimit[Bound.UPPER]) return ''
const percent = (((x < price ? -1 : 1) * (Math.max(x, price) - Math.min(x, price))) / Math.min(x, price)) * 100
return price ? `${format(Math.abs(percent) > 1 ? '.2~s' : '.2~f')(percent)}%` : ''
},
[price, ticksAtLimit]
)
if (isError) {
ReactGA.exception({
...error,
category: 'Liquidity',
fatal: false,
})
if (error?.name === 'UnsupportedChainId') {
// do not show the chart container when the chain is not supported
return null
}
}
return (
<AutoColumn gap="md" style={{ minHeight: '200px' }}>
{isUninitialized ? (
<InfoBox
message={<Trans>Your position will appear here.</Trans>}
icon={<Inbox size={56} stroke={theme.text1} />}
/>
) : isLoading ? (
<InfoBox icon={<Loader size="40px" stroke={theme.text4} />} />
) : isError ? (
<InfoBox
message={<Trans>Subgraph data not available</Trans>}
icon={<CloudOff size={56} stroke={theme.text4} />}
/>
) : !formattedData || formattedData === [] || !price ? (
<InfoBox
message={<Trans>There is no liquidity data</Trans>}
icon={<BarChart2 size={56} stroke={theme.text4} />}
/>
) : (
<ChartWrapper>
<Chart
data={{ series: formattedData, current: price }}
dimensions={{ width: 400, height: 200 }}
margins={{ top: 10, right: 2, bottom: 30, left: 0 }}
styles={{
area: {
selection: theme.blue1,
},
brush: {
handle: {
west: saturate(0.1, tokenAColor) ?? theme.red1,
east: saturate(0.1, tokenBColor) ?? theme.blue1,
},
},
}}
interactive={interactive}
brushLabels={brushLabelValue}
brushDomain={brushDomain}
onBrushDomainChange={onBrushDomainChangeEnded}
initialZoom={feeAmount === FeeAmount.LOW ? 0.02 : 0.3}
/>
</ChartWrapper>
)}
</AutoColumn>
)
}
/*
* Generates an SVG path for the east brush handle.
* Apply `scale(-1, 1)` to generate west brush handle.
*
* |```````\
* | | | |
* |______/
* |
* |
* |
* |
* |
*
* https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90
*/
export const brushHandlePath = (height: number) =>
[
// handle
`M 0 0`, // move to origin
`v ${height}`, // vertical line
'm 1 0', // move 1px to the right
`V 0`, // second vertical line
`M 0 2`, // move to origin
// head
'h 12', // horizontal line
'q 2 0, 2 2', // rounded corner
'v 22', // vertical line
'q 0 2 -2 2', // rounded corner
'h -12', // horizontal line
`z`, // close path
].join(' ')
export const brushHandleAccentPath = () =>
[
'm 6 8', // move to first accent
'v 14', // vertical line
'M 0 0', // move to origin
'm 10 8', // move to second accent
'v 14', // vertical line
'z',
].join(' ')
export interface ChartEntry {
activeLiquidity: number
price0: number
}
export interface Dimensions {
width: number
height: number
}
export interface Margins {
top: number
right: number
bottom: number
left: number
}
export interface LiquidityChartRangeInputProps {
// to distringuish between multiple charts in the DOM
id?: string
data: {
series: ChartEntry[]
current: number
}
styles: {
area: {
// color of the ticks in range
selection: string
}
brush: {
handle: {
west: string
east: string
}
}
}
dimensions: Dimensions
margins: Margins
interactive?: boolean
brushLabels: (d: 'w' | 'e', x: number) => string
brushDomain: [number, number] | undefined
onBrushDomainChange: (domain: [number, number]) => void
initialZoom: number
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment