From 7d84c84d9aad724084ec4f5acd852c6e0cb0a4e0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 22 Jun 2026 21:48:29 -0700 Subject: [PATCH] Clip outlier heatmap clicks without stretching canvas --- .../(reports)/heatmaps/Heatmap.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index a906886d0..4c0d782ed 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -572,17 +572,25 @@ function ClickHeatmapView({ }, [hasSnapshot, snapshot?.id]); const overlayGutter = Math.max(48, Math.round((viewport?.width ?? 1920) * 0.04)); const maxPointX = visible.reduce((max, point) => Math.max(max, point.pageX), 0); - const maxPointY = visible.reduce((max, point) => Math.max(max, point.pageY), 0); + // Size the canvas to the actual page content (snapshot/viewport height) only. + // Outlier clicks recorded far below the real content are clipped by the + // canvas's `overflow: hidden` rather than stretching the canvas and leaving a + // large empty band at the bottom. const baseWidth = Math.max(viewport?.pageW ?? 0, maxPointX + overlayGutter, 1); - const baseHeight = Math.max(viewport?.pageH ?? 0, maxPointY + overlayGutter, 640); const renderWidth = viewport?.width ?? snapshot?.viewportW ?? baseWidth; - const renderHeight = Math.max(viewport?.pageH ?? 0, snapshot?.pageH ?? 0, baseHeight); + // When we have a snapshot, its captured page height is the authoritative + // content height (it ends at the real page bottom). Use it directly so the + // canvas isn't stretched by an inflated aggregate `viewport.pageH` from tall + // outlier sessions. Outlier click dots below the content are clipped by the + // canvas's `overflow: hidden`. Fall back to the aggregate height only when no + // snapshot is available. + const contentHeight = snapshot?.pageH || viewport?.pageH || 0; + const renderHeight = Math.max(contentHeight, 640); const hasMeasuredWidth = Boolean(viewport?.width || snapshot?.viewportW || maxPointX); const fit = useCanvasFit(renderWidth, renderHeight); const canvasWidth = hasMeasuredWidth ? `${fit.width}px` : '100%'; const canvasHeight = hasMeasuredWidth ? `${fit.height}px` : undefined; const overlayPageW = renderWidth; - const overlayPageH = renderHeight; const shouldRenderSnapshot = renderWidth > 0 && hasSnapshot; const showOverlay = !shouldRenderSnapshot || snapshotReady; const totalClicks = visible.reduce((sum, point) => sum + point.count, 0); @@ -616,7 +624,7 @@ function ClickHeatmapView({ > {viewport - ? `${visible.length} positions - ${formatLongNumber(totalClicks)} clicks - screen width ${viewport.width}px` + ? `${visible.length} positions - ${formatLongNumber(totalClicks)} clicks` : 'No click data for this page yet.'} {hasScrollData - ? `${formatLongNumber(totalSessions)} sessions - page ${pageW}x${pageH}${viewportH ? ` - viewport ${viewportW}x${viewportH}` : ''} - screen width ${viewport.width}px` + ? `${formatLongNumber(totalSessions)} sessions - page ${pageW}x${pageH}${viewportH ? ` - viewport ${viewportW}x${viewportH}` : ''}` : 'No scroll data for this page yet.'}