mplsoccer: football pitch charts in matplotlib
Football pitch plotting library for matplotlib
At a glance
- What is it?
- mplsoccer is a Python library for plotting football charts in matplotlib, covering pitches, radar charts, pizza charts, bumpy charts and the usual event overlays, plus a loader for StatsBomb open data and a coordinate standardiser. It suits an analyst who wants a clean pitch graphic without hand drawing the markings, and it is a plotting layer, not a data source or a statistics package.
- Who is it for?
- Adopt mplsoccer if you make football graphics in Python and want a properly drawn pitch, radar or bumpy chart as a matplotlib object you can keep styling yourself, and if you are willing to supply the data. Do not adopt it expecting it to fetch or analyse match data, because the only data source it names is StatsBomb open data and the standardiser only puts coordinates into a common frame.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 39 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What mplsoccer draws, and what it leaves to you
mplsoccer is a Python library for plotting soccer and football charts in matplotlib, and for loading StatsBomb open data. The author describes the intent plainly: to help make insightful graphics faster, so that you do not have to build charts from scratch.
The list of what you can plot is a football analyst's checklist. Football or soccer pitches on nine different pitch types. Radar charts. Nightingale or pizza charts. Bumpy charts for showing changes over time. Arrows, heatmaps, hexbins, scatter, and comet lines. On the data side, it can load StatsBomb data as a tidy dataframe, and it can standardise pitch coordinates into a single format.
That last item is the one that matters most for anyone building more than one graphic. Every data provider encodes a pitch in its own coordinates, and comparing an event from one source against a pitch drawn for another is where analysis quietly goes wrong. A standardiser is not a glamorous feature, but it is the difference between a library that draws and a library that can be trusted to draw on top of several sources.
The library is not a data provider and not an analysis package. It does not fetch live match data, it does not compute expected threat, and it does not do statistics beyond what matplotlib and the scientific Python stack already do. The dependencies make that plain: matplotlib, numpy, pandas, Pillow, requests, scipy and seaborn. Those are the tools it builds on, and the ones it expects you to keep using.
The project is MIT licensed, the default branch is `main`, the last push to the tree was on 2026-09-14, and the repository has no GitHub releases, so the changelog file in the repository is where the version history lives.
Drawing a pitch in four lines
The quick start is a single import and a call. You create a `Pitch` with a colour scheme, draw it, and get back a matplotlib figure and axes that you own.
from mplsoccer import Pitch
import matplotlib.pyplot as plt
pitch = Pitch(pitch_color='grass', line_color='white', stripe=True)
fig, ax = pitch.draw()
plt.show()The three keyword arguments are the interesting part. `pitch_color` sets the surface, `line_color` sets the markings, and `stripe=True` asks for the mowing stripes. They are constructor options rather than post hoc styling, which tells you the library's model: the pitch is configured when you create it, and everything after `draw()` is ordinary matplotlib work.
That last point is the reason to use this library at all. The return value is a standard `(fig, ax)` pair, so everything you already know about matplotlib applies: you add text, you annotate a rectangle, you save the figure, you compose it with other axes. mplsoccer draws the pitch, and the drawing does not take over the canvas.
For an analyst this matters more than it sounds. A football graphic is usually a pitch with data on it, and the data layer is where the custom work is. If the pitch were returned as an image, you would be compositing pixels. Because it is returned as axes, you can place a scatter of event locations at data coordinates and the pitch markings stay underneath as vector output that scales to any size you print.
The documentation site is at mplsoccer.readthedocs.io, and the gallery of examples there is the fastest way to see which chart types are actually usable for the data you have.
Radar charts, drawn in layers you control
The radar example is longer than the pitch example, and the reason is instructive. A radar chart here is not one call that returns a finished image. It is a sequence of drawing steps, each of which returns objects you can restyle afterwards.
from mplsoccer import Radar
import matplotlib.pyplot as plt
radar = Radar(params=['Agility', 'Speed', 'Strength'], min_range=[0, 0, 0], max_range=[10, 10, 10])
fig, ax = radar.setup_axis()
rings_inner = radar.draw_circles(ax=ax, facecolor='#ffb2b2', edgecolor='#fc5f5f')
values = [5, 3, 10]
radar_poly, rings, vertices = radar.draw_radar(values, ax=ax,
kwargs_radar={'facecolor': '#00f2c1', 'alpha': 0.6},
kwargs_rings={'facecolor': '#d80499', 'alpha': 0.6})
range_labels = radar.draw_range_labels(ax=ax)
param_labels = radar.draw_param_labels(ax=ax)
plt.show()`setup_axis` prepares the axes. `draw_circles` adds the background rings and returns them. `draw_radar` takes the values and returns three things at once, the polygon, the rings and the vertices, so you can adjust any of them. Then the range labels and the parameter labels are drawn separately, and each of those steps returns its own artist.
The `min_range` and `max_range` arguments are per parameter, which is the detail that separates a usable radar from a decorative one. Agility rated 5 on a 0 to 10 scale and Strength rated 10 are not comparable to a metric measured on a different scale, and the per parameter ranges let you normalise them. The colour is passed as keyword dictionaries, `kwargs_radar` and `kwargs_rings`, which means the colour is data, not a hard coded default.
The same layered design appears in the pitch example, and it is the library's consistent idea: the chart is assembled from parts, and the caller decides how each part looks.
The other chart families and the examples directory
Beyond pitches and radars, the README lists Nightingale or pizza charts, bumpy charts for showing changes over time, and the event overlay types: arrows, heatmaps, hexbins, scatter, and comet lines.
The comet line is worth a sentence of its own because it is the one that is specific to this kind of data. A line plot normally joins consecutive points with a straight segment. A comet line leaves a trail, so a sequence of positions over time reads as movement rather than as a jump between two places. On a pitch, that is how you show the path of a carry or a sequence of touches, with the direction of travel visible.
Heatmaps and hexbins are the density counterparts, and both are named in the same list, which tells you the library covers both the continuous and the binned version of the same question. Bumpy charts sit in a different family again: they are not pitch based at all, and they exist to show how something changes over time, which in football is usually a ranking.
The repository shows the same structure in its directories. The `examples/` folder is organised by chart type, with subdirectories for `bumpy_charts/`, `pitch_plots/`, `pitch_setup/`, `pizza_plots/`, `radar/`, `sonars/`, `statsbomb/` and `tutorials/`, plus a README of its own. The `sonars/` directory is not mentioned in the README's feature list at all, which is a good reminder that the gallery and the examples directory are where the full API surface actually shows up. The `mplsoccer/` package directory sits beside `docs/` and `tests/`.
The pitch types themselves are the other axis of choice. Nine different pitch types are supported, and the pitch setup examples are the place to see which coordinate system each one expects.
StatsBomb loading and the coordinate standardiser
Two of the features are about data rather than drawing, and they are the reason the library is more than a set of pitch shapes.
The first is loading StatsBomb data as a tidy dataframe. StatsBomb publishes free open data, and the README credits StatsBomb both for that data and for its visual design, which is a reasonable place to have taken the chart styling cues from. The `statsbomb/` example directory and the `requests` dependency are the visible signs of this, and the loader's job is to hand you a dataframe rather than a pile of nested JSON, so that the rest of the work is pandas.
The second is the standardiser, and it deserves more attention than the README gives it. Its job is to take pitch coordinates from a source and put them into a single format, so that a chart drawn for one provider can be compared with a chart drawn for another.
The credits are explicit about the lineage here. The design and the Standardizer were influenced by ggsoccer, and the API design was heavily inspired by Peter McKeever. That is worth knowing because it tells you where to look if the standardiser does not do what you expect: the behaviour it was modelled on is documented elsewhere, and the standardiser's semantics are about mapping between coordinate frames, not about parsing a specific provider's format.
The practical risk is the one that comes with any coordinate mapping. If your data is in a frame the standardiser does not recognise, the result is a plot that still renders and is still wrong, and there is no error to catch it. That is why the pitch type you choose and the frame your data is in have to be checked against each other before you draw anything.
For the inspiration list more broadly, the README credits lastrow's animations, fcrstats' and fcpython's tutorials for using football data, Karun Singh's expected threat visualisations, and a John Burn-Murdoch tweet as the original spark for the author.
Packaging, dependencies and the documentation build
Installation is the ordinary pair of routes, pip or conda.
pip install mplsoccerconda install -c conda-forge mplsoccerThe packaging metadata in `pyproject.toml` uses hatchling as the build backend, with the version read from `mplsoccer/__about__.py` through the hatch version hook, and it declares `requires-python = ">=3.10"`. The dependency list is the scientific Python stack: matplotlib at 3.8 or newer, numpy, pandas, Pillow, requests, scipy and seaborn.
Two things there are worth noticing. The floor on matplotlib is explicit at 3.8, so a project pinned to an older matplotlib will not install cleanly. And seaborn is a hard dependency rather than an optional extra, which tells you the chart styling leans on seaborn's defaults.
The classifiers list Python 3.9 through 3.14 even though the metadata requires 3.10 or newer, a small inconsistency in the packaging rather than a problem in practice.
The development setup uses hatch environments with uv as the installer, and the default environment carries pytest and pytest-cov with a `cov` script that runs the suite with a coverage report over the `tests` directory. The docs environment pulls in a wider set: adjustText, cmasher, highlight-text, kloppy, lxml, numpydoc, pyarrow and Sphinx. The presence of kloppy in the docs dependencies is a hint that the documentation examples include data from that project, though the README itself does not mention it.
The repository also has a `.pylintrc`, a `.readthedocs.yml` for the documentation host, and a `CHANGELOG.md` that the README points to for the full list of recent changes, which matters here because there are no GitHub releases to read instead.
Editorial conclusion
Adopt mplsoccer if you make football graphics in Python and want a properly drawn pitch, radar or bumpy chart as a matplotlib object you can keep styling yourself, and if you are willing to supply the data. Do not adopt it expecting it to fetch or analyse match data, because the only data source it names is StatsBomb open data and the standardiser only puts coordinates into a common frame. Before your first chart, read the gallery at mplsoccer.readthedocs.io, pick the pitch type matching your data source, and check that the coordinates you hold are in the frame the standardiser expects, since a mismatch there produces a chart that looks plausible and is wrong.
Frequently asked questions
How do I install mplsoccer?
Use pip with `pip install mplsoccer`, or install from conda-forge with `conda install -c conda-forge mplsoccer`. The package requires Python 3.10 or newer and matplotlib 3.8 or newer.
How do I draw a football pitch in mplsoccer?
Create a `Pitch` with the colours you want, for example `Pitch(pitch_color='grass', line_color='white', stripe=True)`, call `pitch.draw()`, and you get back a matplotlib figure and axes that you can add your own annotations to.
What chart types does mplsoccer support?
The README lists pitches on nine different pitch types, radar charts, Nightingale or pizza charts, bumpy charts for changes over time, and overlays including arrows, heatmaps, hexbins, scatter and comet lines. The examples directory also has a `sonars/` folder that the README does not list.
Can mplsoccer load StatsBomb data?
Yes, it can load StatsBomb data as a tidy dataframe, and it can standardise pitch coordinates into a single format so that data from different providers can be drawn on the same pitch. The coordinate standardiser was influenced by ggsoccer.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/andrewrowlinson-mplsoccer)Was this useful?