Heatmaps
Generate gaze heatmap visualizations to understand where users focus their attention.
Overview
A gaze heatmap overlays a color-coded density map on your page showing where the user has been looking. High-density areas appear red/yellow; areas never looked at appear transparent.
WebGazer-TS provides a useGazeHeatmap React hook and a HeatmapOverlay component for rendering heatmaps, as well as utilities for exporting the data.
Quick Start
With the HeatmapOverlay Component
The easiest approach — drop it anywhere inside a <WebgazerProvider>:
tsx
import { WebgazerProvider, HeatmapOverlay } from '@webgazer-ts/react';
export function App() {
return (
<WebgazerProvider autoStart>
<YourPageContent />
<HeatmapOverlay /> {/* Full-screen canvas, pointer-events: none */}
</WebgazerProvider>
);
}With the useGazeHeatmap Hook
For more control, use the hook directly:
tsx
import { useGazeHeatmap } from '@webgazer-ts/react';
function MyHeatmap() {
const { canvasRef, points, clear, exportImage } = useGazeHeatmap({
radius: 40,
maxOpacity: 0.7,
blur: 20,
});
return (
<div style={{ position: 'relative', width: '100%', height: '100vh' }}>
<YourContent />
<canvas
ref={canvasRef}
style={{
position: 'absolute',
top: 0,
left: 0,
pointerEvents: 'none',
opacity: 0.8,
}}
/>
<div>
<button onClick={clear}>Clear ({points.length} points)</button>
<button onClick={() => {
const dataUrl = exportImage();
if (dataUrl) window.open(dataUrl);
}}>
Save as PNG
</button>
</div>
</div>
);
}Configuration Options
useGazeHeatmap Options
typescript
interface UseGazeHeatmapOptions {
/** Canvas width in pixels. Default: window.innerWidth */
width?: number;
/** Canvas height in pixels. Default: window.innerHeight */
height?: number;
/** Radius of each gaze point's influence (px). Default: 30 */
radius?: number;
/** Maximum opacity of the densest areas (0–1). Default: 0.8 */
maxOpacity?: number;
/** Gaussian blur applied to the heat layer (px). Default: 15 */
blur?: number;
/** Color gradient stops (0–1 → CSS color). Default: blue→cyan→lime→yellow→red */
gradient?: Record<number, string>;
}Custom Gradient
typescript
const { canvasRef } = useGazeHeatmap({
gradient: {
0.0: 'rgba(0, 0, 255, 0)', // Transparent blue at low density
0.4: '#00bcd4', // Cyan
0.6: '#ffeb3b', // Yellow
1.0: '#f44336', // Red at high density
},
});Returned Values
typescript
interface UseGazeHeatmapReturn {
/** Attach to a <canvas> element */
canvasRef: React.RefObject<HTMLCanvasElement>;
/** All collected gaze points (up to 1000) */
points: HeatmapPoint[]; // { x, y, timestamp }
/** Clear all points and the canvas */
clear: () => void;
/** Export points as CSV string */
exportData: () => string;
/** Export canvas as PNG data URL */
exportImage: () => string | null;
}Exporting Data
Export as CSV
tsx
function DownloadButton() {
const { exportData } = useGazeHeatmap();
const handleDownload = () => {
const csv = exportData();
// csv format: "x,y,timestamp\n100,200,1716912345678\n..."
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'gaze-heatmap.csv';
a.click();
URL.revokeObjectURL(url);
};
return <button onClick={handleDownload}>Download CSV</button>;
}Export as PNG
tsx
function SaveButton() {
const { exportImage } = useGazeHeatmap();
return (
<button onClick={() => {
const dataUrl = exportImage(); // Returns PNG data URL
if (dataUrl) {
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'heatmap.png';
a.click();
}
}}>
Save Heatmap PNG
</button>
);
}Full Example — Research Session
A complete example suitable for user research sessions, combining recording control with heatmap visualization:
tsx
import { useState } from 'react';
import { WebgazerProvider, useGazeHeatmap } from '@webgazer-ts/react';
function ResearchSession() {
const [phase, setPhase] = useState<'idle' | 'recording' | 'review'>('idle');
const { canvasRef, points, clear, exportData, exportImage } = useGazeHeatmap({
radius: 35,
blur: 20,
maxOpacity: 0.75,
});
return (
<div>
<header>
<h1>User Research Session</h1>
<div>
{phase === 'idle' && (
<button onClick={() => setPhase('recording')}>Start Session</button>
)}
{phase === 'recording' && (
<button onClick={() => setPhase('review')}>End Session</button>
)}
{phase === 'review' && (
<>
<button onClick={exportData}>Download CSV ({points.length} points)</button>
<button onClick={() => {
const url = exportImage();
if (url) window.open(url);
}}>View Heatmap PNG</button>
<button onClick={() => { clear(); setPhase('idle'); }}>New Session</button>
</>
)}
</div>
</header>
<main>
{/* Your stimulus content here */}
<YourStimulusContent />
{/* Heatmap overlay — only show during review */}
{phase === 'review' && (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
}}
/>
)}
</main>
</div>
);
}
export default function App() {
return (
<WebgazerProvider autoStart>
<ResearchSession />
</WebgazerProvider>
);
}Vanilla JS Heatmap
Without React, you can build a simple heatmap manually using the setGazeListener callback:
javascript
const canvas = document.createElement('canvas');
canvas.style.cssText = 'position:fixed;top:0;left:0;pointer-events:none;opacity:0.7';
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
const points = [];
webgazer.setGazeListener((data) => {
if (!data) return;
points.push({ x: data.x, y: data.y });
// Redraw every 30 points for performance
if (points.length % 30 !== 0) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.filter = 'blur(20px)';
points.forEach(({ x, y }) => {
const gradient = ctx.createRadialGradient(x, y, 0, x, y, 40);
gradient.addColorStop(0, 'rgba(255, 0, 0, 0.15)');
gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(x - 40, y - 40, 80, 80);
});
ctx.filter = 'none';
});Performance Notes
- The hook caps stored points at 1,000 to prevent unbounded memory growth.
- Canvas redraws are triggered by React state updates on every new gaze point. For long sessions, consider throttling: pass
throttleMsto youruseGazeTrackinghook and only callsetPointson the throttled callback. - For sessions with many thousands of points, consider offloading the canvas rendering to a Web Worker using
OffscreenCanvas.