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:
cartcrom 2022-08-10 15:15:41 -04:00 committed by GitHub
parent 4b5246394b
commit b26c2bbc98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 38 deletions

@ -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)}>

@ -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>
)