football-docs 0.1.2 → 0.2.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.
@@ -0,0 +1,280 @@
1
+ # DataBallPy Usage Guide
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ pip install databallpy
7
+
8
+ # With kloppy integration (expands provider support)
9
+ pip install 'databallpy[kloppy]'
10
+
11
+ # With dangerous accessible space metric
12
+ pip install 'databallpy[accessible-space]'
13
+ ```
14
+
15
+ ## Loading Data
16
+
17
+ ### From Files with Native Parsers
18
+
19
+ ```python
20
+ from databallpy import get_game
21
+
22
+ # Tracab tracking + Opta events (most common combo)
23
+ game = get_game(
24
+ tracking_data_loc="../data/tracking_data.dat",
25
+ tracking_metadata_loc="../data/tracking_metadata.xml",
26
+ tracking_data_provider="tracab",
27
+ event_data_loc="../data/event_data_f24.xml",
28
+ event_metadata_loc="../data/event_metadata_f7.xml",
29
+ event_data_provider="opta",
30
+ )
31
+
32
+ # StatsBomb events only (requires extra files)
33
+ game = get_game(
34
+ event_data_loc="events.json",
35
+ event_match_loc="match.json",
36
+ event_lineup_loc="lineup.json",
37
+ event_data_provider="statsbomb",
38
+ )
39
+
40
+ # Metrica tracking + events
41
+ game = get_game(
42
+ tracking_data_loc="tracking_home.csv",
43
+ tracking_metadata_loc="metadata.xml",
44
+ tracking_data_provider="metrica",
45
+ event_data_loc="events.json",
46
+ event_data_provider="metrica",
47
+ )
48
+ ```
49
+
50
+ ### From Kloppy Datasets
51
+
52
+ ```python
53
+ from kloppy import sportec
54
+ from databallpy import get_game_from_kloppy
55
+
56
+ tracking_dataset = sportec.load_open_tracking_data(match_id="J03WPY", only_alive=False)
57
+ event_dataset = sportec.load_open_event_data(match_id="J03WPY")
58
+ game = get_game_from_kloppy(
59
+ tracking_dataset=tracking_dataset,
60
+ event_dataset=event_dataset
61
+ )
62
+ ```
63
+
64
+ ### Open Data (DFL/Bundesliga)
65
+
66
+ ```python
67
+ from databallpy import get_open_game
68
+
69
+ # 7 available matches (2 Bundesliga 1 + 5 Bundesliga 2)
70
+ game = get_open_game(provider="sportec", game_id="J03WMX") # Köln vs Bayern
71
+ ```
72
+
73
+ ### Save and Load Games
74
+
75
+ ```python
76
+ # Save (creates parquet + JSON files)
77
+ game.save_game(name="my_game", path="/data")
78
+
79
+ # Load
80
+ from databallpy import get_saved_game
81
+ game = get_saved_game(name="my_game", path="/data")
82
+ ```
83
+
84
+ ## Accessing Data
85
+
86
+ ```python
87
+ # DataFrames
88
+ game.tracking_data # TrackingData (custom DataFrame)
89
+ game.event_data # EventData (custom DataFrame)
90
+ game.shot_events # DataFrame of ShotEvent objects
91
+ game.pass_events # DataFrame of PassEvent objects
92
+ game.dribble_events # DataFrame of DribbleEvent objects
93
+
94
+ # Player column IDs (for tracking data)
95
+ home_col_ids = game.get_column_ids(team="home")
96
+ defenders = game.get_column_ids(team="home", positions=["defender"])
97
+
98
+ # Get specific frames
99
+ frame_data = game.get_frames(12345, playing_direction="team_oriented")
100
+ frame_data = game.get_frames(12345, playing_direction="possession_oriented")
101
+
102
+ # Get event and its tracking frame
103
+ event = game.get_event(event_id=42)
104
+ frame = game.get_event_frame(event_id=42)
105
+
106
+ # Match metadata
107
+ game.home_team_name, game.away_team_name
108
+ game.home_score, game.away_score
109
+ game.home_formation, game.away_formation
110
+ game.pitch_dimensions # [length, width] in metres
111
+ ```
112
+
113
+ ## Synchronization
114
+
115
+ Aligns tracking and event data using the Needleman-Wunsch algorithm. After sync, tracking data gets `event_id`, `databallpy_event`, `sync_certainty` columns; event data gets `tracking_frame`, `sync_certainty` columns.
116
+
117
+ ```python
118
+ game.synchronise_tracking_and_event_data(
119
+ n_batches="smart", # or int for manual batch count
120
+ verbose=True,
121
+ offset=1, # seconds offset for alignment
122
+ )
123
+
124
+ # Check sync status
125
+ game.is_synchronised # bool
126
+ ```
127
+
128
+ Quality checks must pass for sync to be allowed (kick-off near origin, ball alive >50% of game, player positions within bounds).
129
+
130
+ ## Preprocessing
131
+
132
+ ### Filtering Tracking Data
133
+
134
+ ```python
135
+ game.tracking_data.filter_tracking_data(
136
+ column_ids=home_col_ids,
137
+ filter_type="savitzky_golay",
138
+ window_length=7,
139
+ polyorder=2,
140
+ )
141
+ ```
142
+
143
+ ### Velocity and Acceleration
144
+
145
+ ```python
146
+ # Add velocity columns (_vx, _vy)
147
+ game.tracking_data.add_velocity(
148
+ column_ids=home_col_ids,
149
+ filter_type=None, # optional smoothing
150
+ max_velocity=np.inf,
151
+ )
152
+
153
+ # Add acceleration columns (_ax, _ay)
154
+ game.tracking_data.add_acceleration(
155
+ column_ids=home_col_ids,
156
+ filter_type=None,
157
+ max_acceleration=np.inf,
158
+ )
159
+ ```
160
+
161
+ ## Analytics and Metrics
162
+
163
+ ### Covered Distance (by velocity/acceleration zones)
164
+
165
+ ```python
166
+ result_df = game.tracking_data.get_covered_distance(
167
+ column_ids=home_col_ids,
168
+ velocity_intervals=((0, 5), (5, 20)), # m/s
169
+ acceleration_intervals=((2, np.inf),), # m/s²
170
+ )
171
+ ```
172
+
173
+ ### Pressure (Herold & Kempe 2022)
174
+
175
+ ```python
176
+ pressure = game.tracking_data.get_pressure_on_player(
177
+ index=12345, # frame index
178
+ column_id="home_1", # player to measure pressure on
179
+ pitch_size=game.pitch_dimensions,
180
+ d_front="variable", # or float
181
+ d_back=3.0,
182
+ q=1.75,
183
+ )
184
+ ```
185
+
186
+ ### Team Possession (from synchronized events)
187
+
188
+ ```python
189
+ game.tracking_data.add_team_possession(game.event_data, game.home_team_id)
190
+ ```
191
+
192
+ ### Individual Player Possession (Vidal-Codina et al. 2022)
193
+
194
+ ```python
195
+ game.tracking_data.add_individual_player_possession(
196
+ pz_radius=1.5,
197
+ bv_threshold=5.0,
198
+ ba_threshold=10.0,
199
+ min_frames_pz=0,
200
+ )
201
+ ```
202
+
203
+ ### Pitch Control (Gaussian influence model)
204
+
205
+ ```python
206
+ pitch_control = game.tracking_data.get_pitch_control(
207
+ pitch_dimensions=game.pitch_dimensions,
208
+ n_x_bins=106,
209
+ n_y_bins=68,
210
+ )
211
+ ```
212
+
213
+ ### Voronoi Space Occupation
214
+
215
+ ```python
216
+ distances, assigned = game.tracking_data.get_approximate_voronoi(
217
+ pitch_dimensions=game.pitch_dimensions,
218
+ n_x_bins=106,
219
+ n_y_bins=68,
220
+ )
221
+ ```
222
+
223
+ ### Dangerous Accessible Space
224
+
225
+ Requires `pip install 'databallpy[accessible-space]'`:
226
+
227
+ ```python
228
+ game.tracking_data.add_dangerous_accessible_space(mask=None)
229
+ ```
230
+
231
+ ## Visualization
232
+
233
+ ```python
234
+ from databallpy.visualize import (
235
+ plot_soccer_pitch, plot_tracking_data, plot_events, save_tracking_video
236
+ )
237
+
238
+ # Plot empty pitch
239
+ fig, ax = plot_soccer_pitch(field_dimen=(106.0, 68.0), pitch_color="mediumseagreen")
240
+
241
+ # Plot single tracking frame
242
+ fig, ax = plot_tracking_data(
243
+ game, idx=12345,
244
+ team_colors=["green", "red"],
245
+ events=["pass", "shot"],
246
+ add_velocities=True,
247
+ add_player_possession=True,
248
+ heatmap_overlay=some_array,
249
+ )
250
+
251
+ # Plot events on pitch
252
+ fig, ax = plot_events(game, events=["shot"], outcome=True, team_id=game.home_team_id)
253
+
254
+ # Save video clip (requires ffmpeg)
255
+ save_tracking_video(
256
+ game, start_idx=1000, end_idx=2000,
257
+ save_folder="./clips", title="goal_clip",
258
+ events=["shot"],
259
+ )
260
+ ```
261
+
262
+ ## Video Analysis Export
263
+
264
+ Export events to SportsCode/Longomatch-compatible XML:
265
+
266
+ ```python
267
+ xml_str = game.event_data.to_video_analysis_xml(
268
+ databallpy_events=["shot", "pass"],
269
+ before_seconds=3.0,
270
+ after_seconds=3.0,
271
+ )
272
+ ```
273
+
274
+ ## Long Format Conversion
275
+
276
+ Convert tracking data from wide format (one column per player axis) to long format (one row per frame/player):
277
+
278
+ ```python
279
+ long_df = game.tracking_data.to_long_format()
280
+ ```
@@ -0,0 +1,25 @@
1
+ # mplsoccer
2
+
3
+ ## Overview
4
+
5
+ mplsoccer is a Python library for plotting soccer/football charts using Matplotlib and loading StatsBomb open data. Created by Andrew Rowlinson and Anmol Durgapal, licensed under MIT. Current version: 1.6.1.
6
+
7
+ - **Install**: `pip install mplsoccer` or `conda install -c conda-forge mplsoccer`
8
+ - **Dependencies**: matplotlib>=3.6, numpy, pandas, pillow, requests, scipy, seaborn
9
+
10
+ ## Key Features
11
+
12
+ **Pitch-based visualizations**: scatter plots, heatmaps, hexbin, KDE plots, arrows (pass maps), comet lines, flow maps, Voronoi diagrams, Delaunay tessellation, convex hulls, polygons, goal angle visualization, pass networks, shot freeze frames, formations, Juego de Posición heatmaps, pass sonars, animations.
13
+
14
+ **Non-pitch chart types**: radar charts (`Radar`), pizza/Nightingale charts (`PyPizza`), bumpy charts (`Bumpy`), turbine charts.
15
+
16
+ **Utilities**: `FontManager` (Google Fonts), `Standardizer` (coordinate conversion), `add_image`/`inset_image`, `grid()`/`jointgrid()` layout helpers, `Sbopen`/`Sbapi`/`Sblocal` (StatsBomb data loading).
17
+
18
+ ## Limitations and Caveats
19
+
20
+ - **Y-axis inversion varies by provider**: StatsBomb, Wyscout, and Metrica have inverted y-axes. mplsoccer handles this internally.
21
+ - **Tracking data pitch types require explicit dimensions**: `tracab`, `metricasports`, `skillcorner`, `secondspectrum`, and `custom` all need `pitch_length` and `pitch_width`.
22
+ - **Wyscout goal width discrepancy**: mplsoccer uses ggsoccer's 12 units (vs socceraction's 10 units).
23
+ - **Opta vs Wyscout**: Both use 0-100 coordinates but Wyscout has inverted y-axis. Easy to mix up.
24
+ - **Matplotlib-based**: Performance for very large datasets or interactive use can be limited.
25
+ - **No built-in data fetching beyond StatsBomb**: For Opta, Wyscout, etc., load data yourself; mplsoccer only handles visualization and coordinate standardization.
@@ -0,0 +1,88 @@
1
+ # mplsoccer Pitch Types and Coordinate Systems
2
+
3
+ ## Supported Pitch Types
4
+
5
+ | Pitch Type | Provider | X Range | Y Range | Dimensions Required | Notes |
6
+ |---|---|---|---|---|---|
7
+ | `statsbomb` (default) | StatsBomb | 0-120 | 80-0 (inverted y) | No (fixed) | Default pitch type |
8
+ | `opta` | Stats Perform / Opta | 0-100 | 0-100 | No (fixed) | Also used by Sofascore, WhoScored |
9
+ | `wyscout` | Wyscout / Hudl | 0-100 | 0-100 (inverted y) | No (fixed) | Similar to Opta but y-inverted |
10
+ | `tracab` | TRACAB / ChyronHego | Centered, cm | Centered, cm | Yes | Values in centimetres |
11
+ | `uefa` | UEFA | 0-105 | 0-68 | No (fixed) | Standard 105m x 68m |
12
+ | `metricasports` | Metrica Sports | 0-1 | 0-1 (inverted y) | Yes | Normalised coordinates |
13
+ | `skillcorner` | SkillCorner | Centered, m | Centered, m | Yes | Values in metres |
14
+ | `secondspectrum` | Second Spectrum | Centered, m | Centered, m | Yes | Values in metres |
15
+ | `impect` | Impect | -52.5 to 52.5 | -34 to 34 | No (fixed) | Centered |
16
+ | `custom` | Any | 0 to pitch_length | 0 to pitch_width | Yes | Fully configurable |
17
+
18
+ Tracking data providers (`tracab`, `metricasports`, `skillcorner`, `secondspectrum`, `custom`) require explicit `pitch_length` and `pitch_width` (typically 105 and 68).
19
+
20
+ ## Pitch Constructor
21
+
22
+ ```python
23
+ from mplsoccer import Pitch, VerticalPitch
24
+
25
+ # Basic pitch
26
+ pitch = Pitch(pitch_type='statsbomb')
27
+
28
+ # Tracking data pitch (requires dimensions)
29
+ pitch = Pitch(pitch_type='tracab', pitch_length=105, pitch_width=68)
30
+
31
+ # Customisation
32
+ pitch = Pitch(
33
+ pitch_type='statsbomb',
34
+ pitch_color='grass', # 'grass' or any color
35
+ line_color='white',
36
+ stripe=True, # alternating grass stripes
37
+ stripe_color='#c2d59d',
38
+ half=True, # show only half pitch
39
+ goal_type='box', # 'line', 'box', 'circle', or None
40
+ positional=True, # Juego de Posición zone lines
41
+ corner_arcs=True,
42
+ pad_left=5, pad_right=5, # padding in data units
43
+ pad_bottom=5, pad_top=5,
44
+ )
45
+
46
+ # Vertical orientation (rotated 90°)
47
+ vpitch = VerticalPitch(pitch_type='opta')
48
+ ```
49
+
50
+ ## Coordinate Standardization
51
+
52
+ The `Standardizer` converts coordinates between any two pitch types by mapping pitch markings (penalty areas, six-yard boxes, etc.) -- not just linear scaling. This is more accurate for event data but means the mapping is piecewise-linear.
53
+
54
+ ```python
55
+ from mplsoccer import Standardizer
56
+
57
+ # Convert Opta (0-100) to StatsBomb (120x80)
58
+ std = Standardizer(pitch_from='opta', pitch_to='statsbomb')
59
+ x_new, y_new = std.transform(x, y)
60
+
61
+ # Reverse direction
62
+ x_orig, y_orig = std.transform(x_new, y_new, reverse=True)
63
+
64
+ # For tracking data, specify dimensions
65
+ std = Standardizer(
66
+ pitch_from='tracab', pitch_to='statsbomb',
67
+ length_from=105, width_from=68
68
+ )
69
+ ```
70
+
71
+ ## Custom Dimensions
72
+
73
+ For normalised ML coordinate systems, use `center_scale_dims()`:
74
+
75
+ ```python
76
+ from mplsoccer.dimensions import center_scale_dims
77
+
78
+ # Create a -1 to 1 coordinate space
79
+ custom_dims = center_scale_dims(
80
+ pitch_width=68, pitch_length=105,
81
+ width=2, length=2,
82
+ invert_y=False
83
+ )
84
+
85
+ pitch = Pitch(pitch_type=custom_dims)
86
+ ```
87
+
88
+ For fully custom dimensions, subclass `BaseSoccerDims`.
@@ -0,0 +1,280 @@
1
+ # mplsoccer Visualizations
2
+
3
+ ## Drawing a Pitch
4
+
5
+ ```python
6
+ from mplsoccer import Pitch, VerticalPitch
7
+
8
+ pitch = Pitch()
9
+ fig, ax = pitch.draw()
10
+
11
+ # Grid of pitches
12
+ fig, axs = pitch.draw(nrows=2, ncols=3)
13
+
14
+ # On existing axes
15
+ pitch.draw(ax=existing_ax)
16
+ ```
17
+
18
+ All plotting methods are called on the pitch object, passing `ax=ax` as a parameter.
19
+
20
+ ## Scatter Plots (Shot Maps, Player Positions)
21
+
22
+ ```python
23
+ pitch.scatter(x, y, ax=ax, s=100, c='red', edgecolors='black', zorder=2)
24
+
25
+ # With rotation (e.g., for player orientation arrows)
26
+ pitch.scatter(x, y, rotation_degrees=angle, ax=ax)
27
+
28
+ # Football marker
29
+ pitch.scatter(x, y, marker='football', ax=ax)
30
+ ```
31
+
32
+ ## Heatmaps (Binned Statistics)
33
+
34
+ Two-step workflow: bin the data, then plot.
35
+
36
+ ```python
37
+ # Step 1: Bin the data
38
+ bin_statistic = pitch.bin_statistic(x, y, statistic='count', bins=(25, 25))
39
+
40
+ # Optional: Gaussian smoothing
41
+ from scipy.ndimage import gaussian_filter
42
+ bin_statistic['statistic'] = gaussian_filter(bin_statistic['statistic'], sigma=1)
43
+
44
+ # Step 2: Plot
45
+ pcm = pitch.heatmap(bin_statistic, ax=ax, cmap='hot', edgecolors='#22312b')
46
+
47
+ # Optional: Add labels
48
+ pitch.label_heatmap(bin_statistic, ax=ax, str_format='{:.0f}', color='white',
49
+ fontsize=8, exclude_zeros=True)
50
+ ```
51
+
52
+ ## Juego de Posición Heatmaps
53
+
54
+ Positional play zones (used by Pep Guardiola-style analysis):
55
+
56
+ ```python
57
+ bin_statistic = pitch.bin_statistic_positional(x, y, statistic='count')
58
+ pitch.heatmap_positional(bin_statistic, ax=ax, cmap='coolwarm', edgecolors='#22312b')
59
+ pitch.label_heatmap(bin_statistic, ax=ax, str_format='{:.0f}')
60
+ ```
61
+
62
+ ## Hexbin Plots
63
+
64
+ ```python
65
+ pitch.hexbin(x, y, ax=ax, gridsize=20, cmap='Reds', edgecolors='grey')
66
+ ```
67
+
68
+ NaN values are filtered out internally.
69
+
70
+ ## KDE Plots (Kernel Density Estimation)
71
+
72
+ Uses seaborn internally, auto-clipped to pitch boundaries.
73
+
74
+ ```python
75
+ pitch.kdeplot(x, y, ax=ax, fill=True, levels=100, cmap='Reds', thresh=0)
76
+ ```
77
+
78
+ ## Arrows (Pass Maps)
79
+
80
+ ```python
81
+ pitch.arrows(xstart, ystart, xend, yend, ax=ax,
82
+ color='blue', width=2, headwidth=5, headlength=5)
83
+ ```
84
+
85
+ ## Lines and Comet Lines
86
+
87
+ ```python
88
+ # Standard lines
89
+ pitch.lines(xstart, ystart, xend, yend, ax=ax, lw=2, color='blue')
90
+
91
+ # Comet lines (transparency gradient from start to end)
92
+ pitch.lines(xstart, ystart, xend, yend, ax=ax, comet=True,
93
+ transparent=True, alpha_start=0.01, alpha_end=1.0)
94
+
95
+ # Comet with colormap
96
+ pitch.lines(xstart, ystart, xend, yend, ax=ax, comet=True,
97
+ cmap='plasma', n_segments=100)
98
+ ```
99
+
100
+ Note: comet lines split each line into `n_segments` (default 100). Large datasets may be slow.
101
+
102
+ ## Flow Maps
103
+
104
+ Binned average direction and distance as arrows:
105
+
106
+ ```python
107
+ pitch.flow(xstart, ystart, xend, yend, ax=ax,
108
+ bins=(6, 4), arrow_type='same', arrow_length=10, color='blue')
109
+ ```
110
+
111
+ ## Voronoi Diagrams
112
+
113
+ ```python
114
+ team1, team2 = pitch.voronoi(x, y, teams)
115
+ pitch.polygon(team1, ax=ax, fc='red', alpha=0.3, ec='black')
116
+ pitch.polygon(team2, ax=ax, fc='blue', alpha=0.3, ec='black')
117
+ ```
118
+
119
+ Uses a reflection trick to extend Voronoi cells to pitch edges. Players outside pitch boundaries are clipped.
120
+
121
+ ## Convex Hulls
122
+
123
+ ```python
124
+ hull = pitch.convexhull(x, y)
125
+ pitch.polygon(hull, ax=ax, facecolor='cornflowerblue', alpha=0.3)
126
+ ```
127
+
128
+ ## Delaunay Tessellation
129
+
130
+ ```python
131
+ pitch.triplot(x, y, ax=ax, color='blue', linewidth=2)
132
+ ```
133
+
134
+ ## Goal Angle Visualization
135
+
136
+ ```python
137
+ pitch.goal_angle(x, y, ax=ax, goal='right', alpha=0.3, color='red')
138
+ ```
139
+
140
+ ## Pass Sonars
141
+
142
+ ```python
143
+ bin_statistic = pitch.bin_statistic_sonar(x, y, angle, bins=(6, 4, 8))
144
+ pitch.sonar(bin_statistic, ax=ax, fc='cornflowerblue')
145
+ pitch.sonar_grid(bin_statistic, ax=ax)
146
+ ```
147
+
148
+ ## Radar Charts
149
+
150
+ ```python
151
+ from mplsoccer import Radar
152
+
153
+ radar = Radar(
154
+ params=['Goals', 'Assists', 'xG', 'xA', 'Passes'],
155
+ min_range=[0, 0, 0, 0, 0],
156
+ max_range=[30, 20, 25, 15, 3000],
157
+ lower_is_better=['Miscontrol'], # invert specific params
158
+ num_rings=4,
159
+ ring_width=1,
160
+ center_circle_radius=2,
161
+ )
162
+
163
+ fig, ax = radar.setup_axis()
164
+ radar.draw_circles(ax=ax)
165
+ rings_inner, rings_outer, vertices = radar.draw_radar(
166
+ values, ax=ax, kwargs_radar={'facecolor': '#aa65b2', 'alpha': 0.6}
167
+ )
168
+ radar.draw_range_labels(ax=ax)
169
+ radar.draw_param_labels(ax=ax)
170
+
171
+ # Comparison radar (two players)
172
+ radar.draw_radar_compare(values1, values2, ax=ax)
173
+
174
+ # Multi-player (3+) -- stacked solid fills
175
+ radar.draw_radar_solid(values_list, ax=ax)
176
+ ```
177
+
178
+ ## Pizza Charts (Percentile Ranks)
179
+
180
+ ```python
181
+ from mplsoccer import PyPizza
182
+
183
+ baker = PyPizza(
184
+ params=params,
185
+ straight_line_color="#000000",
186
+ straight_line_lw=1,
187
+ last_circle_lw=1,
188
+ other_circle_lw=1,
189
+ other_circle_ls="-.",
190
+ inner_circle_size=20,
191
+ )
192
+
193
+ fig, ax = baker.make_pizza(
194
+ values, figsize=(8, 8), param_location=110,
195
+ kwargs_slices=dict(facecolor='cornflowerblue', edgecolor='black'),
196
+ kwargs_params=dict(color='black', fontsize=12),
197
+ kwargs_values=dict(color='black', fontsize=12),
198
+ )
199
+ ```
200
+
201
+ ## Bumpy Charts (Rank Changes)
202
+
203
+ ```python
204
+ from mplsoccer import Bumpy
205
+
206
+ bumpy = Bumpy(
207
+ scatter_color='#282828',
208
+ line_color='#252525',
209
+ rotate_xticks=90,
210
+ ticklabel_size=12,
211
+ )
212
+
213
+ fig, ax = bumpy.plot(
214
+ x_list=matchweeks, y_list=rankings, values=team_names,
215
+ highlight_dict={'Arsenal': 'red'},
216
+ figsize=(16, 10),
217
+ )
218
+ ```
219
+
220
+ ## Layout Helpers
221
+
222
+ ### Grid (title + pitch + endnote)
223
+
224
+ ```python
225
+ fig, axs = pitch.grid(figheight=10, title_height=0.06, endnote_height=0.03)
226
+ # axs is a dict: axs['pitch'], axs['title'], axs['endnote']
227
+ axs['title'].text(0.5, 0.5, 'My Title', ha='center', va='center')
228
+ ```
229
+
230
+ ### Joint Grid (pitch + marginal axes)
231
+
232
+ ```python
233
+ fig, axs = pitch.jointgrid(ax_top=True, ax_right=True)
234
+ # axs['pitch'], axs['top'], axs['right'], axs['title'], axs['endnote']
235
+ ```
236
+
237
+ ## StatsBomb Data Loading
238
+
239
+ ```python
240
+ from mplsoccer import Sbopen
241
+
242
+ parser = Sbopen()
243
+
244
+ # Browse available data
245
+ df_competition = parser.competition()
246
+ df_match = parser.match(competition_id=11, season_id=1)
247
+
248
+ # Load match data
249
+ df_lineup = parser.lineup(match_id)
250
+ df_event, df_related, df_freeze, df_tactics = parser.event(match_id)
251
+
252
+ # StatsBomb 360 data
253
+ frames, visible = parser.frame(match_id)
254
+ ```
255
+
256
+ Also available: `Sbapi` (for API access with credentials) and `Sblocal` (for local files).
257
+
258
+ ## Utility Functions
259
+
260
+ ```python
261
+ from mplsoccer import FontManager, add_image
262
+
263
+ # Load Google Fonts
264
+ fm = FontManager('https://raw.githubusercontent.com/google/fonts/main/ofl/roboto/Roboto-Regular.ttf')
265
+
266
+ # Add image/logo to plot
267
+ add_image(image, fig, left=0.1, bottom=0.9, width=0.1)
268
+
269
+ # Inset axes on pitch (e.g., for mini charts at player positions)
270
+ inset_ax = pitch.inset_axes(x, y, width=10, height=10, ax=ax)
271
+
272
+ # Inset image on pitch
273
+ pitch.inset_image(x, y, image, width=10, ax=ax)
274
+
275
+ # Flip coordinates to other half
276
+ x_flip, y_flip = pitch.flip_side(x, y, flip=True)
277
+
278
+ # Calculate angle and distance
279
+ angle, distance = pitch.calculate_angle_and_distance(x1, y1, x2, y2, degrees=True)
280
+ ```