diff --git a/apps/insights/src/components/LivePrices/index.tsx b/apps/insights/src/components/LivePrices/index.tsx
index 0bf30b37e7..a590679047 100644
--- a/apps/insights/src/components/LivePrices/index.tsx
+++ b/apps/insights/src/components/LivePrices/index.tsx
@@ -2,6 +2,7 @@
import { PlusMinus } from "@phosphor-icons/react/dist/ssr/PlusMinus";
import type { PriceData, PriceComponent } from "@pythnetwork/client";
+import { PriceStatus } from "@pythnetwork/client";
import { Skeleton } from "@pythnetwork/component-library/Skeleton";
import type { ReactNode } from "react";
import { useMemo } from "react";
@@ -39,9 +40,13 @@ const LiveAggregatePrice = ({
cluster: Cluster;
}) => {
const { prev, current } = useLivePriceData(cluster, feedKey);
- return (
-
- );
+ if (current === undefined) {
+ return ;
+ } else if (current.status === PriceStatus.Trading) {
+ return ;
+ } else {
+ return ;
+ }
};
const LiveComponentPrice = ({
@@ -101,7 +106,16 @@ const LiveAggregateConfidence = ({
cluster: Cluster;
}) => {
const { current } = useLivePriceData(cluster, feedKey);
- return ;
+ return (
+
+ );
};
const LiveComponentConfidence = ({
@@ -153,7 +167,13 @@ export const LiveLastUpdated = ({
});
const formattedTimestamp = useMemo(() => {
if (current) {
- const timestamp = new Date(Number(current.timestamp * 1000n));
+ const timestamp = new Date(
+ Number(
+ (current.status === PriceStatus.Trading
+ ? current.timestamp
+ : current.previousTimestamp) * 1000n,
+ ),
+ );
return isToday(timestamp)
? formatterWithoutDate.format(timestamp)
: formatterWithDate.format(timestamp);
diff --git a/apps/insights/src/components/PriceFeed/Chart/chart.tsx b/apps/insights/src/components/PriceFeed/Chart/chart.tsx
index 3e85671b86..e807c99d0a 100644
--- a/apps/insights/src/components/PriceFeed/Chart/chart.tsx
+++ b/apps/insights/src/components/PriceFeed/Chart/chart.tsx
@@ -1,5 +1,6 @@
"use client";
+import { PriceStatus } from "@pythnetwork/client";
import { useLogger } from "@pythnetwork/component-library/useLogger";
import { useResizeObserver, useMountEffect } from "@react-hookz/web";
import {
@@ -12,7 +13,9 @@ import type {
IChartApi,
ISeriesApi,
LineData,
+ Time,
UTCTimestamp,
+ WhitespaceData,
} from "lightweight-charts";
import {
AreaSeries,
@@ -68,6 +71,12 @@ const useChartElem = (symbol: string, feedId: string) => {
const isBackfilling = useRef(false);
const priceFormatter = usePriceFormatter();
const abortControllerRef = useRef(undefined);
+ // Lightweight charts has [a
+ // bug](https://github.com/tradingview/lightweight-charts/issues/1649) where
+ // it does not properly return whitespace data back to us. So we use this ref
+ // to manually keep track of whitespace data so we can merge it at the
+ // appropriate times.
+ const whitespaceData = useRef>(new Set());
const { current: livePriceData } = useLivePriceData(Cluster.Pythnet, feedId);
@@ -81,8 +90,6 @@ const useChartElem = (symbol: string, feedId: string) => {
return;
}
- // Update last data point
- const { price, confidence } = livePriceData.aggregate;
const timestampMs = startOfResolution(
new Date(Number(livePriceData.timestamp) * 1000),
resolution,
@@ -90,35 +97,51 @@ const useChartElem = (symbol: string, feedId: string) => {
const time = (timestampMs / 1000) as UTCTimestamp;
- const priceData: LineData = { time, value: price };
- const confidenceHighData: LineData = { time, value: price + confidence };
- const confidenceLowData: LineData = { time, value: price - confidence };
+ if (livePriceData.status === PriceStatus.Trading) {
+ // Update last data point
+ const { price, confidence } = livePriceData.aggregate;
- const lastDataPoint = chartRef.current.price.data().at(-1);
+ const priceData: LineData = { time, value: price };
+ const confidenceHighData: LineData = { time, value: price + confidence };
+ const confidenceLowData: LineData = { time, value: price - confidence };
- if (lastDataPoint && lastDataPoint.time > priceData.time) {
- return;
- }
+ const lastDataPoint = mergeData(chartRef.current.price.data(), [
+ ...whitespaceData.current,
+ ]).at(-1);
+
+ if (lastDataPoint && lastDataPoint.time > priceData.time) {
+ return;
+ }
- chartRef.current.confidenceHigh.update(confidenceHighData);
- chartRef.current.confidenceLow.update(confidenceLowData);
- chartRef.current.price.update(priceData);
+ chartRef.current.confidenceHigh.update(confidenceHighData);
+ chartRef.current.confidenceLow.update(confidenceLowData);
+ chartRef.current.price.update(priceData);
+ } else {
+ chartRef.current.price.update({ time });
+ chartRef.current.confidenceHigh.update({ time });
+ chartRef.current.confidenceLow.update({ time });
+ whitespaceData.current.add({ time });
+ }
}, [livePriceData, resolution]);
function maybeResetVisibleRange() {
if (chartRef.current === undefined || didResetVisibleRange.current) {
return;
}
- const data = chartRef.current.price.data();
- const first = data.at(0);
- const last = data.at(-1);
- if (!first || !last) {
- return;
+ const data = mergeData(chartRef.current.price.data(), [
+ ...whitespaceData.current,
+ ]);
+ if (data.length > 0) {
+ const first = data.at(0);
+ const last = data.at(-1);
+ if (!first || !last) {
+ return;
+ }
+ chartRef.current.chart
+ .timeScale()
+ .setVisibleRange({ from: first.time, to: last.time });
+ didResetVisibleRange.current = true;
}
- chartRef.current.chart
- .timeScale()
- .setVisibleRange({ from: first.time, to: last.time });
- didResetVisibleRange.current = true;
}
const fetchHistoricalData = useCallback(
@@ -159,37 +182,49 @@ const useChartElem = (symbol: string, feedId: string) => {
// Get the current historical price data
// Note that .data() returns (WhitespaceData | LineData)[], hence the type cast.
// We never populate the chart with WhitespaceData, so the type cast is safe.
- const currentHistoricalPriceData =
- chartRef.current.price.data() as LineData[];
+ const currentHistoricalPriceData = chartRef.current.price.data();
const currentHistoricalConfidenceHighData =
- chartRef.current.confidenceHigh.data() as LineData[];
+ chartRef.current.confidenceHigh.data();
const currentHistoricalConfidenceLowData =
- chartRef.current.confidenceLow.data() as LineData[];
+ chartRef.current.confidenceLow.data();
const newHistoricalPriceData = data.map((d) => ({
time: d.time,
- value: d.price,
+ ...(d.status === PriceStatus.Trading && {
+ value: d.price,
+ }),
}));
const newHistoricalConfidenceHighData = data.map((d) => ({
time: d.time,
- value: d.price + d.confidence,
+ ...(d.status === PriceStatus.Trading && {
+ value: d.price + d.confidence,
+ }),
}));
const newHistoricalConfidenceLowData = data.map((d) => ({
time: d.time,
- value: d.price - d.confidence,
+ ...(d.status === PriceStatus.Trading && {
+ value: d.price - d.confidence,
+ }),
}));
// Combine the current and new historical price data
+ const whitespaceDataAsArray = [...whitespaceData.current];
const mergedPriceData = mergeData(
- currentHistoricalPriceData,
+ mergeData(currentHistoricalPriceData, whitespaceDataAsArray),
newHistoricalPriceData,
);
const mergedConfidenceHighData = mergeData(
- currentHistoricalConfidenceHighData,
+ mergeData(
+ currentHistoricalConfidenceHighData,
+ whitespaceDataAsArray,
+ ),
newHistoricalConfidenceHighData,
);
const mergedConfidenceLowData = mergeData(
- currentHistoricalConfidenceLowData,
+ mergeData(
+ currentHistoricalConfidenceLowData,
+ whitespaceDataAsArray,
+ ),
newHistoricalConfidenceLowData,
);
@@ -199,6 +234,12 @@ const useChartElem = (symbol: string, feedId: string) => {
chartRef.current.confidenceLow.setData(mergedConfidenceLowData);
maybeResetVisibleRange();
didLoadInitialData.current = true;
+
+ for (const point of data) {
+ if (point.status !== PriceStatus.Trading) {
+ whitespaceData.current.add({ time: point.time });
+ }
+ }
})
.catch((error: unknown) => {
if (error instanceof Error && error.name === "AbortError") {
@@ -252,7 +293,9 @@ const useChartElem = (symbol: string, feedId: string) => {
return;
}
const { from, to } = range;
- const first = chartRef.current?.price.data().at(0);
+ const first = mergeData(chartRef.current?.price.data() ?? [], [
+ ...whitespaceData.current,
+ ]).at(0);
if (!from || !to || !first) {
return;
@@ -344,11 +387,13 @@ const historicalDataSchema = z.array(
timestamp: z.number(),
price: z.number(),
confidence: z.number(),
+ status: z.nativeEnum(PriceStatus),
})
.transform((d) => ({
time: Number(d.timestamp) as UTCTimestamp,
price: d.price,
confidence: d.confidence,
+ status: d.status,
})),
);
const priceFormat = {
@@ -451,18 +496,27 @@ const getColors = (container: HTMLDivElement, resolvedTheme: string) => {
/**
* Merge (and sort) two arrays of line data, deduplicating by time
*/
-export function mergeData(as: LineData[], bs: LineData[]) {
- const unique = new Map();
+export function mergeData(
+ as: readonly (LineData | WhitespaceData)[],
+ bs: (LineData | WhitespaceData)[],
+) {
+ const unique = new Map