@@ -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,117 @@ 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+ if (
430+ ! param . time ||
431+ param . point === undefined ||
432+ param . point . x < 0 ||
433+ param . point . y < 0
434+ ) {
435+ tooltip . style . display = "none" ;
436+ return ;
437+ }
438+
439+ const priceData = param . seriesData . get ( price ) ;
440+ const confHighData = param . seriesData . get ( confidenceHigh ) ;
441+ const confLowData = param . seriesData . get ( confidenceLow ) ;
442+
443+ if ( ! priceData || ! ( "value" in priceData ) ) {
444+ tooltip . style . display = "none" ;
445+ return ;
446+ }
447+
448+ const priceValue = priceData . value ;
449+ const date = new Date ( Number ( param . time ) * 1000 ) ;
450+ const formattedDate = dateFormatter . format ( date ) ;
451+ const formattedPrice = priceFormatter . format ( priceValue ) ;
452+
453+ let confidenceText = "N/A" ;
454+ if (
455+ confHighData &&
456+ "value" in confHighData &&
457+ confLowData &&
458+ "value" in confLowData
459+ ) {
460+ const confidence = confHighData . value - priceValue ;
461+ confidenceText = `±${ priceFormatter . format ( confidence ) } ` ;
462+ }
463+
464+ tooltip . innerHTML = `
465+ <table class="${ styles . tooltipTable ?? "" } ">
466+ <tbody>
467+ <tr>
468+ <td colspan="2">${ formattedDate } </td>
469+ </tr>
470+ <tr>
471+ <tr>
472+ <td>Price</td>
473+ <td>${ formattedPrice } </td>
474+ </tr>
475+ <tr>
476+ <td>Confidence</td>
477+ <td>${ confidenceText } </td>
478+ </tr>
479+ </tbody>
480+ </table>
481+ ` ;
482+
483+ const tooltipRect = tooltip . getBoundingClientRect ( ) ;
484+ const containerRect = chartElem . getBoundingClientRect ( ) ;
485+ const tooltipMargin = 20 ;
486+
487+ const left = Math . max (
488+ 0 ,
489+ Math . min (
490+ containerRect . width - tooltipRect . width ,
491+ param . point . x - tooltipRect . width / 2 ,
492+ ) ,
493+ ) ;
494+
495+ const coordinate = price . priceToCoordinate ( priceValue ) ;
496+ if ( coordinate === null ) {
497+ tooltip . style . display = "none" ;
498+ return ;
499+ }
500+ const top = Math . max ( 0 , coordinate - tooltipRect . height - tooltipMargin ) ;
501+
502+ tooltip . style . left = `${ String ( left ) } px` ;
503+ tooltip . style . top = `${ String ( top ) } px` ;
504+ tooltip . style . display = "block" ;
505+ } ;
506+
507+ chart . subscribeCrosshairMove ( handleCrosshairMove ) ;
508+
509+ return ( ) => {
510+ chart . unsubscribeCrosshairMove ( handleCrosshairMove ) ;
511+ } ;
512+ } , [ priceFormatter , chartContainerRef , dateFormatter ] ) ;
513+
386514 return { chartRef, chartContainerRef } ;
387515} ;
388516
0 commit comments