@@ -26,6 +26,7 @@ import {
2626import { useTheme } from "next-themes" ;
2727import type { RefObject } from "react" ;
2828import { useCallback , useEffect , useRef } from "react" ;
29+ import { useDateFormatter } from "react-aria" ;
2930import { z } from "zod" ;
3031
3132import styles from "./chart.module.scss" ;
@@ -68,6 +69,7 @@ const useChartElem = (symbol: string, feedId: string) => {
6869 const [ resolution ] = useChartResolution ( ) ;
6970 const chartContainerRef = useRef < HTMLDivElement | null > ( null ) ;
7071 const chartRef = useRef < ChartRefContents | undefined > ( undefined ) ;
72+ const tooltipRef = useRef < HTMLDivElement | undefined > ( undefined ) ;
7173 const isBackfilling = useRef ( false ) ;
7274 const abortControllerRef = useRef < AbortController | undefined > ( undefined ) ;
7375 // Lightweight charts has [a
@@ -264,6 +266,16 @@ const useChartElem = (symbol: string, feedId: string) => {
264266 if ( chartRef . current ) {
265267 chartRef . current . chart . remove ( ) ;
266268 }
269+ if ( tooltipRef . current ) {
270+ tooltipRef . current . remove ( ) ;
271+ }
272+
273+ const tooltip = document . createElement ( "div" ) ;
274+ tooltip . className = styles . tooltip ?? "" ;
275+ tooltip . style . display = "none" ;
276+ chartElem . append ( tooltip ) ;
277+ tooltipRef . current = tooltip ;
278+
267279 const chart = createChart ( chartElem , {
268280 layout : {
269281 attributionLogo : false ,
@@ -283,6 +295,7 @@ const useChartElem = (symbol: string, feedId: string) => {
283295 } ,
284296 localization : {
285297 priceFormatter : priceFormatter . format ,
298+ dateFormat : "dd MMM yy," ,
286299 } ,
287300 } ) ;
288301
@@ -337,6 +350,10 @@ const useChartElem = (symbol: string, feedId: string) => {
337350
338351 return ( ) => {
339352 chart . remove ( ) ;
353+ if ( tooltipRef . current ) {
354+ tooltipRef . current . remove ( ) ;
355+ tooltipRef . current = undefined ;
356+ }
340357 } ;
341358 } ) ;
342359
@@ -383,6 +400,110 @@ const useChartElem = (symbol: string, feedId: string) => {
383400 }
384401 } , [ livePriceData ?. exponent , priceFormatter ] ) ;
385402
403+ const dateFormatter = useDateFormatter ( {
404+ year : "2-digit" ,
405+ month : "short" ,
406+ day : "numeric" ,
407+ hour : "2-digit" ,
408+ minute : "2-digit" ,
409+ second : "2-digit" ,
410+ hour12 : false ,
411+ timeZone : "UTC" ,
412+ } ) ;
413+
414+ // Subscribe to crosshair move for tooltip updates
415+ useEffect ( ( ) => {
416+ const chartData = chartRef . current ;
417+ const tooltip = tooltipRef . current ;
418+ const chartElem = chartContainerRef . current ;
419+
420+ if ( ! chartData || ! tooltip || ! chartElem ) {
421+ return ;
422+ }
423+
424+ const { chart, price, confidenceHigh, confidenceLow } = chartData ;
425+
426+ const handleCrosshairMove : Parameters <
427+ typeof chart . subscribeCrosshairMove
428+ > [ 0 ] = ( param ) => {
429+ const priceData = param . seriesData . get ( price ) ;
430+ const confidenceHighData = param . seriesData . get ( confidenceHigh ) ;
431+ const confidenceLowData = param . seriesData . get ( confidenceLow ) ;
432+
433+ const hasPrice = priceData && "value" in priceData ;
434+ const hasTime = param . time !== undefined ;
435+ const hasPoint = param . point && "x" in param . point && "y" in param . point ;
436+
437+ if ( ! hasPrice || ! hasTime || ! hasPoint ) {
438+ tooltip . style . display = "none" ;
439+ return ;
440+ }
441+
442+ const priceValue = priceData . value ;
443+ const date = new Date ( Number ( param . time ) * 1000 ) ;
444+ const formattedDate = dateFormatter . format ( date ) ;
445+ const formattedPrice = priceFormatter . format ( priceValue ) ;
446+
447+ let confidenceText = "N/A" ;
448+ if (
449+ confidenceHighData &&
450+ "value" in confidenceHigh &&
451+ confidenceLowData &&
452+ "value" in confidenceLow
453+ ) {
454+ confidenceText = `±${ priceFormatter . format ( confidenceHighData . value - priceValue ) } ` ;
455+ }
456+
457+ tooltip . innerHTML = `
458+ <table class="${ styles . tooltipTable ?? "" } ">
459+ <tbody>
460+ <tr>
461+ <td colspan="2">${ formattedDate } </td>
462+ </tr>
463+ <tr>
464+ <tr>
465+ <td>Price</td>
466+ <td>${ formattedPrice } </td>
467+ </tr>
468+ <tr>
469+ <td>Confidence</td>
470+ <td>${ confidenceText } </td>
471+ </tr>
472+ </tbody>
473+ </table>
474+ ` ;
475+
476+ const tooltipRect = tooltip . getBoundingClientRect ( ) ;
477+ const containerRect = chartElem . getBoundingClientRect ( ) ;
478+ const tooltipMargin = 20 ;
479+
480+ const left = Math . max (
481+ 0 ,
482+ Math . min (
483+ containerRect . width - tooltipRect . width ,
484+ param . point . x - tooltipRect . width / 2 ,
485+ ) ,
486+ ) ;
487+
488+ const coordinate = price . priceToCoordinate ( priceValue ) ;
489+ if ( coordinate === null ) {
490+ tooltip . style . display = "none" ;
491+ return ;
492+ }
493+ const top = Math . max ( 0 , coordinate - tooltipRect . height - tooltipMargin ) ;
494+
495+ tooltip . style . left = `${ String ( left ) } px` ;
496+ tooltip . style . top = `${ String ( top ) } px` ;
497+ tooltip . style . display = "block" ;
498+ } ;
499+
500+ chart . subscribeCrosshairMove ( handleCrosshairMove ) ;
501+
502+ return ( ) => {
503+ chart . unsubscribeCrosshairMove ( handleCrosshairMove ) ;
504+ } ;
505+ } , [ priceFormatter , chartContainerRef , dateFormatter ] ) ;
506+
386507 return { chartRef, chartContainerRef } ;
387508} ;
388509
0 commit comments