@@ -183,6 +183,14 @@ function resetHeatmap(message = HEATMAP_DEFAULT_STREAM_MESSAGE) {
183183 heatmapState . viewHeight = heatmapCanvas . height ;
184184 heatmapState . pendingScale = null ;
185185 heatmapState . sliceRequestId = 0 ;
186+ heatmapDragState . active = false ;
187+ heatmapDragState . touchId = null ;
188+ heatmapDragState . offsetX = 0 ;
189+ heatmapDragState . offsetY = 0 ;
190+ heatmapDragState . moved = false ;
191+ if ( heatmapCanvas ) {
192+ heatmapCanvas . classList . remove ( "heatmap-canvas--dragging" ) ;
193+ }
186194 if ( heatmapGainOffsetToggle ) {
187195 heatmapGainOffsetToggle . checked = false ;
188196 }
@@ -304,20 +312,48 @@ function ensureHeatmapBuffer(width, height) {
304312 }
305313}
306314
315+ function getPaddingBottomPx ( element ) {
316+ if ( ! element || typeof window . getComputedStyle !== "function" ) {
317+ return 0 ;
318+ }
319+ const styles = window . getComputedStyle ( element ) ;
320+ if ( ! styles ) {
321+ return 0 ;
322+ }
323+ const value = Number . parseFloat ( styles . paddingBottom ) ;
324+ if ( ! Number . isFinite ( value ) ) {
325+ return 0 ;
326+ }
327+ return Math . max ( 0 , value ) ;
328+ }
329+
307330function syncHeatmapToViewport ( fetchAfterResize ) {
308331 if ( heatmapCanvasWrapper && heatmapCanvasWrapper . offsetParent === null ) {
309332 return ;
310333 }
311334
312335 const viewport = getViewportDimensions ( ) ;
313- const availableHeight = Math . max ( 1 , viewport . height - VIEWPORT_MARGIN ) ;
336+ const viewportHeight = Math . max ( 1 , Math . floor ( viewport . height ) ) ;
337+ let availableHeight = viewportHeight ;
314338 if ( heatmapCanvasWrapper ) {
339+ const wrapperRect = heatmapCanvasWrapper . getBoundingClientRect ( ) ;
340+ const topOffset = Math . max ( 0 , Math . floor ( wrapperRect . top ) ) ;
341+ let paddingOffset = 1 ;
342+ const parentSection = heatmapCanvasWrapper . closest ( "section" ) ;
343+ if ( parentSection ) {
344+ paddingOffset += Math . ceil ( getPaddingBottomPx ( parentSection ) ) ;
345+ }
346+ const mainElement = heatmapCanvasWrapper . closest ( "main" ) ;
347+ if ( mainElement ) {
348+ paddingOffset += Math . ceil ( getPaddingBottomPx ( mainElement ) ) ;
349+ }
350+ availableHeight = Math . max ( 1 , viewportHeight - topOffset - paddingOffset ) ;
315351 heatmapCanvasWrapper . style . height = `${ availableHeight } px` ;
316352 }
317353
318354 const wrapperWidth = heatmapCanvasWrapper
319355 ? Math . max ( 1 , Math . floor ( heatmapCanvasWrapper . clientWidth || heatmapCanvasWrapper . offsetWidth || 1 ) )
320- : Math . max ( 1 , viewport . width - 64 ) ;
356+ : Math . max ( 1 , viewport . width ) ;
321357
322358 const hasTensor = ! ! heatmapState . tensor ;
323359 const layoutWidth = hasTensor && heatmapState . layout && heatmapState . layout . width > 0
@@ -340,6 +376,11 @@ function syncHeatmapToViewport(fetchAfterResize) {
340376 heatmapDragState . offsetX = 0 ;
341377 heatmapDragState . offsetY = 0 ;
342378 heatmapDragState . moved = false ;
379+ heatmapDragState . active = false ;
380+ heatmapDragState . touchId = null ;
381+ if ( heatmapCanvas ) {
382+ heatmapCanvas . classList . remove ( "heatmap-canvas--dragging" ) ;
383+ }
343384 if ( hasTensor ) {
344385 heatmapState . windowX = clampWindowX ( heatmapState . windowX ) ;
345386 heatmapState . windowY = clampWindowY ( heatmapState . windowY ) ;
@@ -1100,21 +1141,93 @@ function heatmapCanvasScale() {
11001141 return { scaleX, scaleY } ;
11011142}
11021143
1103- heatmapCanvas . addEventListener ( "mousedown" , ( event ) => {
1104- if ( ! heatmapState . tensor ) {
1105- return ;
1144+ function beginHeatmapDrag ( clientX , clientY , options = { } ) {
1145+ if ( ! heatmapState . tensor || heatmapDragState . active ) {
1146+ return false ;
11061147 }
1107- event . preventDefault ( ) ;
11081148 hideHeatmapTooltip ( ) ;
11091149 heatmapDragState . active = true ;
1110- heatmapDragState . lastX = event . clientX ;
1111- heatmapDragState . lastY = event . clientY ;
1150+ heatmapDragState . lastX = clientX ;
1151+ heatmapDragState . lastY = clientY ;
11121152 heatmapDragState . offsetX = 0 ;
11131153 heatmapDragState . offsetY = 0 ;
11141154 heatmapDragState . moved = false ;
1155+ heatmapDragState . touchId = typeof options . touchId === "number" ? options . touchId : null ;
11151156 heatmapCanvas . classList . add ( "heatmap-canvas--dragging" ) ;
1157+ return true ;
1158+ }
1159+
1160+ function updateHeatmapDragPosition ( clientX , clientY ) {
1161+ if ( ! heatmapDragState . active ) {
1162+ return ;
1163+ }
1164+ const { scaleX, scaleY } = heatmapCanvasScale ( ) ;
1165+ const dx = ( clientX - heatmapDragState . lastX ) * scaleX ;
1166+ const dy = ( clientY - heatmapDragState . lastY ) * scaleY ;
1167+ heatmapDragState . lastX = clientX ;
1168+ heatmapDragState . lastY = clientY ;
1169+
1170+ if ( dx === 0 && dy === 0 ) {
1171+ return ;
1172+ }
1173+
1174+ const beforeX = heatmapDragState . offsetX ;
1175+ const beforeY = heatmapDragState . offsetY ;
1176+ heatmapDragState . offsetX += dx ;
1177+ heatmapDragState . offsetY += dy ;
1178+ clampDragOffsets ( ) ;
1179+ if ( heatmapDragState . offsetX !== beforeX || heatmapDragState . offsetY !== beforeY ) {
1180+ heatmapDragState . moved = true ;
1181+ }
1182+
1183+ if ( heatmapState . imageReady ) {
1184+ drawHeatmap ( heatmapDragState . offsetX , heatmapDragState . offsetY ) ;
1185+ }
1186+ }
1187+
1188+ function findTouchById ( touchList , identifier ) {
1189+ if ( ! touchList || typeof touchList . length !== "number" ) {
1190+ return null ;
1191+ }
1192+ for ( let i = 0 ; i < touchList . length ; i += 1 ) {
1193+ const touch = typeof touchList . item === "function" ? touchList . item ( i ) : touchList [ i ] ;
1194+ if ( touch && touch . identifier === identifier ) {
1195+ return touch ;
1196+ }
1197+ }
1198+ return null ;
1199+ }
1200+
1201+ function getTrackedTouch ( event ) {
1202+ if ( heatmapDragState . touchId === null ) {
1203+ return null ;
1204+ }
1205+ const { touchId } = heatmapDragState ;
1206+ return findTouchById ( event . touches , touchId ) || findTouchById ( event . changedTouches , touchId ) ;
1207+ }
1208+
1209+ heatmapCanvas . addEventListener ( "mousedown" , ( event ) => {
1210+ if ( ! beginHeatmapDrag ( event . clientX , event . clientY ) ) {
1211+ return ;
1212+ }
1213+ event . preventDefault ( ) ;
11161214} ) ;
11171215
1216+ heatmapCanvas . addEventListener ( "touchstart" , ( event ) => {
1217+ const touch = event . changedTouches && event . changedTouches . length > 0
1218+ ? event . changedTouches [ 0 ]
1219+ : event . touches && event . touches . length > 0
1220+ ? event . touches [ 0 ]
1221+ : null ;
1222+ if ( ! touch ) {
1223+ return ;
1224+ }
1225+ if ( ! beginHeatmapDrag ( touch . clientX , touch . clientY , { touchId : touch . identifier } ) ) {
1226+ return ;
1227+ }
1228+ event . preventDefault ( ) ;
1229+ } , { passive : false } ) ;
1230+
11181231heatmapCanvas . addEventListener ( "mousemove" , ( event ) => {
11191232 if ( ! heatmapState . tensor || ! heatmapState . imageReady || heatmapDragState . active ) {
11201233 return ;
@@ -1158,38 +1271,27 @@ heatmapCanvas.addEventListener("mousemove", (event) => {
11581271} ) ;
11591272
11601273window . addEventListener ( "mousemove" , ( event ) => {
1274+ updateHeatmapDragPosition ( event . clientX , event . clientY ) ;
1275+ } ) ;
1276+
1277+ window . addEventListener ( "touchmove" , ( event ) => {
11611278 if ( ! heatmapDragState . active ) {
11621279 return ;
11631280 }
1164- const { scaleX, scaleY } = heatmapCanvasScale ( ) ;
1165- const dx = ( event . clientX - heatmapDragState . lastX ) * scaleX ;
1166- const dy = ( event . clientY - heatmapDragState . lastY ) * scaleY ;
1167- heatmapDragState . lastX = event . clientX ;
1168- heatmapDragState . lastY = event . clientY ;
1169-
1170- if ( dx === 0 && dy === 0 ) {
1281+ const touch = getTrackedTouch ( event ) ;
1282+ if ( ! touch ) {
11711283 return ;
11721284 }
1173-
1174- const beforeX = heatmapDragState . offsetX ;
1175- const beforeY = heatmapDragState . offsetY ;
1176- heatmapDragState . offsetX += dx ;
1177- heatmapDragState . offsetY += dy ;
1178- clampDragOffsets ( ) ;
1179- if ( heatmapDragState . offsetX !== beforeX || heatmapDragState . offsetY !== beforeY ) {
1180- heatmapDragState . moved = true ;
1181- }
1182-
1183- if ( heatmapState . imageReady ) {
1184- drawHeatmap ( heatmapDragState . offsetX , heatmapDragState . offsetY ) ;
1185- }
1186- } ) ;
1285+ event . preventDefault ( ) ;
1286+ updateHeatmapDragPosition ( touch . clientX , touch . clientY ) ;
1287+ } , { passive : false } ) ;
11871288
11881289function endHeatmapDrag ( ) {
11891290 if ( ! heatmapDragState . active ) {
11901291 return ;
11911292 }
11921293 heatmapDragState . active = false ;
1294+ heatmapDragState . touchId = null ;
11931295 heatmapCanvas . classList . remove ( "heatmap-canvas--dragging" ) ;
11941296 clampDragOffsets ( ) ;
11951297 if ( ! heatmapDragState . moved ) {
@@ -1218,6 +1320,27 @@ function endHeatmapDrag() {
12181320}
12191321
12201322window . addEventListener ( "mouseup" , endHeatmapDrag ) ;
1323+ window . addEventListener ( "touchend" , ( event ) => {
1324+ if ( ! heatmapDragState . active ) {
1325+ return ;
1326+ }
1327+ const touch = getTrackedTouch ( event ) ;
1328+ if ( ! touch ) {
1329+ return ;
1330+ }
1331+ event . preventDefault ( ) ;
1332+ endHeatmapDrag ( ) ;
1333+ } , { passive : false } ) ;
1334+ window . addEventListener ( "touchcancel" , ( event ) => {
1335+ if ( ! heatmapDragState . active ) {
1336+ return ;
1337+ }
1338+ const touch = getTrackedTouch ( event ) ;
1339+ if ( ! touch ) {
1340+ return ;
1341+ }
1342+ endHeatmapDrag ( ) ;
1343+ } ) ;
12211344heatmapCanvas . addEventListener ( "mouseleave" , ( ) => {
12221345 hideHeatmapTooltip ( ) ;
12231346 if ( heatmapDragState . active ) {
0 commit comments