@pitchkit/react 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yohahn Ribeiro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ # @pitchkit/react
2
+
3
+ [![npm](https://img.shields.io/npm/v/@pitchkit/react)](https://www.npmjs.com/package/@pitchkit/react)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/yribeiro/pitchkit/blob/main/LICENSE)
5
+
6
+ Declarative React components for football pitch visualisation — mplsoccer's feature set for
7
+ the web. Responsive by default, themed with CSS variables, SSR-safe.
8
+
9
+ **Docs and live examples: [pitchkitjs.com](https://pitchkitjs.com)**
10
+
11
+ > **Early days.** `0.1.x` is the first public release. Usable and tested, but the API isn't
12
+ > stable yet — expect breaking changes before `1.0`.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @pitchkit/react
18
+ ```
19
+
20
+ React >= 18 is a peer dependency. [`@pitchkit/core`](https://www.npmjs.com/package/@pitchkit/core)
21
+ is installed automatically.
22
+
23
+ ## Usage
24
+
25
+ ```tsx
26
+ import { Pitch, Scatter } from "@pitchkit/react";
27
+
28
+ const shots = [
29
+ { x: 112, y: 39, xg: 0.76, outcome: "goal" },
30
+ { x: 105, y: 44, xg: 0.31, outcome: "saved" },
31
+ { x: 99, y: 47, xg: 0.13, outcome: "off target" },
32
+ ];
33
+
34
+ export function ShotMap() {
35
+ return (
36
+ <Pitch type="statsbomb">
37
+ <Scatter
38
+ data={shots}
39
+ x={(s) => s.x}
40
+ y={(s) => s.y}
41
+ r={(s) => 3 + s.xg * 9}
42
+ fill={(s) => (s.outcome === "goal" ? "#fb923c" : "#38bdf8")}
43
+ tooltip={(s) => `${s.outcome} · xG ${s.xg.toFixed(2)}`}
44
+ />
45
+ </Pitch>
46
+ );
47
+ }
48
+ ```
49
+
50
+ `<Pitch>` owns the coordinate system; children are layers drawn into it, stacked in render
51
+ order. Every visual prop accepts a static value **or** a function of the datum — `fill="red"`
52
+ and `fill={(d) => d.teamColor}` are the same prop.
53
+
54
+ ## Components
55
+
56
+ | Component | Draws |
57
+ | ------------------ | ----------------------------------------------------------- |
58
+ | `<Pitch>` | The pitch surface + coordinate context (horizontal) |
59
+ | `<VerticalPitch>` | Same, rotated to a vertical framing |
60
+ | `<Scatter>` | Circles — shots, players, events |
61
+ | `<Annotate>` | Text labels |
62
+ | `<Arrows>` | Straight arrows — passes, carries |
63
+ | `<Comet>` | Tapered lines with direction implied by width |
64
+ | `<Heatmap>` | Binned density on Canvas (client-only) |
65
+ | `<PositionalHeatmap>` | Juego de Posición zone density on Canvas (client-only) |
66
+ | `<Hexbin>` | Hexagonal density on Canvas (client-only) |
67
+ | `<KDE>` | Smooth kernel density surface on Canvas (client-only) |
68
+ | `<Polygon>` | Arbitrary closed shapes |
69
+ | `<ConvexHull>` | Convex hull of a point set |
70
+ | `<Voronoi>` | Voronoi cells, clipped to the pitch |
71
+ | `<GoalAngle>` | The angle-to-goal wedge from a shot location |
72
+ | `<Flow>` | Binned direction + magnitude vectors |
73
+ | `usePitch()` | Hook exposing the pixel transform for custom SVG |
74
+
75
+ ## Sizing
76
+
77
+ Responsive is the default — with no size props the pitch fills its container via
78
+ `ResizeObserver` and recomputes on resize. Explicit sizing is the opt-out:
79
+
80
+ ```tsx
81
+ <Pitch type="statsbomb" /> {/* fills container */}
82
+ <Pitch type="statsbomb" width={1200} height={800} /> {/* fixed — exports, OG images */}
83
+ ```
84
+
85
+ ## Layer order
86
+
87
+ Markings paint *below* the layer children by default, so discrete marks sit on top of the
88
+ lines. An opaque density fill will therefore cover them — set `appearance.linesOnTop` to paint
89
+ the markings above instead (mplsoccer's `line_zorder`):
90
+
91
+ ```tsx
92
+ <Pitch type="statsbomb" appearance={{ linesOnTop: true }}>
93
+ <KDE data={touches} x={(t) => t.x} y={(t) => t.y} />
94
+ </Pitch>
95
+ ```
96
+
97
+ Only the markings move — the grass surface and stripes stay at the bottom either way.
98
+
99
+ ## Pitch types
100
+
101
+ `statsbomb` · `opta` · `uefa`, each using the provider's real coordinate space so event data
102
+ goes in unmodified.
103
+
104
+ ```tsx
105
+ import { cropForHalf, getPitchDimensions } from "@pitchkit/core";
106
+
107
+ const dimensions = getPitchDimensions("statsbomb");
108
+
109
+ <VerticalPitch type="statsbomb" crop={cropForHalf(dimensions)}>{/* … */}</VerticalPitch>
110
+ ```
111
+
112
+ ## Theming
113
+
114
+ CSS variables only — no theme objects, no providers. Define once; every chart inherits,
115
+ including dark mode:
116
+
117
+ ```css
118
+ :root {
119
+ --pitch-surface: #1a472a;
120
+ --pitch-stripe: #1d4f30;
121
+ --pitch-lines: rgba(255, 255, 255, 0.8);
122
+ }
123
+ ```
124
+
125
+ Marks accept `className`, so Tailwind works directly:
126
+
127
+ ```tsx
128
+ <Scatter data={shots} x={(s) => s.x} y={(s) => s.y} className="fill-emerald-400 stroke-white" />
129
+ ```
130
+
131
+ Setting `className` without an explicit `fill`/`stroke` makes the mark drop its themed default
132
+ so your class wins. For marks whose JSX you don't own, every element carries
133
+ `data-pitchkit-mark` / `-layer` / `-part` attributes to target instead.
134
+
135
+ ## Next.js / SSR
136
+
137
+ SVG marks server-render cleanly. Because layer components take accessor *functions* as props,
138
+ the `<Pitch>` tree must originate inside a `"use client"` component — React Server Components
139
+ can't pass functions across the client boundary. SSR still happens; only the prop-serialisation
140
+ boundary moves. `<Heatmap>` is Canvas-backed and therefore client-only.
141
+
142
+ ## Agent Skill
143
+
144
+ No model has PitchKit in its training data, so coding agents asked for a shot map tend to
145
+ invent an mplsoccer-flavoured API. This package ships an Agent Skill — `skills/pitchkit/`
146
+ inside the tarball — that documents the real one.
147
+
148
+ ```bash
149
+ npx @pitchkit/react skills install # -> .claude/skills/pitchkit/
150
+ npx @pitchkit/react skills install --dir .cursor/skills
151
+ npx @pitchkit/react skills path # where it lives in node_modules
152
+ ```
153
+
154
+ Install *symlinks* the target at the copy inside `node_modules`, so `npm update
155
+ @pitchkit/react` moves the skill with it and an agent can't end up reading last version's
156
+ API. On a filesystem that won't take a symlink it copies instead and says so — that copy is
157
+ a snapshot, so re-run with `--force` after upgrading.
158
+
159
+ The default directory suits Claude Code; for any other agent, point `--dir` at wherever it
160
+ reads skills from, or hand it the `skills path` output to read directly. The layout follows
161
+ the `skills/<name>/SKILL.md` convention, so generic installers like `skills-npm` find it in
162
+ `node_modules` without needing this CLI at all.
163
+
164
+ ## Links
165
+
166
+ - [Documentation & gallery](https://pitchkitjs.com)
167
+ - [Repository](https://github.com/yribeiro/pitchkit)
168
+ - [Issues](https://github.com/yribeiro/pitchkit/issues)
169
+ - [`@pitchkit/core`](https://www.npmjs.com/package/@pitchkit/core) — the framework-agnostic engine
170
+
171
+ ## Licence
172
+
173
+ MIT © Yohahn Ribeiro
@@ -0,0 +1,223 @@
1
+ // The `npx @pitchkit/react skills install` implementation, kept separate from
2
+ // bin/pitchkit.mjs so it can be unit-tested without spawning a process or
3
+ // doing the "am I the main module?" dance (which npx's bin symlinks make
4
+ // unreliable). Zero dependencies on purpose: this runs in a consumer's
5
+ // project, via npx, before anything of ours is necessarily installed.
6
+ import { cp, lstat, mkdir, readdir, rm, symlink } from "node:fs/promises";
7
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ export const SKILL_NAME = "pitchkit";
11
+
12
+ /**
13
+ * Claude Code's project-scoped skills directory. Chosen as the default
14
+ * because it's the one convention with a settled on-disk layout; every
15
+ * other agent gets pointed at `--dir` in the help text rather than us
16
+ * guessing at a location it may not read.
17
+ */
18
+ export const DEFAULT_SKILLS_DIR = join(".claude", "skills");
19
+
20
+ /** `bin/` -> the package root, so this resolves inside a consumer's node_modules too. */
21
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
22
+
23
+ export const skillSourceDir = join(packageRoot, "skills", SKILL_NAME);
24
+
25
+ export class CliError extends Error {}
26
+
27
+ export function formatHelp() {
28
+ return `pitchkit — ships the PitchKit Agent Skill with the package you installed
29
+
30
+ Usage
31
+ npx @pitchkit/react skills install [options] Link the skill into your project
32
+ npx @pitchkit/react skills path Print the skill's location in node_modules
33
+ npx @pitchkit/react --help
34
+
35
+ Options
36
+ --dir <path> Skills directory to install into (default: ${DEFAULT_SKILLS_DIR})
37
+ --force Replace an existing installation
38
+ -h, --help Show this message
39
+
40
+ <dir>/${SKILL_NAME}/ is symlinked to the copy inside node_modules, so the skill stays in
41
+ step with the installed version; it falls back to a plain copy on filesystems that
42
+ won't take a link. The default directory suits Claude Code; for another agent, point
43
+ --dir at wherever it reads skills from, or use \`skills path\` and read SKILL.md
44
+ directly.`;
45
+ }
46
+
47
+ export function parseArgs(argv) {
48
+ const options = { command: null, dir: DEFAULT_SKILLS_DIR, force: false, help: false };
49
+ const positional = [];
50
+
51
+ for (let i = 0; i < argv.length; i += 1) {
52
+ const arg = argv[i];
53
+ if (arg === "-h" || arg === "--help") {
54
+ options.help = true;
55
+ } else if (arg === "--force" || arg === "-f") {
56
+ options.force = true;
57
+ } else if (arg === "--dir") {
58
+ const value = argv[i + 1];
59
+ if (value === undefined || value.startsWith("-")) {
60
+ throw new CliError("--dir needs a path, e.g. --dir .claude/skills");
61
+ }
62
+ options.dir = value;
63
+ i += 1;
64
+ } else if (arg.startsWith("--dir=")) {
65
+ const value = arg.slice("--dir=".length);
66
+ if (value === "") throw new CliError("--dir needs a path, e.g. --dir .claude/skills");
67
+ options.dir = value;
68
+ } else if (arg.startsWith("-")) {
69
+ throw new CliError(`Unknown option: ${arg}`);
70
+ } else {
71
+ positional.push(arg);
72
+ }
73
+ }
74
+
75
+ // `skills install` and a bare `install` both work — the former is what the
76
+ // docs show, the latter is what people type.
77
+ const words = positional[0] === "skills" ? positional.slice(1) : positional;
78
+ options.command = words[0] ?? null;
79
+
80
+ if (words.length > 1) {
81
+ throw new CliError(`Unexpected argument: ${words[1]}`);
82
+ }
83
+
84
+ return options;
85
+ }
86
+
87
+ /**
88
+ * `lstat`, not `access`, so a dangling symlink left by an uninstalled
89
+ * dependency still counts as present and gets cleaned up by `--force`
90
+ * rather than colliding with EEXIST.
91
+ */
92
+ async function entryExists(path) {
93
+ try {
94
+ await lstat(path);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Links the destination at the skill inside node_modules, so `npm update
103
+ * @pitchkit/react` carries the skill with it — the property that makes a
104
+ * bundled skill worth more than scraped docs, and the one a plain copy
105
+ * silently loses. Falls back to copying where the filesystem won't take a
106
+ * link (Windows without Developer Mode, some network and container mounts).
107
+ *
108
+ * Returns "link" or "copy" so the caller can tell the user which they got;
109
+ * the update semantics differ, so this isn't an implementation detail.
110
+ */
111
+ async function linkOrCopy(destination) {
112
+ try {
113
+ if (process.platform === "win32") {
114
+ // Junctions need an absolute target, but unlike real symlinks they
115
+ // don't need elevation or Developer Mode.
116
+ await symlink(skillSourceDir, destination, "junction");
117
+ } else {
118
+ // Relative, so the link survives the project being moved or checked
119
+ // out at a different path with node_modules already in place.
120
+ await symlink(relative(dirname(destination), skillSourceDir), destination, "dir");
121
+ }
122
+ return "link";
123
+ } catch {
124
+ await cp(skillSourceDir, destination, { recursive: true, force: true });
125
+ return "copy";
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Installs `skills/pitchkit/` from the installed package into the consumer's
131
+ * project. Returns the destination, how it was installed, and what's now
132
+ * readable there.
133
+ */
134
+ export async function installSkill({
135
+ cwd = process.cwd(),
136
+ dir = DEFAULT_SKILLS_DIR,
137
+ force = false,
138
+ } = {}) {
139
+ if (!(await entryExists(skillSourceDir))) {
140
+ throw new CliError(
141
+ `Could not find the bundled skill at ${skillSourceDir}. ` +
142
+ "Is @pitchkit/react installed, and new enough to ship one?",
143
+ );
144
+ }
145
+
146
+ const skillsDir = isAbsolute(dir) ? dir : resolve(cwd, dir);
147
+ const destination = join(skillsDir, SKILL_NAME);
148
+
149
+ if (await entryExists(destination)) {
150
+ if (!force) {
151
+ throw new CliError(
152
+ `${relative(cwd, destination) || destination} already exists. Re-run with --force to replace it.`,
153
+ );
154
+ }
155
+ // `rm` unlinks a symlink rather than following it, so this can't reach
156
+ // through an existing link and delete the copy inside node_modules.
157
+ await rm(destination, { recursive: true, force: true });
158
+ }
159
+
160
+ await mkdir(skillsDir, { recursive: true });
161
+ const method = await linkOrCopy(destination);
162
+
163
+ // Plain codepoint sort, so SKILL.md leads and the references/ paths follow.
164
+ return { destination, method, files: (await listFiles(destination)).sort() };
165
+ }
166
+
167
+ async function listFiles(dir, prefix = "") {
168
+ const entries = await readdir(dir, { withFileTypes: true });
169
+ const files = [];
170
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
171
+ const name = prefix ? `${prefix}/${entry.name}` : entry.name;
172
+ if (entry.isDirectory()) {
173
+ files.push(...(await listFiles(join(dir, entry.name), name)));
174
+ } else {
175
+ files.push(name);
176
+ }
177
+ }
178
+ return files;
179
+ }
180
+
181
+ export async function run(argv, { cwd = process.cwd(), log = console.log } = {}) {
182
+ const options = parseArgs(argv);
183
+
184
+ if (options.help || options.command === null || options.command === "help") {
185
+ log(formatHelp());
186
+ return 0;
187
+ }
188
+
189
+ if (options.command === "path") {
190
+ log(skillSourceDir);
191
+ return 0;
192
+ }
193
+
194
+ if (options.command !== "install") {
195
+ throw new CliError(`Unknown command: ${options.command}\n\n${formatHelp()}`);
196
+ }
197
+
198
+ const { destination, method, files } = await installSkill({
199
+ cwd,
200
+ dir: options.dir,
201
+ force: options.force,
202
+ });
203
+ const shown = relative(cwd, destination) || destination;
204
+
205
+ if (method === "link") {
206
+ log(`Linked the PitchKit skill at ${shown}`);
207
+ } else {
208
+ log(`Copied the PitchKit skill to ${shown}`);
209
+ }
210
+ for (const file of files) log(` ${shown}/${file}`);
211
+ log("");
212
+
213
+ if (method === "link") {
214
+ log("It points into node_modules, so `npm update @pitchkit/react` updates the");
215
+ log("skill too. Add it to .gitignore unless you want the link committed.");
216
+ } else {
217
+ log("This filesystem wouldn't take a symlink, so the skill was copied instead —");
218
+ log("it's a snapshot. Re-run with --force after upgrading @pitchkit/react.");
219
+ }
220
+ log("Start a new agent session so it picks the skill up.");
221
+
222
+ return 0;
223
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ // The package's only bin, so `npx @pitchkit/react skills install` resolves to
3
+ // it despite the bin name not matching the (scoped) package name.
4
+ import { CliError, run } from "./install-skill.mjs";
5
+
6
+ try {
7
+ process.exitCode = await run(process.argv.slice(2));
8
+ } catch (error) {
9
+ if (error instanceof CliError) {
10
+ console.error(error.message);
11
+ process.exitCode = 1;
12
+ } else {
13
+ throw error;
14
+ }
15
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { CSSProperties, ReactNode } from 'react';
3
- import { PitchTypeId, Orientation, CropWindow, ViewportPadding, PitchAppearance, ScatterLayer, AnnotateLayer, ArrowsLayer, CometLayer, HeatmapLayer, PolygonLayer, ConvexHullLayer, VoronoiLayer, GoalAngleLayer, FlowLayer, FlowBin, PitchDimensions, Viewport, PixelTransform } from '@pitchkit/core';
3
+ import { PitchTypeId, Orientation, CropWindow, ViewportPadding, PitchAppearance, ScatterLayer, AnnotateLayer, ArrowsLayer, CometLayer, HeatmapLayer, PositionalHeatmapLayer, HexbinLayer, KdeLayer, PolygonLayer, ConvexHullLayer, VoronoiLayer, GoalAngleLayer, FlowLayer, FlowBin, PitchDimensions, Viewport, PixelTransform } from '@pitchkit/core';
4
4
 
5
5
  interface PitchProps {
6
6
  type: PitchTypeId;
@@ -78,26 +78,59 @@ interface HeatmapProps<T> extends Omit<HeatmapLayer<T>, "type"> {
78
78
  style?: CSSProperties;
79
79
  }
80
80
  /**
81
- * Client-only `<canvas>` heatmap, painted imperatively via core's
82
- * `renderHeatmapLayersToCanvas` in an effect. Unlike the SVG layer
83
- * components, this can't be re-emitted as JSX — Canvas has no declarative
84
- * JSX equivalent, and dense raster data is exactly the case core's hybrid
85
- * SVG+Canvas architecture (PRD §8.1) reserves Canvas for. The `<canvas>`
86
- * itself renders server-side (empty), but its pixels only appear after the
87
- * client effect runs — a documented client-only boundary.
81
+ * Client-only `<canvas>` heatmap: bins `data` into a uniform
82
+ * `binsX` x `binsY` grid and colours each cell by count (or summed
83
+ * `weight`). See `DensityCanvas` for why every Canvas layer is painted
84
+ * imperatively rather than re-emitted as JSX.
85
+ */
86
+ declare function Heatmap<T>({ className, style, ...layerProps }: HeatmapProps<T>): react.JSX.Element;
87
+
88
+ interface PositionalHeatmapProps<T> extends Omit<PositionalHeatmapLayer<T>, "type"> {
89
+ className?: string;
90
+ style?: CSSProperties;
91
+ }
92
+ /**
93
+ * Client-only `<canvas>` heatmap binned into Juego de Posición zones —
94
+ * mplsoccer's `bin_statistic_positional` + `heatmap_positional`. Same
95
+ * aggregation as `<Heatmap>`, but the cells come from the pitch markings
96
+ * (penalty areas, six-yard boxes, halfway line) rather than a uniform
97
+ * grid, which is what makes zone-to-zone comparisons meaningful to an
98
+ * analyst reading positional play.
88
99
  *
89
- * Wrapped in `<foreignObject>` since a raw `<canvas>` can't be a direct
90
- * child of `<svg>` — this keeps the heatmap inside the same SVG tree as
91
- * the pitch and every other layer, rather than needing a second,
92
- * separately-positioned DOM element the way the vanilla-JS core example
93
- * demo has to (core has no JSX to lean on there).
100
+ * Pass `stroke` to outline the zones off by default, since the
101
+ * boundaries compete with the pitch markings they're derived from.
102
+ */
103
+ declare function PositionalHeatmap<T>({ className, style, ...layerProps }: PositionalHeatmapProps<T>): react.JSX.Element;
104
+
105
+ interface HexbinProps<T> extends Omit<HexbinLayer<T>, "type"> {
106
+ className?: string;
107
+ style?: CSSProperties;
108
+ }
109
+ /**
110
+ * Client-only `<canvas>` hexagonal density — mplsoccer's `hexbin`.
111
+ * Hexagons pack more evenly than squares (every neighbour is equidistant),
112
+ * so dense touch/event data reads with less of the axis-aligned banding a
113
+ * rectangular `<Heatmap>` shows. Empty hexagons aren't drawn at all, so
114
+ * the pitch stays visible wherever there's no data.
115
+ */
116
+ declare function Hexbin<T>({ className, style, ...layerProps }: HexbinProps<T>): react.JSX.Element;
117
+
118
+ interface KDEProps<T> extends Omit<KdeLayer<T>, "type"> {
119
+ className?: string;
120
+ style?: CSSProperties;
121
+ }
122
+ /**
123
+ * Client-only `<canvas>` kernel density estimate — mplsoccer's `kdeplot`.
124
+ * Unlike the binned layers, each point spreads influence over its
125
+ * neighbourhood, so the result is a continuous surface rather than a grid
126
+ * of cells, and low-density areas fade out instead of being filled with
127
+ * `colorMin`.
94
128
  *
95
- * No effect dependency array (runs after every render) rather than
96
- * enumerating every spread `props` field plus `dimensions`/`viewport`
97
- * repainting a canvas is cheap and idempotent, so this avoids a fragile,
98
- * easily-stale dependency list.
129
+ * `bandwidth` controls the smoothing radius in provider units; leave it
130
+ * unset to let Silverman's rule of thumb pick one per axis from the data's
131
+ * own spread.
99
132
  */
100
- declare function Heatmap<T>({ className, style, ...layerProps }: HeatmapProps<T>): react.JSX.Element;
133
+ declare function KDE<T>({ className, style, ...layerProps }: KDEProps<T>): react.JSX.Element;
101
134
 
102
135
  interface PolygonProps<T> extends Omit<PolygonLayer<T>, "type"> {
103
136
  tooltip?: (d: T, i: number) => ReactNode;
@@ -175,4 +208,4 @@ interface PitchContextValue {
175
208
  */
176
209
  declare function usePitch(): Pick<PitchContextValue, "dimensions" | "viewport" | "transform">;
177
210
 
178
- export { Annotate, type AnnotateProps, Arrows, type ArrowsProps, Comet, type CometProps, ConvexHull, type ConvexHullProps, Flow, type FlowProps, GoalAngle, type GoalAngleProps, Heatmap, type HeatmapProps, Pitch, type PitchProps, Polygon, type PolygonProps, Scatter, type ScatterProps, type TooltipState, VerticalPitch, Voronoi, type VoronoiProps, usePitch };
211
+ export { Annotate, type AnnotateProps, Arrows, type ArrowsProps, Comet, type CometProps, ConvexHull, type ConvexHullProps, Flow, type FlowProps, GoalAngle, type GoalAngleProps, Heatmap, type HeatmapProps, Hexbin, type HexbinProps, KDE, type KDEProps, Pitch, type PitchProps, Polygon, type PolygonProps, PositionalHeatmap, type PositionalHeatmapProps, Scatter, type ScatterProps, type TooltipState, VerticalPitch, Voronoi, type VoronoiProps, usePitch };