feat: adding timeline to explore price chart (#4292)

* added timeline & time options selector functionality to price chart
This commit is contained in:
cartcrom 2022-08-08 11:55:33 -04:00 committed by GitHub
parent bb2e67fc5a
commit 77c4e74fc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1808 additions and 746 deletions

@ -6,7 +6,7 @@ import styled from 'styled-components/macro'
import { LinkStyledButton } from 'theme'
const CopyIcon = styled(LinkStyledButton)`
color: ${({ color, theme }) => color || theme.deprecated_text3};
color: ${({ color, theme }) => color || theme.accentAction};
flex-shrink: 0;
display: flex;
text-decoration: none;
@ -14,7 +14,7 @@ const CopyIcon = styled(LinkStyledButton)`
:active,
:focus {
text-decoration: none;
color: ${({ color, theme }) => color || theme.deprecated_text2};
color: ${({ color, theme }) => color || theme.accentAction};
}
`
const StyledText = styled.span`

@ -1,17 +1,38 @@
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 { bisect, scaleLinear } from 'd3'
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'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
import styled from 'styled-components/macro'
import {
dayHourFormatter,
hourFormatter,
monthDayFormatter,
monthFormatter,
monthYearDayFormatter,
monthYearFormatter,
weekFormatter,
} from 'utils/formatChartTimes'
import data from './data.json'
const TIME_DISPLAYS: [TimePeriod, string][] = [
[TimePeriod.hour, '1H'],
[TimePeriod.day, '1D'],
[TimePeriod.week, '1W'],
[TimePeriod.month, '1M'],
[TimePeriod.year, '1Y'],
[TimePeriod.all, 'ALL'],
]
type PricePoint = { value: number; timestamp: number }
function getPriceBounds(pricePoints: PricePoint[]): [number, number] {
@ -43,6 +64,7 @@ function getDelta(start: number, current: number) {
export const ChartWrapper = styled.div`
position: relative;
overflow: visible;
`
export const ChartHeader = styled.div`
@ -62,6 +84,55 @@ const ArrowCell = styled.div`
padding-left: 2px;
display: flex;
`
export const TimeOptionsContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 4px;
gap: 4px;
`
const TimeButton = styled.button<{ active: boolean }>`
background-color: ${({ theme, active }) => (active ? theme.accentActive : 'transparent')};
font-size: 14px;
width: 36px;
height: 36px;
border-radius: 12px;
border: none;
cursor: pointer;
color: ${({ theme }) => theme.textPrimary};
`
function getTicks(startTimestamp: number, endTimestamp: number, numTicks = 5) {
return Array.from(
{ length: numTicks },
(v, i) => endTimestamp - ((endTimestamp - startTimestamp) / (numTicks + 1)) * (i + 1)
)
}
function tickFormat(
startTimestamp: number,
endTimestamp: number,
activeTimePeriod: TimePeriod,
locale: string
): [TickFormatter<NumberValue>, (v: number) => string, number[]] {
switch (activeTimePeriod) {
case TimePeriod.hour:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.day:
return [hourFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.week:
return [weekFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp, 6)]
case TimePeriod.month:
return [monthDayFormatter(locale), dayHourFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.year:
return [monthFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
case TimePeriod.all:
return [monthYearFormatter(locale), monthYearDayFormatter(locale), getTicks(startTimestamp, endTimestamp)]
}
}
const margin = { top: 86, bottom: 32, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
interface PriceChartProps {
width: number
@ -69,37 +140,39 @@ interface PriceChartProps {
}
export function PriceChart({ width, height }: PriceChartProps) {
const margin = { top: 80, bottom: 20, crosshair: 72 }
// defining inner measurements
const innerHeight = height - margin.top - margin.bottom
const [activeTimePeriod, setTimePeriod] = useState(TimePeriod.hour)
const locale = useActiveLocale()
const theme = useTheme()
const pricePoints = data.priceHistory
/* TODO: Implement API calls & cache to use here */
const pricePoints = data[activeTimePeriod]
const startingPrice = pricePoints[0]
const endingPrice = pricePoints[pricePoints.length - 1]
const initialState = { pricePoint: endingPrice, xCoordinate: null }
const [selected, setSelected] = useState<{ pricePoint: PricePoint; xCoordinate: number | null }>(initialState)
const graphWidth = width + crosshairDateOverhang
const graphHeight = height - timeOptionsHeight
const graphInnerHeight = graphHeight - margin.top - margin.bottom
// Defining scales
// x scale
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, width])
// y scale
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([innerHeight, 0])
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([graphInnerHeight, 0])
const handleHover = useCallback(
(event: Element | EventType) => {
const { x } = localPoint(event) || { x: 0 }
const x0 = timeScale.invert(x) // get timestamp from the scale
const index = bisect(
data.priceHistory.map((x) => x.timestamp),
pricePoints.map((x) => x.timestamp),
x0,
1
)
const d0 = data.priceHistory[index - 1]
const d1 = data.priceHistory[index]
const d0 = pricePoints[index - 1]
const d1 = pricePoints[index]
let pricePoint = d0
const hasPreviousData = d1 && d1.timestamp
@ -107,12 +180,20 @@ export function PriceChart({ width, height }: PriceChartProps) {
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
}
setSelected({ pricePoint, xCoordinate: x })
setSelected({ pricePoint, xCoordinate: timeScale(pricePoint.timestamp) })
},
[timeScale]
[timeScale, pricePoints]
)
const [tickFormatter, crosshairDateFormatter, ticks] = tickFormat(
startingPrice.timestamp,
endingPrice.timestamp,
activeTimePeriod,
locale
)
const [delta, arrow] = getDelta(startingPrice.value, selected.pricePoint.value)
const crosshairEdgeMax = width * 0.97
const crosshairAtEdge = !!selected.xCoordinate && selected.xCoordinate > crosshairEdgeMax
return (
<ChartWrapper>
@ -123,13 +204,38 @@ export function PriceChart({ width, height }: PriceChartProps) {
<ArrowCell>{arrow}</ArrowCell>
</DeltaContainer>
</ChartHeader>
<svg width={width} height={height}>
{selected.xCoordinate && (
<svg width={graphWidth} height={graphHeight}>
<AxisBottom
scale={timeScale}
stroke={theme.backgroundOutline}
tickFormat={tickFormatter}
tickStroke={theme.backgroundOutline}
tickLength={4}
tickTransform={'translate(0 -5)'}
tickValues={ticks}
top={graphHeight - 1}
tickLabelProps={() => ({
fill: theme.textSecondary,
fontSize: 12,
textAnchor: 'middle',
transform: 'translate(0 -24)',
})}
/>
{selected.xCoordinate !== null && (
<g>
<text
x={selected.xCoordinate + (crosshairAtEdge ? -4 : 4)}
y={margin.crosshair + 10}
textAnchor={crosshairAtEdge ? 'end' : 'start'}
fontSize={12}
fill={theme.textSecondary}
>
{crosshairDateFormatter(selected.pricePoint.timestamp)}
</text>
<Line
from={{ x: selected.xCoordinate, y: margin.crosshair }}
to={{ x: selected.xCoordinate, y: height }}
stroke={'#99A1BD3D'}
to={{ x: selected.xCoordinate, y: graphHeight }}
stroke={theme.backgroundOutline}
strokeWidth={1}
pointerEvents="none"
strokeDasharray="4,4"
@ -138,14 +244,15 @@ export function PriceChart({ width, height }: PriceChartProps) {
)}
<Group top={margin.top}>
<LinePath
curve={radius(1)}
/* 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={data.priceHistory}
data={pricePoints}
x={(d: PricePoint) => timeScale(d.timestamp) ?? 0}
y={(d: PricePoint) => rdScale(d.value) ?? 0}
/>
{selected.xCoordinate && (
{selected.xCoordinate !== null && (
<g>
<GlyphCircle
left={selected.xCoordinate}
@ -162,7 +269,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
x={0}
y={0}
width={width}
height={height}
height={graphHeight}
fill={'transparent'}
onTouchStart={handleHover}
onTouchMove={handleHover}
@ -170,6 +277,13 @@ export function PriceChart({ width, height }: PriceChartProps) {
onMouseLeave={() => setSelected(initialState)}
/>
</svg>
<TimeOptionsContainer>
{TIME_DISPLAYS.map(([value, display]) => (
<TimeButton key={display} active={activeTimePeriod === value} onClick={() => setTimePeriod(value)}>
{display}
</TimeButton>
))}
</TimeOptionsContainer>
</ChartWrapper>
)
}

File diff suppressed because it is too large Load Diff

@ -13,7 +13,6 @@ import {
Stat,
StatPair,
StatsSection,
TimeOptionsContainer,
TokenInfoContainer,
TokenNameCell,
TopArea,
@ -113,9 +112,7 @@ export default function LoadingTokenDetail() {
</ChartAnimation>
</ChartWrapper>
</ChartContainer>
<TimeOptionsContainer>
<Space heightSize={32} />
</TimeOptionsContainer>
<Space heightSize={32} />
</ChartHeader>
<AboutSection>
<AboutHeader>

@ -7,7 +7,6 @@ import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { TimePeriod } from 'hooks/useTopTokens'
import { useAtomValue } from 'jotai/utils'
import { darken } from 'polished'
import { useCallback } from 'react'
@ -22,15 +21,6 @@ import { ClickFavorited } from '../TokenTable/TokenRow'
import Resource from './Resource'
import ShareButton from './ShareButton'
const TIME_DISPLAYS: Record<TimePeriod, string> = {
[TimePeriod.hour]: '1H',
[TimePeriod.day]: '1D',
[TimePeriod.week]: '1W',
[TimePeriod.month]: '1M',
[TimePeriod.year]: '1Y',
}
const TIME_PERIODS = [TimePeriod.hour, TimePeriod.day, TimePeriod.week, TimePeriod.month, TimePeriod.year]
export const AboutSection = styled.div`
display: flex;
flex-direction: column;
@ -89,10 +79,8 @@ const Contract = styled.div`
`
export const ChartContainer = styled.div`
display: flex;
height: 404px;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
height: 436px;
align-items: center;
overflow: hidden;
`
export const Stat = styled.div`
display: flex;
@ -117,21 +105,6 @@ export const StatPair = styled.div`
flex: 1;
flex-wrap: wrap;
`
const TimeButton = styled.button<{ active: boolean }>`
background-color: ${({ theme, active }) => (active ? theme.accentActive : 'transparent')};
font-size: 14px;
width: 36px;
height: 36px;
border-radius: 12px;
border: none;
cursor: pointer;
color: ${({ theme }) => theme.textPrimary};
`
export const TimeOptionsContainer = styled.div`
display: flex;
justify-content: flex-end;
gap: 4px;
`
export const TokenNameCell = styled.div`
display: flex;
gap: 8px;
@ -154,7 +127,6 @@ const TokenSymbol = styled.span`
`
export const TopArea = styled.div`
max-width: 832px;
overflow: hidden;
`
export const ResourcesContainer = styled.div`
display: flex;
@ -186,7 +158,6 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
const token = useToken(address)
const currency = useCurrency(address)
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
const [activeTimePeriod, setTimePeriod] = useState(TimePeriod.hour)
const isFavorited = favoriteTokens.includes(address)
const toggleFavorite = useToggleFavorite(address)
const warning = checkWarning(address)
@ -246,17 +217,6 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<ChartContainer>
<ParentSize>{({ width, height }) => <PriceChart width={width} height={height} />}</ParentSize>
</ChartContainer>
<TimeOptionsContainer>
{TIME_PERIODS.map((timePeriod) => (
<TimeButton
key={timePeriod}
active={activeTimePeriod === timePeriod}
onClick={() => setTimePeriod(timePeriod)}
>
{TIME_DISPLAYS[timePeriod]}
</TimeButton>
))}
</TimeOptionsContainer>
</ChartHeader>
<AboutSection>
<AboutHeader>
@ -274,7 +234,8 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
Market cap<StatPrice>${tokenMarketCap}</StatPrice>
</Stat>
<Stat>
{TIME_DISPLAYS[activeTimePeriod]} volume
{/* TODO: connect to chart's selected time */}
1h volume
<StatPrice>${tokenVolume}</StatPrice>
</Stat>
</StatPair>

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#99A1BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>

After

Width:  |  Height:  |  Size: 303 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#99A1BD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 294 B

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.24453 18.0887C3.24331 19.0467 3.47372 19.7558 3.93576 20.2158C4.39658 20.6771 5.09574 20.904 6.03326 20.8967H8.11975C8.20693 20.8934 8.29386 20.9079 8.37521 20.9395C8.45656 20.9711 8.53062 21.019 8.5928 21.0802L10.0779 22.5484C10.7527 23.2226 11.4139 23.5578 12.0617 23.5541C12.7096 23.5504 13.3709 23.2152 14.0456 22.5484L15.5124 21.0802C15.5767 21.0182 15.6529 20.97 15.7365 20.9385C15.82 20.9069 15.9091 20.8927 15.9982 20.8967H18.0719C19.0192 20.8979 19.7251 20.6673 20.1896 20.2048C20.6541 19.7423 20.8864 19.0333 20.8864 18.0777V16.0021C20.8816 15.8222 20.9474 15.6476 21.0697 15.5157L22.5365 14.0475C23.2198 13.3758 23.559 12.7145 23.5541 12.0636C23.5492 11.4127 23.21 10.7508 22.5365 10.0779L21.0697 8.6097C20.9471 8.47802 20.8812 8.30329 20.8864 8.12336V6.04769C20.8851 5.09092 20.6547 4.3819 20.1951 3.92064C19.7355 3.45939 19.0278 3.22875 18.0719 3.22875H15.9982C15.9091 3.23242 15.8201 3.21807 15.7366 3.18653C15.6532 3.155 15.5769 3.10694 15.5124 3.04523L14.0456 1.57703C13.3709 0.902883 12.7096 0.567648 12.0617 0.571319C11.4139 0.574989 10.7527 0.910224 10.0779 1.57703L8.5928 3.04523C8.53043 3.10622 8.45638 3.15393 8.37508 3.18547C8.29377 3.21701 8.20689 3.23173 8.11975 3.22875H6.03326C5.08718 3.22998 4.38373 3.45877 3.92291 3.91513C3.4621 4.3715 3.23168 5.08235 3.23168 6.04769V8.12887C3.23683 8.3088 3.17096 8.48352 3.04833 8.6152L1.58154 10.0834C0.908042 10.7551 0.571289 11.417 0.571289 12.0691C0.571289 12.7213 0.912332 13.3844 1.59439 14.0585L3.06118 15.5267C3.18346 15.6586 3.24928 15.8332 3.24453 16.0131V18.0887Z" fill="#4C82FB"/>
<path d="M11.996 15.9909C11.7795 16.3208 11.4599 16.5064 11.0887 16.5064C10.7072 16.5064 10.4083 16.3517 10.1299 15.9909L7.69677 13.0216C7.5215 12.8051 7.42871 12.5783 7.42871 12.3309C7.42871 11.8154 7.82049 11.4133 8.32567 11.4133C8.63497 11.4133 8.8824 11.5267 9.12984 11.8463L11.0475 14.2897L15.1199 7.75329C15.3364 7.40275 15.6147 7.23779 15.924 7.23779C16.4086 7.23779 16.8622 7.57802 16.8622 8.0832C16.8622 8.32033 16.7385 8.56777 16.6045 8.78427L11.996 15.9909Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@ -6,6 +6,7 @@ export enum TimePeriod {
week = 'week',
month = 'month',
year = 'year',
all = 'all',
}
export type TokenData = {
@ -38,6 +39,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 16_800_000,
[TimePeriod.month]: 58_920_000,
[TimePeriod.year]: 690_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x0cec1a9154ff802e7934fc916ed7ca50bde6844e': {
@ -53,6 +55,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x6B175474E89094C44Da98b954EedeAC495271d0F': {
@ -68,6 +71,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0xdac17f958d2ee523a2206206994597c13d831ec7': {
@ -83,6 +87,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0xf629cbd94d3791c9250152bd8dfbdf380e2a3b9c': {
@ -98,6 +103,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce': {
@ -113,6 +119,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x8a2279d4a90b6fe1c4b30fa660cc9f926797baa2': {
@ -128,6 +135,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x84ca8bc7997272c7cfb4d0cd3d55cd942b3c9419': {
@ -143,6 +151,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x3845badAde8e6dFF049820680d1F14bD3903a5d0': {
@ -158,6 +167,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x4c19596f5aaff459fa38b0f7ed92f11ae6543784': {
@ -173,6 +183,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x71Ab77b7dbB4fa7e017BC15090b2163221420282': {
@ -188,6 +199,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0xccc8cb5229b0ac8069c51fd58367fd1e622afd97': {
@ -203,6 +215,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x03be5c903c727ee2c8c4e9bc0acc860cca4715e2': {
@ -218,6 +231,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x4674672bcddda2ea5300f5207e1158185c944bc0': {
@ -233,6 +247,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0xdf801468a808a32656d2ed2d2d80b72a129739f4': {
@ -248,6 +263,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0xaDB2437e6F65682B85F814fBc12FeC0508A7B1D0': {
@ -263,6 +279,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
'0x1796ae0b0fa4862485106a0de9b654eFE301D0b2': {
@ -278,6 +295,7 @@ const FAKE_TOP_TOKENS_RESULT = {
[TimePeriod.week]: 800_000,
[TimePeriod.month]: 4_920_000,
[TimePeriod.year]: 100_920_000,
[TimePeriod.all]: 690_920_000,
},
},
}

@ -0,0 +1,48 @@
import { NumberValue } from 'd3'
const createTimeFormatter = (timestamp: NumberValue, locale: string, options: Intl.DateTimeFormatOptions) =>
new Date(timestamp.valueOf() * 1000).toLocaleTimeString(locale, options)
export const hourFormatter = (locale: string) => (timestamp: NumberValue) =>
createTimeFormatter(timestamp, locale, {
hour: 'numeric',
minute: 'numeric',
hour12: true,
})
export const dayHourFormatter = (locale: string) => (timestamp: NumberValue) =>
createTimeFormatter(timestamp, locale, {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
})
const createDateFormatter = (timestamp: NumberValue, locale: string, options: Intl.DateTimeFormatOptions) =>
new Date(timestamp.valueOf() * 1000).toLocaleDateString(locale, options)
export const monthDayFormatter = (locale: string) => (timestamp: NumberValue) =>
createDateFormatter(timestamp, locale, {
month: 'long',
day: 'numeric',
})
export const monthYearFormatter = (locale: string) => (timestamp: NumberValue) =>
createDateFormatter(timestamp, locale, {
month: 'long',
year: 'numeric',
})
export const monthYearDayFormatter = (locale: string) => (timestamp: NumberValue) =>
createDateFormatter(timestamp, locale, {
month: 'short',
year: 'numeric',
day: 'numeric',
})
export const monthFormatter = (locale: string) => (timestamp: NumberValue) =>
createDateFormatter(timestamp, locale, { month: 'long' })
export const weekFormatter = (locale: string) => (timestamp: NumberValue) =>
createDateFormatter(timestamp, locale, { weekday: 'long' })