Commit b26c2bbc authored by cartcrom's avatar cartcrom Committed by GitHub

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
parent 4b524639
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' ...@@ -2,10 +2,8 @@ import { AxisBottom, TickFormatter } from '@visx/axis'
import { localPoint } from '@visx/event' import { localPoint } from '@visx/event'
import { EventType } from '@visx/event/lib/types' import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph' import { GlyphCircle } from '@visx/glyph'
import { Group } from '@visx/group' import { Line } from '@visx/shape'
import { Line, LinePath } from '@visx/shape'
import { bisect, curveBasis, NumberValue, scaleLinear } from 'd3' import { bisect, curveBasis, NumberValue, scaleLinear } from 'd3'
import { radius } from 'd3-curve-circlecorners'
import { useActiveLocale } from 'hooks/useActiveLocale' import { useActiveLocale } from 'hooks/useActiveLocale'
import useTheme from 'hooks/useTheme' import useTheme from 'hooks/useTheme'
import { TimePeriod } from 'hooks/useTopTokens' import { TimePeriod } from 'hooks/useTopTokens'
...@@ -23,6 +21,7 @@ import { ...@@ -23,6 +21,7 @@ import {
} from 'utils/formatChartTimes' } from 'utils/formatChartTimes'
import data from './data.json' import data from './data.json'
import LineChart from './LineChart'
const TIME_DISPLAYS: [TimePeriod, string][] = [ const TIME_DISPLAYS: [TimePeriod, string][] = [
[TimePeriod.hour, '1H'], [TimePeriod.hour, '1H'],
...@@ -204,7 +203,17 @@ export function PriceChart({ width, height }: PriceChartProps) { ...@@ -204,7 +203,17 @@ export function PriceChart({ width, height }: PriceChartProps) {
<ArrowCell>{arrow}</ArrowCell> <ArrowCell>{arrow}</ArrowCell>
</DeltaContainer> </DeltaContainer>
</ChartHeader> </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 <AxisBottom
scale={timeScale} scale={timeScale}
stroke={theme.backgroundOutline} stroke={theme.backgroundOutline}
...@@ -240,31 +249,16 @@ export function PriceChart({ width, height }: PriceChartProps) { ...@@ -240,31 +249,16 @@ export function PriceChart({ width, height }: PriceChartProps) {
pointerEvents="none" pointerEvents="none"
strokeDasharray="4,4" 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> </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 <rect
x={0} x={0}
y={0} y={0}
...@@ -276,7 +270,7 @@ export function PriceChart({ width, height }: PriceChartProps) { ...@@ -276,7 +270,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
onMouseMove={handleHover} onMouseMove={handleHover}
onMouseLeave={() => setSelected(initialState)} onMouseLeave={() => setSelected(initialState)}
/> />
</svg> </LineChart>
<TimeOptionsContainer> <TimeOptionsContainer>
{TIME_DISPLAYS.map(([value, display]) => ( {TIME_DISPLAYS.map(([value, display]) => (
<TimeButton key={display} active={activeTimePeriod === value} onClick={() => setTimePeriod(value)}> <TimeButton key={display} active={activeTimePeriod === value} onClick={() => setTimePeriod(value)}>
......
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' ...@@ -5,6 +5,7 @@ import { Check, Link, Share, Twitter } from 'react-feather'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks' import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer' import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { Z_INDEX } from 'theme'
const TWITTER_WIDTH = 560 const TWITTER_WIDTH = 560
const TWITTER_HEIGHT = 480 const TWITTER_HEIGHT = 480
...@@ -13,6 +14,7 @@ const ShareButtonDisplay = styled.div` ...@@ -13,6 +14,7 @@ const ShareButtonDisplay = styled.div`
display: flex; display: flex;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
z-index: ${Z_INDEX.dropdown};
&:hover { &:hover {
color: ${({ theme }) => darken(0.1, theme.textSecondary)}; color: ${({ theme }) => darken(0.1, theme.textSecondary)};
......
import { Trans } from '@lingui/macro' import { Trans } from '@lingui/macro'
import { ParentSize } from '@visx/responsive'
import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics' import { sendAnalyticsEvent } from 'components/AmplitudeAnalytics'
import { EventName } from 'components/AmplitudeAnalytics/constants' import { EventName } from 'components/AmplitudeAnalytics/constants'
import SparklineChart from 'components/Charts/SparklineChart'
import CurrencyLogo from 'components/CurrencyLogo' import CurrencyLogo from 'components/CurrencyLogo'
import { useCurrency, useToken } from 'hooks/Tokens' import { useCurrency, useToken } from 'hooks/Tokens'
import useTheme from 'hooks/useTheme' import useTheme from 'hooks/useTheme'
...@@ -222,15 +224,9 @@ const SparkLineCell = styled(Cell)` ...@@ -222,15 +224,9 @@ const SparkLineCell = styled(Cell)`
display: none; display: none;
} }
` `
const SparkLineImg = styled(Cell)<{ isPositive: boolean }>` const SparkLine = styled(Cell)`
max-width: 124px; width: 124px;
max-height: 28px; height: 42px;
flex-direction: column;
transform: scale(1.2);
polyline {
stroke: ${({ theme, isPositive }) => (isPositive ? theme.accentSuccess : theme.accentFailure)};
}
` `
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
text-decoration: none; text-decoration: none;
...@@ -507,7 +503,11 @@ export default function LoadedRow({ ...@@ -507,7 +503,11 @@ export default function LoadedRow({
percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>} percentChange={<ClickableContent>{tokenPercentChangeInfo}</ClickableContent>}
marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>} marketCap={<ClickableContent>{formatAmount(tokenData.marketCap).toUpperCase()}</ClickableContent>}
volume={<ClickableContent>{formatAmount(tokenData.volume[timePeriod]).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> </StyledLink>
) )
......
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