@@ -4,8 +4,10 @@ import {
44 CategoryScale ,
55 Chart as ChartJS ,
66 ChartOptions ,
7+ ChartType ,
78 LinearScale ,
89 Tooltip ,
10+ TooltipPositionerFunction ,
911} from "chart.js" ;
1012import { memo , useEffect , useState } from "react" ;
1113import { Bar } from "react-chartjs-2" ;
@@ -16,6 +18,19 @@ import { HourPrice, SpotResponse } from "../../types/spot";
1618
1719ChartJS . register ( BarElement , CategoryScale , LinearScale , Tooltip ) ;
1820
21+ declare module "chart.js" {
22+ interface TooltipPositionerMap {
23+ aboveCursor : TooltipPositionerFunction < ChartType > ;
24+ }
25+ }
26+
27+ // Pin the tooltip to the top of the chart at the pointer's x — on touch it then
28+ // rides above the fingertip (and slides along while scrubbing) instead of
29+ // hiding under it.
30+ Tooltip . positioners . aboveCursor = function ( _items , evt ) {
31+ return { x : evt . x , y : this . chart . chartArea . top } ;
32+ } ;
33+
1934const HOUR_MS = 3_600_000 ;
2035const SCALE_KEY = "halo.spot.showScale" ;
2136
@@ -80,9 +95,15 @@ const PriceChart: React.FC<{
8095 responsive : true ,
8196 maintainAspectRatio : false ,
8297 animation : { duration : 0 } ,
98+ // Select by column (x), not by hitting the bar itself — short bars are easy
99+ // to tap, and dragging scrubs hour-to-hour.
100+ interaction : { mode : "index" , intersect : false } ,
83101 plugins : {
84102 legend : { display : false } ,
85103 tooltip : {
104+ position : "aboveCursor" ,
105+ caretSize : 0 ,
106+ displayColors : false ,
86107 callbacks : {
87108 title : ( items ) => `klo ${ items [ 0 ] . label } ` ,
88109 label : ( ctx ) => `${ ( ctx . parsed . y as number ) . toFixed ( 2 ) } ${ unit } ` ,
@@ -104,7 +125,15 @@ const PriceChart: React.FC<{
104125 } ;
105126
106127 return (
107- < div css = { { position : "relative" , height : 120 } } >
128+ < div
129+ css = { {
130+ position : "relative" ,
131+ height : 120 ,
132+ touchAction : "pan-y" , // let the page scroll vertically; chart scrubs horizontally
133+ userSelect : "none" ,
134+ WebkitTapHighlightColor : "transparent" , // no grey flash on tap (iOS Safari)
135+ } }
136+ >
108137 < Bar data = { data } options = { options } />
109138 </ div >
110139 ) ;
@@ -140,19 +169,23 @@ const ScaleLegend: React.FC<{ unit: string }> = ({ unit }) => {
140169 ) ;
141170} ;
142171
143- // Day label + that day's min–max range — conveys the price scale with no
144- // chart-area cost, so the y-axis can stay hidden.
172+ // Day label + price scale: that day's min–max, and for the day containing the
173+ // current hour a "lowest – now – highest" readout so the live price has context.
174+ // Costs no chart area, so the y-axis can stay hidden.
145175const DayHeader : React . FC < {
146176 label : string ;
147177 points : HourPrice [ ] ;
148178 unit : string ;
149- } > = ( { label, points, unit } ) => {
179+ now : number ;
180+ } > = ( { label, points, unit, now } ) => {
150181 const theme = useTheme ( ) ;
151182 const sub = { ...theme . typography . caption , color : theme . colors . text . muted } ;
152183 const ps = points . map ( ( p ) => p . price ) ;
153- const range = ps . length
154- ? `${ Math . min ( ...ps ) . toFixed ( 1 ) } –${ Math . max ( ...ps ) . toFixed ( 1 ) } ${ unit } `
155- : "" ;
184+ const current = points . find ( ( p ) => {
185+ const start = new Date ( p . hour ) . getTime ( ) ;
186+ return start <= now && now < start + HOUR_MS ;
187+ } ) ?. price ;
188+
156189 return (
157190 < div
158191 css = { {
@@ -164,7 +197,23 @@ const DayHeader: React.FC<{
164197 } }
165198 >
166199 < span > { label } </ span >
167- < span > { range } </ span >
200+ { ps . length > 0 && (
201+ < span >
202+ { Math . min ( ...ps ) . toFixed ( 1 ) }
203+ { current !== undefined ? (
204+ < >
205+ { " – " }
206+ < span css = { { color : theme . colors . text . main , fontWeight : 600 } } >
207+ { current . toFixed ( 1 ) }
208+ </ span >
209+ { " – " }
210+ </ >
211+ ) : (
212+ "–"
213+ ) }
214+ { Math . max ( ...ps ) . toFixed ( 1 ) } { unit }
215+ </ span >
216+ ) }
168217 </ div >
169218 ) ;
170219} ;
@@ -288,7 +337,12 @@ const SpotPrice: React.FC<{ className?: string }> = ({ className }) => {
288337 intrinsic width, so the charts scale on narrow phones instead
289338 of overflowing. */ }
290339 < div css = { { minWidth : 0 } } >
291- < DayHeader label = "tänään" points = { data . today } unit = { data . unit } />
340+ < DayHeader
341+ label = "tänään"
342+ points = { data . today }
343+ unit = { data . unit }
344+ now = { now }
345+ />
292346 < PriceChart
293347 points = { data . today }
294348 unit = { data . unit }
@@ -302,6 +356,7 @@ const SpotPrice: React.FC<{ className?: string }> = ({ className }) => {
302356 label = "huomenna"
303357 points = { data . tomorrow }
304358 unit = { data . unit }
359+ now = { now }
305360 />
306361 { data . tomorrow . length ? (
307362 < PriceChart
0 commit comments