Overlays
Heatmap
Canvas-based binned density, for shot maps and touch maps.
"use client";import { useEffect, useRef, useState } from "react";import { Heatmap, Pitch, Scatter } from "@pitchkit/react";import { docsAppearance } from "./docs-appearance";// StatsBomb coordinates (120 x 80) — a fuller shot map: a dense cluster of// good chances inside the box, a spread of half-chances around its edge,// and a few speculative long-range efforts. `xg` stands in for a real// expected-goals value, weighting the heatmap by shot quality rather than// raw count.const shots: { x: number; y: number; xg: number }[] = [ { x: 108, y: 38, xg: 0.62 }, { x: 104, y: 42, xg: 0.41 }, { x: 110, y: 35, xg: 0.55 }, { x: 106, y: 33, xg: 0.34 }, { x: 101, y: 45, xg: 0.28 }, { x: 113, y: 40, xg: 0.71 }, { x: 98, y: 30, xg: 0.19 }, { x: 96, y: 48, xg: 0.15 }, { x: 111, y: 44, xg: 0.48 }, { x: 103, y: 36, xg: 0.33 }, { x: 109, y: 41, xg: 0.52 }, { x: 90, y: 40, xg: 0.12 }, { x: 88, y: 25, xg: 0.06 }, { x: 85, y: 55, xg: 0.08 }, { x: 95, y: 60, xg: 0.09 }, { x: 99, y: 20, xg: 0.11 }, { x: 105, y: 50, xg: 0.22 }, { x: 115, y: 42, xg: 0.58 }, { x: 78, y: 40, xg: 0.04 }, { x: 70, y: 38, xg: 0.03 },];const PITCH_ASPECT = 120 / 80;const FALLBACK_WIDTH = 480;/** * `<Heatmap>` bins point density onto a canvas, which needs a fixed pixel * size up front — unlike the SVG mark layers on other pages, it can't * lean on <Pitch>'s own responsive ResizeObserver. This example measures * its own container instead (the same technique <Pitch> uses internally), * so the pitch still fills the preview card exactly like every other * example rather than sitting at a mismatched fixed size. */export function HeatmapBasic() { const containerRef = useRef<HTMLDivElement>(null); const [width, setWidth] = useState(FALLBACK_WIDTH); useEffect(() => { const el = containerRef.current; if (!el) return; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (entry) setWidth(entry.contentRect.width); }); observer.observe(el); return () => observer.disconnect(); }, []); return ( <div ref={containerRef}> <Pitch type="statsbomb" width={width} height={Math.round(width / PITCH_ASPECT)} appearance={docsAppearance} > <Heatmap data={shots} x={(s) => s.x} y={(s) => s.y} weight={(s) => s.xg} binsX={10} binsY={7} colorMin="#0f3d24" colorMax="#fb923c" style={{ opacity: 0.85 }} /> <Scatter data={shots} x={(s) => s.x} y={(s) => s.y} r={2.5} fill="white" fillOpacity={0.7} /> </Pitch> </div> );}Usage
<Heatmap> bins data by x/y (optionally weighted by a weight accessor) and paints them
to a <canvas> layered inside the pitch's SVG via <foreignObject>. Unlike the SVG mark
layers above, it needs a fixed-size <Pitch> (width/height) — canvas has no server-rendered
content to size against before the client measures its container.
<Pitch type="statsbomb" width={480} height={320}>
<Heatmap data={shots} x={(s) => s.x} y={(s) => s.y} binsX={8} binsY={6} />
</Pitch>