@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 +21 -0
- package/README.md +173 -0
- package/bin/install-skill.mjs +223 -0
- package/bin/pitchkit.mjs +15 -0
- package/dist/index.d.ts +52 -19
- package/dist/index.js +97 -15
- package/dist/index.js.map +1 -1
- package/package.json +29 -2
- package/skills/pitchkit/SKILL.md +384 -0
- package/skills/pitchkit/references/api.md +235 -0
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
|
+
[](https://www.npmjs.com/package/@pitchkit/react)
|
|
4
|
+
[](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
|
+
}
|
package/bin/pitchkit.mjs
ADDED
|
@@ -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
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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
|
|
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 };
|