11"use client" ;
22
3+ import { PriceStatus } from "@pythnetwork/client" ;
34import { useLogger } from "@pythnetwork/component-library/useLogger" ;
45import { useResizeObserver , useMountEffect } from "@react-hookz/web" ;
56import {
@@ -12,7 +13,9 @@ import type {
1213 IChartApi ,
1314 ISeriesApi ,
1415 LineData ,
16+ Time ,
1517 UTCTimestamp ,
18+ WhitespaceData ,
1619} from "lightweight-charts" ;
1720import {
1821 AreaSeries ,
@@ -68,6 +71,12 @@ const useChartElem = (symbol: string, feedId: string) => {
6871 const isBackfilling = useRef ( false ) ;
6972 const priceFormatter = usePriceFormatter ( ) ;
7073 const abortControllerRef = useRef < AbortController | undefined > ( undefined ) ;
74+ // Lightweight charts has [a
75+ // bug](https://github.com/tradingview/lightweight-charts/issues/1649) where
76+ // it does not properly return whitespace data back to us. So we use this ref
77+ // to manually keep track of whitespace data so we can merge it at the
78+ // appropriate times.
79+ const whitespaceData = useRef < Set < WhitespaceData > > ( new Set ( ) ) ;
7180
7281 const { current : livePriceData } = useLivePriceData ( Cluster . Pythnet , feedId ) ;
7382
@@ -81,44 +90,58 @@ const useChartElem = (symbol: string, feedId: string) => {
8190 return ;
8291 }
8392
84- // Update last data point
85- const { price, confidence } = livePriceData . aggregate ;
8693 const timestampMs = startOfResolution (
8794 new Date ( Number ( livePriceData . timestamp ) * 1000 ) ,
8895 resolution ,
8996 ) ;
9097
9198 const time = ( timestampMs / 1000 ) as UTCTimestamp ;
9299
93- const priceData : LineData = { time , value : price } ;
94- const confidenceHighData : LineData = { time , value : price + confidence } ;
95- const confidenceLowData : LineData = { time , value : price - confidence } ;
100+ if ( livePriceData . status === PriceStatus . Trading ) {
101+ // Update last data point
102+ const { price , confidence } = livePriceData . aggregate ;
96103
97- const lastDataPoint = chartRef . current . price . data ( ) . at ( - 1 ) ;
104+ const priceData : LineData = { time, value : price } ;
105+ const confidenceHighData : LineData = { time, value : price + confidence } ;
106+ const confidenceLowData : LineData = { time, value : price - confidence } ;
98107
99- if ( lastDataPoint && lastDataPoint . time > priceData . time ) {
100- return ;
101- }
108+ const lastDataPoint = mergeData ( chartRef . current . price . data ( ) , [
109+ ...whitespaceData . current ,
110+ ] ) . at ( - 1 ) ;
111+
112+ if ( lastDataPoint && lastDataPoint . time > priceData . time ) {
113+ return ;
114+ }
102115
103- chartRef . current . confidenceHigh . update ( confidenceHighData ) ;
104- chartRef . current . confidenceLow . update ( confidenceLowData ) ;
105- chartRef . current . price . update ( priceData ) ;
116+ chartRef . current . confidenceHigh . update ( confidenceHighData ) ;
117+ chartRef . current . confidenceLow . update ( confidenceLowData ) ;
118+ chartRef . current . price . update ( priceData ) ;
119+ } else {
120+ chartRef . current . price . update ( { time } ) ;
121+ chartRef . current . confidenceHigh . update ( { time } ) ;
122+ chartRef . current . confidenceLow . update ( { time } ) ;
123+ whitespaceData . current . add ( { time } ) ;
124+ }
106125 } , [ livePriceData , resolution ] ) ;
107126
108127 function maybeResetVisibleRange ( ) {
109128 if ( chartRef . current === undefined || didResetVisibleRange . current ) {
110129 return ;
111130 }
112- const data = chartRef . current . price . data ( ) ;
113- const first = data . at ( 0 ) ;
114- const last = data . at ( - 1 ) ;
115- if ( ! first || ! last ) {
116- return ;
131+ const data = mergeData ( chartRef . current . price . data ( ) , [
132+ ...whitespaceData . current ,
133+ ] ) ;
134+ if ( data . length > 0 ) {
135+ const first = data . at ( 0 ) ;
136+ const last = data . at ( - 1 ) ;
137+ if ( ! first || ! last ) {
138+ return ;
139+ }
140+ chartRef . current . chart
141+ . timeScale ( )
142+ . setVisibleRange ( { from : first . time , to : last . time } ) ;
143+ didResetVisibleRange . current = true ;
117144 }
118- chartRef . current . chart
119- . timeScale ( )
120- . setVisibleRange ( { from : first . time , to : last . time } ) ;
121- didResetVisibleRange . current = true ;
122145 }
123146
124147 const fetchHistoricalData = useCallback (
@@ -159,37 +182,49 @@ const useChartElem = (symbol: string, feedId: string) => {
159182 // Get the current historical price data
160183 // Note that .data() returns (WhitespaceData | LineData)[], hence the type cast.
161184 // We never populate the chart with WhitespaceData, so the type cast is safe.
162- const currentHistoricalPriceData =
163- chartRef . current . price . data ( ) as LineData [ ] ;
185+ const currentHistoricalPriceData = chartRef . current . price . data ( ) ;
164186 const currentHistoricalConfidenceHighData =
165- chartRef . current . confidenceHigh . data ( ) as LineData [ ] ;
187+ chartRef . current . confidenceHigh . data ( ) ;
166188 const currentHistoricalConfidenceLowData =
167- chartRef . current . confidenceLow . data ( ) as LineData [ ] ;
189+ chartRef . current . confidenceLow . data ( ) ;
168190
169191 const newHistoricalPriceData = data . map ( ( d ) => ( {
170192 time : d . time ,
171- value : d . price ,
193+ ...( d . status === PriceStatus . Trading && {
194+ value : d . price ,
195+ } ) ,
172196 } ) ) ;
173197 const newHistoricalConfidenceHighData = data . map ( ( d ) => ( {
174198 time : d . time ,
175- value : d . price + d . confidence ,
199+ ...( d . status === PriceStatus . Trading && {
200+ value : d . price + d . confidence ,
201+ } ) ,
176202 } ) ) ;
177203 const newHistoricalConfidenceLowData = data . map ( ( d ) => ( {
178204 time : d . time ,
179- value : d . price - d . confidence ,
205+ ...( d . status === PriceStatus . Trading && {
206+ value : d . price - d . confidence ,
207+ } ) ,
180208 } ) ) ;
181209
182210 // Combine the current and new historical price data
211+ const whitespaceDataAsArray = [ ...whitespaceData . current ] ;
183212 const mergedPriceData = mergeData (
184- currentHistoricalPriceData ,
213+ mergeData ( currentHistoricalPriceData , whitespaceDataAsArray ) ,
185214 newHistoricalPriceData ,
186215 ) ;
187216 const mergedConfidenceHighData = mergeData (
188- currentHistoricalConfidenceHighData ,
217+ mergeData (
218+ currentHistoricalConfidenceHighData ,
219+ whitespaceDataAsArray ,
220+ ) ,
189221 newHistoricalConfidenceHighData ,
190222 ) ;
191223 const mergedConfidenceLowData = mergeData (
192- currentHistoricalConfidenceLowData ,
224+ mergeData (
225+ currentHistoricalConfidenceLowData ,
226+ whitespaceDataAsArray ,
227+ ) ,
193228 newHistoricalConfidenceLowData ,
194229 ) ;
195230
@@ -199,6 +234,12 @@ const useChartElem = (symbol: string, feedId: string) => {
199234 chartRef . current . confidenceLow . setData ( mergedConfidenceLowData ) ;
200235 maybeResetVisibleRange ( ) ;
201236 didLoadInitialData . current = true ;
237+
238+ for ( const point of data ) {
239+ if ( point . status !== PriceStatus . Trading ) {
240+ whitespaceData . current . add ( { time : point . time } ) ;
241+ }
242+ }
202243 } )
203244 . catch ( ( error : unknown ) => {
204245 if ( error instanceof Error && error . name === "AbortError" ) {
@@ -252,7 +293,9 @@ const useChartElem = (symbol: string, feedId: string) => {
252293 return ;
253294 }
254295 const { from, to } = range ;
255- const first = chartRef . current ?. price . data ( ) . at ( 0 ) ;
296+ const first = mergeData ( chartRef . current ?. price . data ( ) ?? [ ] , [
297+ ...whitespaceData . current ,
298+ ] ) . at ( 0 ) ;
256299
257300 if ( ! from || ! to || ! first ) {
258301 return ;
@@ -344,11 +387,13 @@ const historicalDataSchema = z.array(
344387 timestamp : z . number ( ) ,
345388 price : z . number ( ) ,
346389 confidence : z . number ( ) ,
390+ status : z . nativeEnum ( PriceStatus ) ,
347391 } )
348392 . transform ( ( d ) => ( {
349393 time : Number ( d . timestamp ) as UTCTimestamp ,
350394 price : d . price ,
351395 confidence : d . confidence ,
396+ status : d . status ,
352397 } ) ) ,
353398) ;
354399const priceFormat = {
@@ -451,18 +496,27 @@ const getColors = (container: HTMLDivElement, resolvedTheme: string) => {
451496/**
452497 * Merge (and sort) two arrays of line data, deduplicating by time
453498 */
454- export function mergeData ( as : LineData [ ] , bs : LineData [ ] ) {
455- const unique = new Map < number , LineData > ( ) ;
499+ export function mergeData (
500+ as : readonly ( LineData | WhitespaceData ) [ ] ,
501+ bs : ( LineData | WhitespaceData ) [ ] ,
502+ ) {
503+ const unique = new Map < Time , LineData | WhitespaceData > ( ) ;
456504
457505 for ( const a of as ) {
458- unique . set ( a . time as number , a ) ;
506+ unique . set ( a . time , a ) ;
459507 }
460508 for ( const b of bs ) {
461- unique . set ( b . time as number , b ) ;
509+ unique . set ( b . time , b ) ;
462510 }
463- return [ ...unique . values ( ) ] . sort (
464- ( a , b ) => ( a . time as number ) - ( b . time as number ) ,
465- ) ;
511+ return [ ...unique . values ( ) ] . sort ( ( a , b ) => {
512+ if ( typeof a . time === "number" && typeof b . time === "number" ) {
513+ return a . time - b . time ;
514+ } else {
515+ throw new TypeError (
516+ "Invariant failed: unexpected time type encountered, all time values must be of type UTCTimestamp" ,
517+ ) ;
518+ }
519+ } ) ;
466520}
467521
468522/**
0 commit comments