# Positional Heatmap

Source: https://www.pitchkitjs.com/docs/overlays/positional-heatmap

> Density binned into Juego de Posición zones instead of a uniform grid.

```tsx
import { useEffect, useRef, useState } from "react";
import { Pitch, PositionalHeatmap } from "@pitchkit/react";

// StatsBomb coordinates (120 x 80). A midfielder's touches over a match:
// heaviest through the left half-space and the middle third, thinning out
// in both penalty areas.
const touches: { x: number; y: number }[] = [
  { x: 34, y: 22 },
  { x: 41, y: 18 },
  { x: 45, y: 26 },
  { x: 52, y: 21 },
  { x: 58, y: 30 },
  { x: 63, y: 24 },
  { x: 49, y: 33 },
  { x: 55, y: 38 },
  { x: 61, y: 41 },
  { x: 67, y: 35 },
  { x: 72, y: 28 },
  { x: 78, y: 24 },
  { x: 70, y: 45 },
  { x: 66, y: 52 },
  { x: 59, y: 48 },
  { x: 51, y: 55 },
  { x: 44, y: 44 },
  { x: 38, y: 39 },
  { x: 31, y: 47 },
  { x: 26, y: 40 },
  { x: 82, y: 33 },
  { x: 88, y: 27 },
  { x: 94, y: 31 },
  { x: 86, y: 44 },
  { x: 91, y: 52 },
  { x: 105, y: 38 },
  { x: 110, y: 42 },
  { x: 20, y: 36 },
  { x: 14, y: 41 },
  { x: 47, y: 12 },
  { x: 53, y: 9 },
  { x: 60, y: 66 },
  { x: 68, y: 71 },
  { x: 75, y: 63 },
  { x: 42, y: 60 },
  { x: 36, y: 68 },
  { x: 57, y: 43 },
  { x: 62, y: 37 },
  { x: 50, y: 40 },
  { x: 65, y: 46 },
];

const PITCH_ASPECT = 120 / 80;
const FALLBACK_WIDTH = 480;

/**
 * Like `<Heatmap>`, `<PositionalHeatmap>` paints to a canvas, which needs
 * a fixed pixel size up front — so this example measures its own container
 * (the same technique `<Pitch>` uses internally) rather than leaning on
 * <Pitch>'s responsive mode.
 */
export function PositionalHeatmapBasic() {
  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)}
      >
        <PositionalHeatmap
          data={touches}
          x={(t) => t.x}
          y={(t) => t.y}
          colorMin="#0f3d24"
          colorMax="#fb923c"
          stroke="rgba(255, 255, 255, 0.35)"
          strokeWidth={1}
          style={{ opacity: 0.85 }}
        />
      </Pitch>
    </div>
  );
}
```

## Usage

`<PositionalHeatmap>` aggregates the same way `<Heatmap>` does — count per zone, or the sum of a
`weight` accessor — but the cells come from the **pitch markings** rather than a `binsX` x `binsY`
grid: the penalty-area lines, the halfway line and the midpoints between them give six columns, and
the touchlines, penalty-area edges and six-yard-box edges give five lateral bands. This is
mplsoccer's `bin_statistic_positional` + `heatmap_positional`, ported zone-for-zone, so the layout
matches what analysts already read.

```tsx
<Pitch type="statsbomb" width={480} height={320}>
  <PositionalHeatmap data={touches} x={(t) => t.x} y={(t) => t.y} />
</Pitch>
```

## Layouts

The default `"full"` layout is not a grid — it's the 20-zone Juego de Posición board: the two flank
bands split across all six columns, the three central bands split only at the penalty-area lines,
and each penalty area as a single wide zone.

| `layout`           | Zones | Shape                                 |
| ------------------ | ----- | ------------------------------------- |
| `"full"` (default) | 20    | The Juego de Posición board           |
| `"horizontal"`     | 5     | Lateral bands only, full pitch length |
| `"vertical"`       | 6     | Columns only, full pitch width        |

```tsx
<PositionalHeatmap data={touches} x={(t) => t.x} y={(t) => t.y} layout="horizontal" />
```

## Zone outlines

Zone boundaries are off by default — they compete with the pitch markings they're derived from. Pass
`stroke` (and optionally `strokeWidth`) to draw them, as the example above does.

Like `<Heatmap>`, this layer renders to a `<canvas>` inside the pitch's SVG via `<foreignObject>`,
so it needs a fixed-size `<Pitch>` (`width`/`height`).

## Keeping the pitch markings visible

A filled density layer paints over the lines underneath it. Set `linesOnTop` on the pitch's
`appearance` — mplsoccer's `line_zorder` — to paint the markings above the layers instead. Only the
markings move; the grass surface and stripes stay underneath either way.

```tsx
<Pitch type="statsbomb" width={480} height={320} appearance={{ linesOnTop: true }}>
  {/* ... */}
</Pitch>
```
