Pitch Examples#
[1]:
from penaltyblog.viz import Pitch
from penaltyblog.matchflow import Flow, where_equals, get_field
from IPython.display import HTML
import plotly.io as pio
flow = (
Flow
.statsbomb
.events(22912)
.cache()
)
/Users/martin/repos/penaltyblog/venv/lib/python3.13/site-packages/statsbombpy/api_client.py:21: NoAuthWarning: credentials were not supplied. open data access only
warnings.warn(
Scatter Plot#
The scatter plot is the most versatile and commonly used chart for visualizing individual events or locations on the pitch. Each marker represents a single data point - such as a shot, pass, or player position - plotted using its (x, y) coordinates. This type of chart is ideal for exploring patterns, clusters, or areas of activity. You can customize marker size, color, and tooltip content to highlight specific aspects of the data. Hover over the dots to reveal interactive tooltips with
additional context, such as player names, event types, or timestamps. Note that by default, the tooltips will only show the x and y coordinates of the marker.
[2]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="right",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
shots = (
flow
.filter(where_equals("type.name", "Shot"))
.filter(where_equals("team.name", "Liverpool"))
)
pitch.plot_scatter(shots, "location.0", "location.1")
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
/Users/martin/repos/penaltyblog/venv/lib/python3.13/site-packages/statsbombpy/api_client.py:21: NoAuthWarning:
credentials were not supplied. open data access only
[2]:
Heatmap#
The heatmap chart shows the distribution or intensity of activity across different areas of the pitch. It works by dividing the field into a grid of rectangular bins and counting how many events (e.g. passes, touches, defensive actions) fall into each one. This makes it easy to identify hotspots - areas where play is concentrated or where certain players are most active. Heatmaps are particularly useful for summarizing large datasets at a glance. Hover over each cell to see the exact count of events and the corresponding location on the pitch.
[3]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
passes = flow.filter(where_equals("type.name", "Pass"))
pitch.plot_heatmap(
passes,
x="location.0",
y="location.1",
colorscale="Viridis",
opacity=0.6,
)
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[3]:
KDE Plot#
The KDE (Kernel Density Estimate) chart provides a smooth, continuous visualization of where events are most concentrated on the pitch. Unlike a heatmap, which uses fixed-size grid bins, the KDE computes a probability density surface by estimating how densely points are clustered in different regions. This results in a more fluid, contour-like view of player movement, shot locations, or ball recoveries. It’s especially useful for visualizing tendencies or patterns in noisy or unevenly distributed data. Hover over any area to see the estimated density value at that point.
[4]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
passes = flow.filter(where_equals("type.name", "Pass"))
pitch.plot_kde(
passes,
x="location.0",
y="location.1",
colorscale="Viridis",
)
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[4]:
Arrow Plot#
The plot_arrows method is used to visualize directional movement between two points—such as passes, runs, or ball progressions—by drawing arrows from a start (x, y) to an end (x2, y2) location. This provides a clear, intuitive way to represent flows of play or tactical patterns on the pitch. Arrows can be styled with adjustable width, head size, and color. However, due to Plotly’s rendering limitations, these arrows are implemented as annotations and may not support interactive hover
tooltips in all environments. For fully interactive arrows with hover text, consider using plot_comets as an alternative, or overlay a scatter chart.
[5]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
arrows = Flow.from_records([
{"x": 10, "y": 20, "x2": 40, "y2": 50},
{"x": 60, "y": 30, "x2": 80, "y2": 60}
])
pitch.plot_arrows(
arrows,
width=3,
)
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[5]:
Comet Plot#
The plot_comets method is designed to show movement between two points using fading trail lines - ideal for illustrating passes, runs, or ball trajectories with a sense of motion. Each “comet” is drawn as a series of line segments that fade from opaque (start) to transparent (end), mimicking momentum or direction. You can customize the number of segments, line width, color, and whether the trail fades. Unlike plot_arrows, comets are built using Plotly Scatter traces, so they fully
support interactive hover tooltips — even in static exports or notebook environments. This makes them a great choice when you want directional lines and interactivity.
[6]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
arrows = Flow.from_records([
{"x": 10, "y": 20, "x2": 40, "y2": 50, "player": "Daniella"},
{"x": 60, "y": 30, "x2": 80, "y2": 60, "player": "Natalie"}
])
pitch.plot_comets(
arrows,
width=5,
hover="player",
)
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[6]:
Overlaying Charts#
One of the key strengths of the Pitch class is its ability to layer multiple charts on top of each other. This makes it easy to combine different types of visual information in a single, coherent view - for example, overlaying a scatter plot of shots on top of a heatmap of overall activity. Each plot is added to a named layer, which can be reordered, hidden, or removed independently. This layering system is especially useful for building rich tactical visualizations that show both patterns
and specific events at once.
[7]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
passes = flow.filter(where_equals("type.name", "Pass"))
pitch.plot_heatmap(
passes,
x="location.0",
y="location.1",
colorscale="Viridis",
)
pitch.plot_scatter(
passes,
x="location.0",
y="location.1",
size=2,
color="white",
)
# NOTE: normally we'd just call `pitch.show()` here, but since
# we're exporting to HTML docs, we need to use `HTML` to export
# the plot
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[7]:
Themes#
The Pitch class supports a range of built-in themes that control the visual style of your charts, including pitch color, line styling, marker colors, fonts, and hover tooltip appearance. Themes like "classic", "night", "retro", "minimal", and "turf" provide distinct aesthetics suited to different presentation styles or branding needs. You can switch themes with a single parameter, or define your own by passing a custom dictionary of style values. Themes ensure your
visualizations look polished and consistent with minimal effort, whether you’re building reports, presentations, or interactive dashboards.
Minimal Theme#
[8]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="minimal",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[8]:
Night Theme#
[9]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[9]:
Classic Theme#
[10]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="classic",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[10]:
Turf#
[11]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="turf",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[11]:
Retro#
[12]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme="retro",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[12]:
Custom Themes#
In addition to the built-in themes, you can create your own custom theme by passing a dictionary of style settings to the Theme.from_dict() method. Custom themes are built on top of a base theme (e.g., "minimal" or "classic"), so you only need to specify the styles you want to change. This makes it easy to tweak one or more aspects of the appearance without redefining everything. You can control background color, line color, marker size, font family, tooltip styling, and more. Custom
themes are especially useful for aligning visuals with a specific color palette, branding guidelines, or publication style. Once created, your custom theme can be passed to the Pitch constructor just like a preset, making it easy to reuse across multiple visualizations.
[13]:
from penaltyblog.viz import Theme
# Define your custom theme
my_theme = Theme.from_dict({
"pitch_color": "#fdf6e3",
"line_color": "#657b83",
"marker_color": "#ff69b4",
"heatmap_colorscale": "YlOrRd",
"heatmap_opacity": 0.7,
"font_family": "Georgia, serif",
"line_width": 1.2,
"marker_size": 10,
"spot_size": 6,
"hover_bgcolor": "#eee8d5",
"hover_font_color": "#073642",
"hover_border_color": "#93a1a1",
"hover_font_size": 14,
"title_margin": 40,
"subtitle_margin": 20,
"subnote_margin": 30,
}, base="minimal")
# Use the theme with a pitch
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="full",
theme=my_theme,
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[13]:
Hover Tooltips#
You can customize the hover tooltips shown on the pitch by specifying which data field should appear when users hover over a chart element. Most plotting methods (such as plot_scatter, plot_comets, and plot_arrows) accept a hover argument, which takes the name of a field from your data — e.g. "player", "event_type", or any nested path like "player.name".
This field will be shown as the tooltip when hovering over each point, line, or arrow. For more complex needs, you can preprocess your data to combine multiple fields into a single string before passing it in. Custom tooltips are a great way to provide context and interactivity without cluttering the visual layout.
[14]:
pitch = Pitch(
provider="statsbomb",
orientation="horizontal",
view="right",
theme="night",
show_axis=False,
show_legend=False,
width=400,
height=400
)
shots = (
flow
.filter(where_equals("type.name", "Shot"))
.filter(where_equals("team.name", "Liverpool"))
.assign(
hover_text=lambda x: get_field(x, "player.name") \
+ ": (" \
+ str(get_field(x, "location.0")) \
+ ", " \
+ str(get_field(x, "location.1"))
+ ")"
)
)
pitch.plot_scatter(shots, "location.0", "location.1", hover="hover_text")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[14]:
Exporting#
The Pitch class supports exporting your visualizations to a variety of static formats, including PNG, SVG, PDF, and more. This is done using the .save() method, which saves the current figure to a file with your chosen filename and format. If no format is specified, it is inferred from the file extension. You can also adjust the resolution using the scale, width, and height parameters. This makes it easy to generate high-quality visuals for use in reports,
presentations, or publications. Note that exporting requires the kaleido package, which you can install with pip install kaleido
pitch.save("pitch.png")
pitch.save("pitch.svg")
pitch.save("pitch.pdf")
pitch.save("pitch.png", scale=2)
pitch.save("pitch.png", width=800, height=600)
pitch.save("pitch.png", scale=2, width=800, height=600)
View Modes and Zooming#
The view parameter lets you control how much of the pitch is visible, making it easy to zoom in on specific areas of the field. This is useful for highlighting activity in a particular zone, such as the final third, a penalty box, or a team’s attacking half.
You can set view in three different ways:
Named views (strings)#
Pitch(view="left") # Show left half
Pitch(view="right") # Show right half
Pitch(view="top") # Top half (horizontal orientation)
Pitch(view="bottom") # Bottom half
Pitch(view="full") # (default) Show the full pitch
These work across all pitch providers and orientations.
Horizontal slice (x0, x1)#
Zoom in on a portion of the pitch by specifying the horizontal range (in native units):
Pitch(view=(30, 90)) # Show only the middle third of the pitch
Custom bounding box ((x0, x1, y0, y1))#
For full control, you can specify both X and Y bounds in the pitch’s native coordinate system:
Pitch(view=(100, 120, 18, 62)) # Zoom in on the right penalty area (StatsBomb units)
This is especially useful for isolating set-piece areas, goalmouth activity, or tight pressing zones.
Notes#
The view works with any provider (
"statsbomb","wyscout", etc.) and is automatically scaled and oriented correctly.Margins are applied automatically to avoid clipping lines or markers.
When using vertical orientation, the axes will flip appropriately behind the scenes.
Orientation Handling#
The orientation parameter controls whether the pitch is drawn horizontally or vertically. By default, Pitch uses a horizontal orientation (left to right), but you can switch to vertical to flip the field and better match certain tactical views.
Pitch(orientation="horizontal") # Default
Pitch(orientation="vertical") # Flips X and Y axes
[15]:
pitch = Pitch(
provider="statsbomb",
orientation="vertical",
view="top",
theme="minimal",
show_axis=False,
show_legend=False,
width=400,
height=400
)
pitch.plot_scatter(shots, "location.0", "location.1")
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
/Users/martin/repos/penaltyblog/venv/lib/python3.13/site-packages/statsbombpy/api_client.py:21: NoAuthWarning:
credentials were not supplied. open data access only
[15]:
Layer management#
Every visual element you add with a plotting method is placed on a named layer, allowing you to treat groups of traces and annotations as a single unit. By default, plot_scatter, plot_heatmap, plot_kde, plot_arrows, and plot_comets drop their output on layers called "scatter", "heatmap", "kde", "arrows", and "comets" respectively, but you can override the name with the layer= argument. Once the figure is built you can:
Hide / show a layer at any time with
set_layer_visibility("heatmap", visible=False).Remove it completely with
remove_layer("arrows").Re-order layers so one set of traces sits above or below another using
set_layer_order(["heatmap", "scatter", "arrows"]).
This makes it easy to toggle tactical annotations, switch between different analytical overlays, or export multiple variants of the same figure without redrawing.
Titles, Subtitles and Subnotes#
You can easily add contextual information to your pitch visualizations by specifying a title, subtitle, and subnote when initializing the Pitch object. These elements are automatically positioned above and below the pitch and styled according to the selected theme. This makes it simple to provide clear, consistent annotations—such as competition names, data sources, or author credits—without needing to manually configure layout or annotations. All elements adapt intelligently to the pitch
orientation and available space, ensuring your visuals remain clean and professional.
[16]:
pitch = Pitch(
title="Shot Map",
subtitle="Stockport County vs Liverpool",
subnote="penaltyblog is awesome",
orientation="horizontal",
theme="minimal",
width=400,
height=400
)
# Plot some example data (e.g., scatter of shot locations)
data = [{"x": 102, "y": 34}, {"x": 95, "y": 20}, {"x": 88, "y": 42}]
pitch.plot_scatter(data)
HTML(pio.to_html(pitch.fig, include_plotlyjs="cdn"))
[16]: