feat: adding timeline to explore price chart (#4292)
* added timeline & time options selector functionality to price chart
This commit is contained in:
parent
bb2e67fc5a
commit
77c4e74fc6
@ -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>
|
||||
|
1
src/components/Explore/TokenTable/search.svg
Normal file
1
src/components/Explore/TokenTable/search.svg
Normal file
@ -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 |
1
src/components/Explore/TokenTable/x.svg
Normal file
1
src/components/Explore/TokenTable/x.svg
Normal file
@ -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 |
4
src/components/TokenSafety/verified.svg
Normal file
4
src/components/TokenSafety/verified.svg
Normal file
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
48
src/utils/formatChartTimes.ts
Normal file
48
src/utils/formatChartTimes.ts
Normal file
@ -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' })
|
Loading…
Reference in New Issue
Block a user