refactor: price chart utility functions (#7266)
* refactor: price chart utility functions * fix: snapshot * fix: combine useMemos * fix: pr nits
This commit is contained in:
parent
1c3ce8fdb7
commit
6aaf0db78d
@ -1,5 +1,100 @@
|
||||
import { timeDay, timeHour, TimeInterval, timeMinute, timeMonth } from 'd3'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { bisector, ScaleLinear, timeDay, timeHour, TimeInterval, timeMinute, timeMonth } from 'd3'
|
||||
import { PricePoint, TimePeriod } from 'graphql/data/util'
|
||||
|
||||
/**
|
||||
* Returns the minimum and maximum values in the given array of PricePoints.
|
||||
*/
|
||||
export function getPriceBounds(prices: PricePoint[]): { min: number; max: number } {
|
||||
if (!prices.length) return { min: 0, max: 0 }
|
||||
|
||||
let min = prices[0].value
|
||||
let max = prices[0].value
|
||||
|
||||
for (const pricePoint of prices) {
|
||||
if (pricePoint.value < min) {
|
||||
min = pricePoint.value
|
||||
}
|
||||
if (pricePoint.value > max) {
|
||||
max = pricePoint.value
|
||||
}
|
||||
}
|
||||
|
||||
return { min, max }
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans an array of PricePoints by removing zero values and marking gaps in data as blanks.
|
||||
*
|
||||
* @param prices - The original array of PricePoints
|
||||
* @returns An object containing two arrays: fixedChart and blanks
|
||||
*/
|
||||
export function cleanPricePoints(prices: PricePoint[]) {
|
||||
const validPrices: PricePoint[] = [] // PricePoint array with 0 values removed
|
||||
const blanks: [PricePoint, PricePoint][] = [] // PricePoint pairs that represent blank spaces in the chart
|
||||
let lastValidPrice: PricePoint | undefined
|
||||
|
||||
prices.forEach((pricePoint, index) => {
|
||||
if (pricePoint.value !== 0) {
|
||||
const isFirstValidPrice = validPrices.length === 0
|
||||
|
||||
if (isFirstValidPrice && index !== 0) {
|
||||
const blankStart = { timestamp: prices[0].timestamp, value: pricePoint.value }
|
||||
blanks.push([blankStart, pricePoint])
|
||||
}
|
||||
|
||||
lastValidPrice = pricePoint
|
||||
validPrices.push(pricePoint)
|
||||
}
|
||||
})
|
||||
|
||||
if (lastValidPrice) {
|
||||
const isLastPriceInvalid = lastValidPrice !== prices[prices.length - 1]
|
||||
|
||||
if (isLastPriceInvalid) {
|
||||
const blankEnd = { timestamp: prices[prices.length - 1].timestamp, value: lastValidPrice.value }
|
||||
blanks.push([lastValidPrice, blankEnd])
|
||||
}
|
||||
}
|
||||
|
||||
return { prices: validPrices, blanks, lastValidPrice }
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the nearest PricePoint to a given x-coordinate based on a linear time scale.
|
||||
*
|
||||
* @param x - The x-coordinate to find the nearest PricePoint for.
|
||||
* @param prices - An array of PricePoints, assumed to be sorted by timestamp.
|
||||
* @param timeScale - A D3 ScaleLinear instance for time scaling.
|
||||
* @returns The nearest PricePoint to the given x-coordinate.
|
||||
*/
|
||||
export function getNearestPricePoint(
|
||||
x: number,
|
||||
prices: PricePoint[],
|
||||
timeScale: ScaleLinear<number, number, never>
|
||||
): PricePoint | undefined {
|
||||
// Convert the x-coordinate back to a timestamp
|
||||
const targetTimestamp = timeScale.invert(x)
|
||||
|
||||
// Use bisector for O(log N) complexity, assumes prices are sorted by timestamp
|
||||
const bisect = bisector((d: PricePoint) => d.timestamp).left
|
||||
const index = bisect(prices, targetTimestamp, 1)
|
||||
|
||||
// Get potential nearest PricePoints
|
||||
const previousPoint = prices[index - 1]
|
||||
const nextPoint = prices[index]
|
||||
|
||||
// Default to the previous point if next point doesn't exist
|
||||
if (!nextPoint) {
|
||||
return previousPoint
|
||||
}
|
||||
|
||||
// Calculate temporal distances to target timestamp
|
||||
const distanceToPrevious = Math.abs(targetTimestamp.valueOf() - previousPoint.timestamp.valueOf())
|
||||
const distanceToNext = Math.abs(nextPoint.timestamp.valueOf() - targetTimestamp.valueOf())
|
||||
|
||||
// Return the PricePoint with the smallest temporal distance to targetTimestamp
|
||||
return distanceToPrevious < distanceToNext ? previousPoint : nextPoint
|
||||
}
|
||||
|
||||
const fiveMinutes = timeMinute.every(5)
|
||||
const TIME_PERIOD_INTERVAL_TABLE: Record<TimePeriod, { interval: TimeInterval; step: number }> = {
|
||||
|
@ -5,8 +5,8 @@ import { PricePoint } from 'graphql/data/util'
|
||||
import { memo } from 'react'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
|
||||
import { getPriceBounds } from '../Tokens/TokenDetails/PriceChart'
|
||||
import LineChart from './LineChart'
|
||||
import { getPriceBounds } from './PriceChart/utils'
|
||||
|
||||
const LoadingContainer = styled.div`
|
||||
height: 100%;
|
||||
@ -48,7 +48,9 @@ function _SparklineChart({ width, height, tokenData, pricePercentChange, sparkli
|
||||
// the range of possible output values that the inputs should be transformed to (see https://www.d3indepth.com/scales/ for details)
|
||||
[0, 110]
|
||||
)
|
||||
const rdScale = scaleLinear().domain(getPriceBounds(pricePoints)).range([30, 0])
|
||||
|
||||
const { min, max } = getPriceBounds(pricePoints)
|
||||
const rdScale = scaleLinear().domain([min, max]).range([30, 0])
|
||||
const curveTension = 0.9
|
||||
|
||||
return (
|
||||
|
@ -7,9 +7,9 @@ import { Line } from '@visx/shape'
|
||||
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
|
||||
import FadedInLineChart from 'components/Charts/FadeInLineChart'
|
||||
import { getTimestampFormatter, TimestampFormatterType } from 'components/Charts/PriceChart/format'
|
||||
import { getTicks } from 'components/Charts/PriceChart/utils'
|
||||
import { cleanPricePoints, getNearestPricePoint, getPriceBounds, getTicks } from 'components/Charts/PriceChart/utils'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { bisect, curveCardinal, scaleLinear } from 'd3'
|
||||
import { curveCardinal, scaleLinear } from 'd3'
|
||||
import { PricePoint, TimePeriod } from 'graphql/data/util'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@ -23,13 +23,6 @@ import { calculateDelta, DeltaArrow, formatDelta } from './Delta'
|
||||
|
||||
const DATA_EMPTY = { value: 0, timestamp: 0 }
|
||||
|
||||
export 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]
|
||||
}
|
||||
|
||||
const ChartHeader = styled.div`
|
||||
position: absolute;
|
||||
${textFadeIn};
|
||||
@ -65,29 +58,6 @@ const OutdatedPriceContainer = styled.div`
|
||||
line-height: 44px;
|
||||
`
|
||||
|
||||
function fixChart(prices: PricePoint[] | undefined | null) {
|
||||
if (!prices) return { prices: null, blanks: [] }
|
||||
|
||||
const fixedChart: PricePoint[] = []
|
||||
const blanks: PricePoint[][] = []
|
||||
let lastValue: PricePoint | undefined = undefined
|
||||
for (let i = 0; i < prices.length; i++) {
|
||||
if (prices[i].value !== 0) {
|
||||
if (fixedChart.length === 0 && i !== 0) {
|
||||
blanks.push([{ ...prices[0], value: prices[i].value }, prices[i]])
|
||||
}
|
||||
lastValue = prices[i]
|
||||
fixedChart.push(prices[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (lastValue && lastValue !== prices[prices.length - 1]) {
|
||||
blanks.push([lastValue, { ...prices[prices.length - 1], value: lastValue.value }])
|
||||
}
|
||||
|
||||
return { prices: fixedChart, blanks }
|
||||
}
|
||||
|
||||
const margin = { top: 100, bottom: 48, crosshair: 72 }
|
||||
const timeOptionsHeight = 44
|
||||
|
||||
@ -119,7 +89,8 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
|
||||
const theme = useTheme()
|
||||
|
||||
const { prices, blanks } = useMemo(
|
||||
() => (originalPrices && originalPrices.length > 0 ? fixChart(originalPrices) : { prices: null, blanks: [] }),
|
||||
() =>
|
||||
originalPrices && originalPrices.length > 0 ? cleanPricePoints(originalPrices) : { prices: null, blanks: [] },
|
||||
[originalPrices]
|
||||
)
|
||||
|
||||
@ -180,34 +151,17 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
|
||||
[startingPrice, endingPrice, width]
|
||||
)
|
||||
// y scale
|
||||
const rdScale = useMemo(
|
||||
() =>
|
||||
scaleLinear()
|
||||
.domain(getPriceBounds(originalPrices ?? []))
|
||||
.range([graphInnerHeight, 0]),
|
||||
[originalPrices, graphInnerHeight]
|
||||
)
|
||||
const rdScale = useMemo(() => {
|
||||
const { min, max } = getPriceBounds(originalPrices ?? [])
|
||||
return scaleLinear().domain([min, max]).range([graphInnerHeight, 0])
|
||||
}, [originalPrices, graphInnerHeight])
|
||||
|
||||
const handleHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
if (!prices) return
|
||||
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scalexw
|
||||
const index = bisect(
|
||||
prices.map((x) => x.timestamp),
|
||||
x0,
|
||||
1
|
||||
)
|
||||
|
||||
const d0 = prices[index - 1]
|
||||
const d1 = prices[index]
|
||||
let pricePoint = d0
|
||||
|
||||
const hasPreviousData = d1 && d1.timestamp
|
||||
if (hasPreviousData) {
|
||||
pricePoint = x0.valueOf() - d0.timestamp.valueOf() > d1.timestamp.valueOf() - x0.valueOf() ? d1 : d0
|
||||
}
|
||||
const pricePoint = getNearestPricePoint(x, prices, timeScale)
|
||||
|
||||
if (pricePoint) {
|
||||
setCrosshair(timeScale(pricePoint.timestamp))
|
||||
|
@ -2,15 +2,7 @@
|
||||
|
||||
exports[`LoadedRow.tsx renders a row 1`] = `
|
||||
<DocumentFragment>
|
||||
.c18 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
.c9 {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 250ms ease-in;
|
||||
transition: opacity 250ms ease-in;
|
||||
@ -50,6 +42,14 @@ exports[`LoadedRow.tsx renders a row 1`] = `
|
||||
display: none;
|
||||
}
|
||||
|
||||
.c18 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c19 {
|
||||
color: #40B66B;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
|
Loading…
Reference in New Issue
Block a user