feat: added token sparklines to explore page (#4307)
* added sparklines, fixed overlay issue * refactored, made LineChart generic * memoized line and sparkline charts, used theme z-index
This commit is contained in:
parent
4b5246394b
commit
b26c2bbc98
53
src/components/Charts/LineChart.tsx
Normal file
53
src/components/Charts/LineChart.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { Group } from '@visx/group'
|
||||
import { LinePath } from '@visx/shape'
|
||||
import { CurveFactory } from 'd3'
|
||||
import { radius } from 'd3-curve-circlecorners'
|
||||
import React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { useTheme } from 'styled-components/macro'
|
||||
import { Color } from 'theme/styled'
|
||||
|
||||
interface LineChartProps<T> {
|
||||
data: T[]
|
||||
getX: (t: T) => number
|
||||
getY: (t: T) => number
|
||||
marginTop: number
|
||||
curve?: CurveFactory
|
||||
color?: Color
|
||||
strokeWidth: number
|
||||
children?: ReactNode
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function LineChart<T>({
|
||||
data,
|
||||
getX,
|
||||
getY,
|
||||
marginTop,
|
||||
curve,
|
||||
color,
|
||||
strokeWidth,
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
}: LineChartProps<T>) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<Group top={marginTop}>
|
||||
<LinePath
|
||||
curve={curve ?? radius(0.25)}
|
||||
stroke={color ?? theme.accentAction}
|
||||
strokeWidth={strokeWidth}
|
||||
data={data}
|
||||
x={getX}
|
||||
y={getY}
|
||||
/>
|
||||
</Group>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(LineChart) as typeof LineChart
|
@ -2,10 +2,8 @@ import { AxisBottom, TickFormatter } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Group } from '@visx/group'
|
||||
import { Line, LinePath } from '@visx/shape'
|
||||
import { Line } from '@visx/shape'
|
||||
import { bisect, curveBasis, NumberValue, scaleLinear } from 'd3'
|
||||
import { radius } from 'd3-curve-circlecorners'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { TimePeriod } from 'hooks/useTopTokens'
|
||||
@ -23,6 +21,7 @@ import {
|
||||
} from 'utils/formatChartTimes'
|
||||
|
||||
import data from './data.json'
|
||||
import LineChart from './LineChart'
|
||||
|
||||
const TIME_DISPLAYS: [TimePeriod, string][] = [
|
||||
[TimePeriod.hour, '1H'],
|
||||
@ -204,7 +203,17 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
<svg width={graphWidth} height={graphHeight}>
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={margin.top}
|
||||
/* Default curve doesn't look good for the ALL chart */
|
||||
curve={activeTimePeriod === TimePeriod.all ? curveBasis : undefined}
|
||||
strokeWidth={2}
|
||||
width={graphWidth}
|
||||
height={graphHeight}
|
||||
>
|
||||
<AxisBottom
|
||||
scale={timeScale}
|
||||
stroke={theme.backgroundOutline}
|
||||
@ -240,31 +249,16 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
<GlyphCircle
|
||||
left={selected.xCoordinate}
|
||||
top={rdScale(selected.pricePoint.value) + margin.top}
|
||||
size={50}
|
||||
fill={theme.accentActive}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
<Group top={margin.top}>
|
||||
<LinePath
|
||||
/* ALL chart renders poorly using circle corners; use d3 curve for ALL instead */
|
||||
curve={activeTimePeriod === TimePeriod.all ? curveBasis : radius(0.25)}
|
||||
stroke={theme.accentActive}
|
||||
strokeWidth={2}
|
||||
data={pricePoints}
|
||||
x={(d: PricePoint) => timeScale(d.timestamp) ?? 0}
|
||||
y={(d: PricePoint) => rdScale(d.value) ?? 0}
|
||||
/>
|
||||
{selected.xCoordinate !== null && (
|
||||
<g>
|
||||
<GlyphCircle
|
||||
left={selected.xCoordinate}
|
||||
top={rdScale(selected.pricePoint.value)}
|
||||
size={50}
|
||||
fill={theme.accentActive}
|
||||
stroke={theme.backgroundOutline}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>
|
||||
)}
|
||||
</Group>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
@ -276,7 +270,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
|
||||
onMouseMove={handleHover}
|
||||
onMouseLeave={() => setSelected(initialState)}
|
||||
/>
|
||||
</svg>
|
||||
</LineChart>
|
||||
<TimeOptionsContainer>
|
||||
{TIME_DISPLAYS.map(([value, display]) => (
|
||||
<TimeButton key={display} active={activeTimePeriod === value} onClick={() => setTimePeriod(value)}>
|
||||
|
49
src/components/Charts/SparklineChart.tsx
Normal file
49
src/components/Charts/SparklineChart.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { scaleLinear } from 'd3'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import React from 'react'
|
||||
|
||||
import data from './data.json'
|
||||
import LineChart from './LineChart'
|
||||
|
||||
type PricePoint = { value: number; timestamp: number }
|
||||
|
||||
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
|
||||
const prices = pricePoints.map((x) => x.value)
|
||||
const min = Math.min(...prices)
|
||||
const max = Math.max(...prices)
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
interface SparklineChartProps {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
function SparklineChart({ width, height }: SparklineChartProps) {
|
||||
const theme = useTheme()
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const pricePoints = data.day
|
||||
const startingPrice = pricePoints[0]
|
||||
const endingPrice = pricePoints[pricePoints.length - 1]
|
||||
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([height, 0])
|
||||
|
||||
const isPositive = endingPrice.value >= startingPrice.value
|
||||
|
||||
return (
|
||||
<LineChart
|
||||
data={pricePoints}
|
||||
getX={(p: PricePoint) => timeScale(p.timestamp)}
|
||||
getY={(p: PricePoint) => rdScale(p.value)}
|
||||
marginTop={0}
|
||||
color={isPositive ? theme.accentSuccess : theme.accentFailure}
|
||||
strokeWidth={1.5}
|
||||
width={width}
|
||||
height={height}
|
||||
></LineChart>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(SparklineChart)
|
@ -5,6 +5,7 @@ import { Check, Link, Share, Twitter } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { Z_INDEX } from 'theme'
|
||||
|
||||
const TWITTER_WIDTH = 560
|
||||
const TWITTER_HEIGHT = 480
|
||||
@ -13,6 +14,7 @@ const ShareButtonDisplay = styled.div`
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ParentSize } from '@visx/responsive'
|
||||
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
|
||||
import { EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { useCurrency, useToken } from 'hooks/Tokens'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
@ -222,15 +224,9 @@ const SparkLineCell = styled(Cell)`
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
const SparkLineImg = styled(Cell)<{ isPositive: boolean }>`
|
||||
max-width: 124px;
|
||||
max-height: 28px;
|
||||
flex-direction: column;
|
||||
transform: scale(1.2);
|
||||
|
||||
polyline {
|
||||
stroke: ${({ theme, isPositive }) => (isPositive ? theme.accentSuccess : theme.accentFailure)};
|
||||
}
|
||||
const SparkLine = styled(Cell)`
|
||||
width: 124px;
|
||||
height: 42px;
|
||||
`
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
@ -507,7 +503,11 @@ export default function LoadedRow({
|
||||
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>}
|
||||
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>}
|
||||
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).toUpperCase()}</ClickableContent>}
|
||||
sparkLine={<SparkLineImg dangerouslySetInnerHTML={{ __html: tokenData.sparkline }} isPositive={isPositive} />}
|
||||
sparkLine={
|
||||
<SparkLine>
|
||||
<ParentSize>{({ width, height }) => <SparklineChart width={width} height={height} />}</ParentSize>
|
||||
</SparkLine>
|
||||
}
|
||||
/>
|
||||
</StyledLink>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user