From beb4881c2423684c188cb36c377b499d9178c3f0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 21 Jun 2026 14:23:05 -0700 Subject: [PATCH] Fit heatmap canvases to viewport --- .../(reports)/heatmaps/Heatmap.module.css | 15 ++- .../(reports)/heatmaps/Heatmap.tsx | 98 +++++++++++++++---- 2 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css index 689061ba4..9be551874 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.module.css @@ -112,21 +112,17 @@ max-width: 100%; max-height: 75vh; overflow-x: hidden; - overflow-y: auto; + overflow-y: hidden; overscroll-behavior: contain; } -.canvasWrapperScrollable { - overflow-x: auto; -} - .snapshotControlRow { margin-top: 8px; } .canvas { position: relative; - overflow: visible; + overflow: hidden; border: 1px solid var(--border-base); border-radius: 8px; background: var(--surface-sunken); @@ -134,10 +130,11 @@ margin-inline: auto; } -.canvasClip { +.canvasSurface { position: absolute; - inset: 0; - overflow: hidden; + top: 0; + left: 0; + transform-origin: top left; border-radius: inherit; background: inherit; } diff --git a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx index 1101ee640..95d3bbfde 100644 --- a/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx +++ b/src/app/(main)/websites/[websiteId]/(reports)/heatmaps/Heatmap.tsx @@ -1,6 +1,6 @@ 'use client'; import { Column, Grid, Heading, Loading, Row, Switch, Text } from '@umami/react-zen'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { LoadingPanel } from '@/components/common/LoadingPanel'; import { useResultQuery } from '@/components/hooks'; import { getClientAuthToken } from '@/lib/client'; @@ -10,6 +10,7 @@ import styles from './Heatmap.module.css'; const CLICK_EDGE_PERCENT = 1.5; const SCROLL_BUCKET_SIZE = 10; +const CANVAS_MAX_HEIGHT_RATIO = 0.75; interface ViewportBucket { width: number; @@ -230,6 +231,53 @@ function pickViewport(points: HeatmapPoint[]): ViewportBucket | null { }; } +function useCanvasFit(renderWidth: number, renderHeight: number) { + const wrapperRef = useRef(null); + const [available, setAvailable] = useState({ width: 0, height: 0 }); + + useEffect(() => { + const updateAvailableSize = () => { + const rect = wrapperRef.current?.getBoundingClientRect(); + const width = rect?.width ?? 0; + const height = window.innerHeight * CANVAS_MAX_HEIGHT_RATIO; + + setAvailable(current => + current.width === width && current.height === height ? current : { width, height }, + ); + }; + + updateAvailableSize(); + + const resizeObserver = + typeof ResizeObserver !== 'undefined' ? new ResizeObserver(updateAvailableSize) : null; + + if (wrapperRef.current && resizeObserver) { + resizeObserver.observe(wrapperRef.current); + } + + window.addEventListener('resize', updateAvailableSize); + + return () => { + resizeObserver?.disconnect(); + window.removeEventListener('resize', updateAvailableSize); + }; + }, []); + + const safeWidth = Math.max(1, renderWidth); + const safeHeight = Math.max(1, renderHeight); + const scale = + available.width && available.height + ? Math.min(1, available.width / safeWidth, available.height / safeHeight) + : 1; + + return { + wrapperRef, + scale, + width: Math.max(1, Math.round(safeWidth * scale)), + height: Math.max(1, Math.round(safeHeight * scale)), + }; +} + function ClickHeatmapView({ urlPath, points, @@ -274,11 +322,9 @@ function ClickHeatmapView({ const renderWidth = snapshot?.pageW ?? baseWidth; const renderHeight = snapshot?.pageH ?? baseHeight; const hasMeasuredWidth = Boolean(snapshot?.pageW || viewport?.pageW || maxPointX); - const canvasWidth = hasMeasuredWidth - ? snapshot?.kind === 'iframe' - ? `${renderWidth}px` - : `min(100%, ${renderWidth}px)` - : '100%'; + const fit = useCanvasFit(renderWidth, renderHeight); + const canvasWidth = hasMeasuredWidth ? `${fit.width}px` : '100%'; + const canvasHeight = hasMeasuredWidth ? `${fit.height}px` : undefined; const overlayPageW = snapshot?.pageW ?? viewport?.pageW ?? baseWidth; const overlayPageH = snapshot?.pageH ?? viewport?.pageH ?? baseHeight; const shouldRenderSnapshot = renderWidth > 0 && hasSnapshot; @@ -317,13 +363,12 @@ function ClickHeatmapView({ )} -
+
@@ -332,7 +377,14 @@ function ClickHeatmapView({ ) : !viewport || visible.length === 0 ? ( ) : ( - <> +
{showSnapshot && !snapshotReady && } {shouldRenderSnapshot && snapshot && ( @@ -378,7 +430,7 @@ function ClickHeatmapView({ })}
)} - +
)}
@@ -426,11 +478,9 @@ function ScrollHeatmapView({ const renderWidth = snapshot?.pageW ?? baseWidth; const renderHeight = snapshot?.pageH ?? baseHeight; const hasMeasuredWidth = Boolean(snapshot?.pageW || pageW); - const canvasWidth = hasMeasuredWidth - ? snapshot?.kind === 'iframe' - ? `${renderWidth}px` - : `min(100%, ${renderWidth}px)` - : '100%'; + const fit = useCanvasFit(renderWidth, renderHeight); + const canvasWidth = hasMeasuredWidth ? `${fit.width}px` : '100%'; + const canvasHeight = hasMeasuredWidth ? `${fit.height}px` : undefined; const shouldRenderSnapshot = renderWidth > 0 && hasSnapshot; const showSnapshot = shouldRenderSnapshot && showPage; const showOverlay = !showPage || !shouldRenderSnapshot || snapshotReady; @@ -484,13 +534,12 @@ function ScrollHeatmapView({ )} -
+
@@ -499,7 +548,14 @@ function ScrollHeatmapView({ ) : !hasScrollData ? ( ) : ( -
+
{showSnapshot && !snapshotReady && } {shouldRenderSnapshot && snapshot && (