@@ -289,17 +289,14 @@ function countUp(node, target, opts = {}) {
289289 const ys = ( t ) => VH - PAD - ( ( t - minTotal ) / range ) * ( VH - 2 * PAD ) ;
290290 const line = points . map ( ( p , i ) => `${ i ? "L" : "M" } ${ xs ( i ) . toFixed ( 1 ) } ${ ys ( p . total ) . toFixed ( 1 ) } ` ) . join ( " " ) ;
291291 const area = `${ line } L${ xs ( points . length - 1 ) . toFixed ( 1 ) } ${ VH } L${ xs ( 0 ) . toFixed ( 1 ) } ${ VH } Z` ;
292- const last = points [ points . length - 1 ] ;
293292 chartEl . innerHTML = `<svg class="history-svg" viewBox="0 0 ${ VW } ${ VH } " preserveAspectRatio="none" aria-label="Dataset growth curve">
294293 <defs><linearGradient id="histfill" x1="0" y1="0" x2="0" y2="1">
295294 <stop offset="0%" stop-color="var(--accent)" stop-opacity=".34"></stop>
296295 <stop offset="100%" stop-color="var(--accent)" stop-opacity="0"></stop>
297296 </linearGradient></defs>
298297 <path d="${ area } " fill="url(#histfill)"></path>
299298 <path d="${ line } " fill="none" stroke="var(--accent)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"></path>
300- </svg>
301- <span class="history-cap history-cap-lo">${ esc ( points [ 0 ] . when ) } · ${ minTotal . toLocaleString ( ) } </span>
302- <span class="history-cap history-cap-hi">${ esc ( last . when ) } · ${ last . total . toLocaleString ( ) } </span>` ;
299+ </svg>` ;
303300
304301 // Show every sync (newest first), growth-first. The list scrolls (CSS
305302 // max-height) so the full history stays reachable without a giant section.
@@ -318,6 +315,47 @@ function countUp(node, target, opts = {}) {
318315 <small>${ esc ( changes ) } ${ tag ? ` · ${ esc ( tag ) } ` : "" } </small>
319316 </span></li>` ;
320317 } ) . join ( "" ) ;
318+
319+ // Hover the curve: snap to the nearest sync and show its date + total + delta.
320+ const hover = document . createElement ( "div" ) ;
321+ hover . className = "history-hover" ;
322+ hover . hidden = true ;
323+ hover . innerHTML = `<span class="hh-line"></span><span class="hh-dot"></span><div class="hh-tip"></div>` ;
324+ chartEl . appendChild ( hover ) ;
325+ const hLine = hover . querySelector ( ".hh-line" ) ;
326+ const hDot = hover . querySelector ( ".hh-dot" ) ;
327+ const hTip = hover . querySelector ( ".hh-tip" ) ;
328+
329+ function moveHover ( clientX ) {
330+ const rect = chartEl . getBoundingClientRect ( ) ;
331+ if ( ! rect . width ) return ;
332+ const relX = Math . min ( 1 , Math . max ( 0 , ( clientX - rect . left ) / rect . width ) ) ;
333+ const i = Math . round ( relX * ( points . length - 1 ) ) ;
334+ const p = points [ i ] ;
335+ const px = ( xs ( i ) / VW ) * rect . width ;
336+ const py = ( ys ( p . total ) / VH ) * rect . height ;
337+ hover . hidden = false ;
338+ hLine . style . left = px + "px" ;
339+ hDot . style . left = px + "px" ;
340+ hDot . style . top = py + "px" ;
341+ const chg = p . changes && p . changes . length
342+ ? p . changes . map ( ( row ) => `${ shortLabel [ row . key ] } ${ formatDelta ( row . delta ) } ` ) . join ( ", " )
343+ : "" ;
344+ hTip . innerHTML = `<b>${ esc ( p . when ) } </b>` +
345+ `<span>${ p . total . toLocaleString ( ) } records</span>` +
346+ `<span class="hh-delta${ p . delta < 0 ? " is-negative" : "" } ">${ p . baseline ? "baseline" : esc ( formatDelta ( p . delta ) ) } </span>` +
347+ ( chg ? `<span class="hh-chg">${ esc ( chg ) } </span>` : "" ) ;
348+ const tipW = hTip . offsetWidth || 150 ;
349+ let tx = px + 14 ;
350+ if ( tx + tipW > rect . width ) tx = px - tipW - 14 ;
351+ hTip . style . left = Math . max ( 4 , tx ) + "px" ;
352+ hTip . style . top = Math . min ( rect . height - ( hTip . offsetHeight || 70 ) - 4 , Math . max ( 4 , py - 24 ) ) + "px" ;
353+ }
354+ chartEl . onmousemove = ( e ) => moveHover ( e . clientX ) ;
355+ chartEl . onmouseleave = ( ) => { hover . hidden = true ; } ;
356+ chartEl . ontouchstart = chartEl . ontouchmove = ( e ) => {
357+ if ( e . touches [ 0 ] ) moveHover ( e . touches [ 0 ] . clientX ) ;
358+ } ;
321359 }
322360
323361 const fmtWhen = ( date ) => date
0 commit comments