Repository navigation
Releases: Ultraplot/UltraPlot
Release list
UltraPlot 2.7.2: Fixed malformed docstrings
Very minor changes that fixes the stubs generated for usage with static code analyzers like Pylance
What's Changed
- Feat/cheat sheet by @cvanelteren in #813
- Add full-screen option to the images by @cvanelteren in #834
- Fix/issue 835 docstrings by @cvanelteren in #836
- [pre-commit.ci] pre-commit autoupdate by @pre-commit-ci[bot] in #837
Full Changelog: v2.7.1...v2.7.2
UltraPlot v2.7.1: MPL 3.11.2 compatibility
Minor update that fixes a bug on mpl 3.11.2 where Colormaps are not allowed to pass None
What's Changed
- Fix geoticks for shifted rectangular projections by @cvanelteren in #829
- Bump basedpyright from 1.31.4 to 1.40.1 in the python-dependencies group by @dependabot[bot] in #830
- Fix lookup issue with newer matplotlib by @cvanelteren in #832
Full Changelog: v2.7.0...v2.7.1
What's Changed
- Fix geoticks for shifted rectangular projections by @cvanelteren in #829
- Bump basedpyright from 1.31.4 to 1.40.1 in the python-dependencies group by @dependabot[bot] in #830
- Fix lookup issue with newer matplotlib by @cvanelteren in #832
Full Changelog: v2.7.0...v2.7.1
UltraPlot v2.7.0 : Harder, Better, Faster, Stronger
UltraPlot 2.7.0 makes figure updates faster by reusing layout work and redrawing changed content more selectively. With fixed limits and a primed redraw cache, updating one line in our 2×2 and 4×4 benchmarks was 10× and 78× faster than v2.6.0. Initial draws were up to 1.2× faster, with little change for the single-panel case. The performance work also covers 3D rotation and saved animations. This release makes per-panel formatting easier, adds precise inset colorbar placement, supports Matplotlib 3.11, and brings the UltraPlot documentation and API directly to AI assistants through an optional MCP server.
Performance
- Faster figure drawing and updates: UltraPlot now reuses unchanged layouts, caches repeated tick calculations and axes measurements during layout, and uses selective redraws when the figure supports them. These changes reduce the work surrounding the renderer, especially for figures with many panels, labels, and guides (#781).
The gains are largest when most of a figure stays unchanged. Updating one line took 104 → 10 ms in the 2×2 grid and 288 → 3.7 ms in the 4×4 grid; the single-panel case improved from 37 → 23 ms. These measurements include the ordinary canvas.draw() call after a data update, with no private blitting API required. The first redraw builds the retained cache: that setup draw was 7–45% slower across the four figures, and steady unchanged draws showed smaller, mixed changes. The chart keeps those costs visible alongside the faster updates.
Drawing times are medians with interquartile ranges from seven fresh figures per workload and implementation, measured on an AMD Ryzen 9 4900HS using Python 3.13.7, Matplotlib 3.10.6, and the Agg backend. Both UltraPlot versions use the same dependency environment. Matplotlib is shown with automatic tight layout on every draw and with a one-time tight layout followed by fixed positions. The four phases separate the first display, first redraw/cache setup, steady unchanged drawing, and an update to one line with unchanged limits. Heatmaps have no line-update measurement. Every measured selective line update matched a forced full redraw pixel for pixel. These results describe the supplied workloads; they do not measure interactive display latency, data processing, or movie encoding. Source, samples, workload previews, and editable figures are included with the release assets.
Reproduce the measurements and edit the figure
The release assets include the benchmark script, individual timing samples, environment metadata, plotting source, and editable SVG and PDF figures. Plotting reads the saved measurements, so changing colors, labels, or layout does not rerun the benchmark. See the accompanying README for the complete commands and measurement protocol.
- Drawing comparison with Matplotlib: Replaying the original #781 overview on current source, UltraPlot's initial draw, clean second draw, and title update were 1.2–2.4× faster than Matplotlib with automatic tight layout across the 2×2 and 5×5 grids. Initial drawing was also 1.2–1.3× faster than plain Matplotlib, which performs no layout adjustment in this protocol. Figure construction remains more expensive, and plain Matplotlib was faster for the second draw and title update.
This separate replay preserves the original 600-point lines, automatic ticks, shared style state, and five timing categories, with five fresh figures per case. The second draw includes retained-cache setup. The earlier selective redraw figure in #781 instead compared retention enabled and disabled on the same source, after warm-up. Its speedups therefore describe a different baseline from a release-to-release comparison. The assets retain the recovered original script and a protocol comparison so these results can be edited and reproduced without mixing the baselines.
- Faster animation saving (
uplt.FuncAnimation,uplt.ArtistAnimation): The new classes retain Matplotlib's animation interface while rendering supported movie formats directly from the Agg buffer. The fast save path avoids repeated figure-saving machinery, holds the layout fixed, and can redraw only the artists returned by the update function. Supported FFmpeg and Pillow output paths use this automatically; unsupported settings fall back to Matplotlib. Passfast=Falsetosave()to select the regular path explicitly (#803).
Saving the supplied 60-frame, 320×320 GIF took a median 0.277 s through the fast path and 3.31 s through the regular path, approximately 12× faster. Five fresh animations were saved per path with alternating order, using Pillow 11.3.0 and blit=True on the same current UltraPlot source. The timings include save-time drawing and encoding, and exclude constructor startup. All outputs contained 60 changing frames. Decoded GIFs showed small color differences consistent with palette and edge rendering, so this export comparison is not pixel-identical. The samples and frame comparison are included in the assets.
snippet
import numpy as np
import ultraplot as uplt
x = np.linspace(0, 2 * np.pi, 400)
fig, ax = uplt.subplots(refwidth=4)
(line,) = ax.plot(x, np.sin(x))
ax.format(xlabel="Phase", ylabel="Amplitude", ylim=(-1.1, 1.1))
def update(frame):
line.set_ydata(np.sin(x + frame / 10))
return (line,)
animation = uplt.FuncAnimation(fig, update, frames=60)
animation.save("waves.gif", writer="pillow", fps=20)Blitting defaults to True: return every artist changed by the update function. Use blit=False when the whole figure must be redrawn, such as when updates also change ticks or other artists outside that return value.
- More responsive navigation: Interactive panning and 3D rotation can use temporary lower-detail previews, restoring full detail after the gesture. Set
uplt.rc["navigation.preview"] = Falseto disable these previews. Ordinary draws, animations, and saved figures retain their normal detail (#781).
In the dense 3D scene, navigation rendering fell from 386 to 343 ms per frame versus v2.6.0; with a static 2D panel beside it, 301 to 268 ms. These are approximately 1.12× speedups. Full-detail results were mixed, and the smaller helix grid's preview was slightly slower. Each case uses five fresh figures and six view angles on Agg, with fixed limits and ticks. The timings measure drawing after changing the view, excluding GUI scheduling and display latency. Every tested navigation gesture restored full-quality pixels on release.
The retained 2×2 helix grid requires further review: although its full-detail time fell from 79 to 71 ms, the final frame differed from a forced full redraw at 240 of 388,800 pixels in tick-label regions. That result is marked in the figure and is not a verified pixel-equivalent speedup. The assets include the reproducer, compared frames, and difference mask.
New Features
-
Per-panel formatting with sequences and mappings: Pass a sequence of axis labels, titles, or limits to
format()to distribute them across the subplot grid. Per-panel labels can keep their limits shared, while distinct limits detach the relevant sharing. Mappings select individual axes or groups with one-based selectors, and the new sharing controls let you configure limits, axis labels, and tick labels independently (#817).snippet
import numpy as np import ultraplot as uplt x = np.linspace(0, 1, 100) fig, axs = uplt.subplots(ncols=2) for ax, phase in zip(axs, (0, np.pi / 2)): ax.plot(x, np.sin(2 * np.pi * x + phase)) axs.format( title=["Sine", "Cosine"], xlabel=["Cycle A", "Cycle B"], ylabel="Amplitude", ) # Mapping selectors are one-based: this changes the second subplot. axs.format(title={2: "Shifted by a quarter cycle"})
-
Precise inset colorbar placement: Inset colorbars now accept
bbox_to_anchorandbbox_transform, giving them positioning controls familiar from legends. Label space is also reserved correctly near an axes edge, including upper-right colorbars with labels above them (#812).
Per-panel sequences give the two line plots their own labels and limits, while the colorbar anchors place the full guide footprint at a chosen corner inside each heatmap. The accompanying SVG, PDF, and plotting source are editable.
snippet: anchored inset colorbar
import numpy as np
import ultraplot as uplt
fig, ax = uplt.subplots(refwidth=3)
field = np.outer(np.sin(np.linspace(0, 3, 30)),
np.cos(np.linspace(0, 3, 30)))
mesh = ax.pcolormesh(field, cmap="roma", levels=np.linspace(-1, 1, 13))
ax.colorbar(
mesh,
loc="ur",
orientation="vertical",
bbox_to_anchor=(0.88, 0.86),
width=1.2,
length=8,
)Integrations
- UltraPlot MCP server: The optional server lets an AI assistant search documentation and examples, inspect the live Python API, and read source code and release notes. ...
UltraPlot v2.6.0: Histogram KDEs, sticky edges, and smoother integrations
UltraPlot v2.6.0: Histogram KDEs, sticky edges, and smoother integrations
UltraPlot 2.6.0 adds kernel density overlays to histograms, makes sticky axis
edges configurable, expands geographic legends, and improves interoperability
with Seaborn. This release also fixes shared geographic tick configuration,
refreshes the documentation experience, and hardens tag-based package releases.
New Features
- Kernel density overlays for histograms (
hist(..., kde=True)): Histograms
can now draw a Gaussian kernel density estimate for every data column. Each
curve follows its histogram's color, count or density scaling, orientation,
weights, and stacking. Usekde_kwto select the bandwidth and evaluation
resolution or pass ordinary line styling. SciPy is available through the new
statsextra withpip install ultraplot[stats](#795).
snippet
import numpy as np
import ultraplot as uplt
rng = np.random.default_rng(51423)
data = rng.normal(size=(500, 3)) + np.arange(3)
fig, ax = uplt.subplots(refwidth=4)
ax.hist(
data,
bins=20,
kde=True,
kde_kw={"bw_method": "silverman", "linewidth": 2},
labels=("A", "B", "C"),
legend="ur",
)
ax.format(xlabel="value", ylabel="count")-
Configurable sticky edges: The new
axes.sticky_edgesrc setting and
per-axesuse_sticky_edgesproperty control whether lines, fills, and similar
artists meet the axes bounds without automatic padding. This keeps the useful
default while making it easy to restore margins globally or for one axes
(#796).snippet
import ultraplot as uplt uplt.rc["axes.sticky_edges"] = False fig, axs = uplt.subplots(ncols=2) axs[0].plot([0, 1], [0, 1]) # Override the global setting for an individual axes. axs[1].use_sticky_edges = True axs[1].plot([0, 1], [0, 1])
-
Line entries in geographic legends: Geographic legends now accept line
symbols alongside the existing point and area symbols (#783).
Integrations
-
Better Seaborn legend compatibility:
UltraLegendnow implements
remove(), and UltraPlot supplies the compatibility hooks expected by
seaborn.move_legend. Legends created by Seaborn insideax.external()can
therefore be moved or removed normally (#793).snippet
import seaborn as sns import ultraplot as uplt fig, ax = uplt.subplots() with ax.external(): sns.histplot(data, ax=ax, kde=True, legend=True) sns.move_legend(ax, "upper right")
Bug Fixes
- Shared geographic ticks: Explicit longitude and latitude locators,
minor locators, and formatters now propagate across sharedGeoAxes. The
single-tick edge case is also handled correctly (#801). - Statistical plotting docs: Fixed the documentation build after adding the
histogram KDE example (#798).
Deprecations
- Basemap backend: The Basemap geographic backend is now deprecated for
UltraPlot 3.0. Use Cartopy for new geographic plots (#786). - Legacy ProPlot API: Removed items that had remained deprecated since the
ProPlot transition (#779).
Documentation and Maintenance
- Reworked the Why UltraPlot? page into interactive before-and-after
comparisons and made the divider directly draggable (#788, #799). - Corrected tag-derived package versions and limited release-version validation
to publish builds (#790, #791). - Made
docs/Makefileperform a genuinely clean documentation rebuild while
preserving efficient CI caching (#789). - Refreshed CI workflows and GitHub Actions dependencies (#784, #785).
Commits
5828da6fAdd line to geolegend (#783)9987c1dcBump the github-actions group with 2 updates (#784)11cc5416Remove deprecated items58568d6cFix black formatting2ab3b6ceUpdate the other workflow files (#785)3eb03e35Deprecate the basemap backend starting from version 3.0 (#786)f0b07a9dRemove deprecated items (#779)f6a0611fMake docs clean fully reset the build cache (#789)81cba25bFix release build versioning from Git tags (#790)c4665a99Only validate release version on non-PR publish builds (#791)9c1d900aRework the Why UltraPlot? page with comparison cards (#788)3502a4e3Add remove to legend (#793)70f8fb9eAdd sticky edges configuration (#796)7de5adb1Fix failing documentation (#798)ae8e13e2Add KDE support for histograms (#795)bbcdfff6Make the comparison selector directly draggable (#799)9583a012Fix shared geographic tick synchronization (#801)
What's Changed
- Add line to geolegend by @gepcel in #783
- Bump the GitHub Actions group by @dependabot in #784
- Update source-formatting workflows by @cvanelteren in #785
- Deprecate the Basemap backend by @cvanelteren in #786
- Remove deprecated items by @cvanelteren in #779
- Rework the Why UltraPlot? page by @cvanelteren in #788
- Fully reset the docs build cache on clean by @cvanelteren in #789
- Fix release build versioning from Git tags by @cvanelteren in #790
- Fix release version validation for pull requests by @cvanelteren in #791
- Add Seaborn-compatible legend removal by @cvanelteren in #793
- Add configurable sticky edges by @cvanelteren in #796
- Fix the statistical plotting documentation by @cvanelteren in #798
- Add KDE support for histograms by @gepcel in #795
- Improve the comparison selector by @cvanelteren in #799
- Fix shared geographic tick synchronization by @cvanelteren in #801
Full Changelog: v2.5.0...v2.6.0
What's Changed
- license update by @cvanelteren in #6
- typo in readme shield by @cvanelteren in #7
- Logo square by @cvanelteren in #5
- replaced pplt -> uplt by @cvanelteren in #8
- Revert "license update" by @cvanelteren in #10
- Conda and pypi publish workflow by @cvanelteren in #4
- Allow triangulation object in tricountour(f) by @cvanelteren in #13
- added linter workflow by @cvanelteren in #16
- mpl 3.10 by @cvanelteren in #17
- Axis sharing fix by @cvanelteren in #18
- update python versions and dependencies in pyproject.toml by @Jhsmit in #20
- Hotfix contour by @cvanelteren in #24
- Fix prop cycling not working by @cvanelteren in #26
- Remove pytest mpl from workflow by @cvanelteren in #30
- Cycling hotfix2 by @cvanelteren in #29
- Compatiblity update for mpl 3.10 by @cvanelteren in #33
- Move theme under organization by @cvanelteren in #37
- Updated contributing to reflect effver by @cvanelteren in #35
- prod: add dependabot to update github action workflow versions by @beckermr in #38
- Bump the github-actions group with 3 updates by @dependabot[bot] in #39
- prod: drop python 3.9 per MPL and NEP29 by @beckermr in #40
- prod: clean out old env files, clean out old ci files, and add pre-commit by @beckermr in #41
- fix missing logo by @cvanelteren in #42
- Add baseline comparison to workflow by @cvanelteren in #45
- Adding pytest-mpl to ultraplot workflow by @cvanelteren in #47
- Readthedocs fix by @cvanelteren in #48
- Fix margin by @cvanelteren in #44
- Workflow needs a dep or otherwise it is skipped by @cvanelteren in #53
- Read the docs fix by @cvanelteren in #55
- test: adjust test matrix to use one locale and add matrix dimension over MPL versions by @beckermr in #51
- Dep build by @cvanelteren in #57
- Second dep on build still being skipped by @cvanelteren in #58
- another attempt to fix publish by @cvanelteren in #59
- fix: remove custom classifier fo...
UltraPlot v2.5.0: Hawkeye, text-alignment, and latex fonts
In UltraPlot 2.5.0, we introduce the Hawkeye feature for GeoAxes, a new automatic text-alignment feature, and the ability to change latex fonts with more control.
New Features
- Hawkeye map insets (
GeoAxes.hawkeye): Added a geographic callout inset
that draws attention to a region of a map without inheriting the parent
projection's aspect. Regular insets follow the parent projection, so Cartopy
stretches them to whatever the projection dictates; a hawkeye instead lets you
request a square — or circular — locator map, anchored anywhere on the parent
axes, with an automatically drawn indicator box and optional connectors.
Hawkeyes are excluded from automatic layout, so they can extend past the parent
axes without reserving subplot space, and the returned object is an ordinary
GeoAxesyou can draw external geospatial data into.
snippet
import ultraplot as uplt
singapore = (103.8198, 1.3521)
fig, ax = uplt.subplots(proj="robin", refwidth=4)
ax.format(land=True, landcolor="gray8", oceancolor="blue9")
ax.plot(*singapore, marker="o", color="red", ms=5, transform="cyl")
ax.text(106, 4, "Singapore", color="red", size=7, transform="map")
# A circular locator map anchored to the upper-right corner
inax = ax.hawkeye(
(0.97, 0.97),
size=0.23,
anchor="ur",
proj="merc",
extent=(103.76, 103.90, 1.27, 1.41),
shape="circle",
target="circle",
connector="line",
color="red",
indicator_kw={"linewidth": 1.5},
)
inax.format(land=True, landcolor="gray9", oceancolor="blue9")
inax.plot(*singapore, marker="o", color="red", ms=5, transform="cyl")- Circular and aspect-aware insets: Generalized inset support so geographic
insets can use a circular frame while still preserving projection scale. A new
aspect-aware locator keeps a circular or square inset anchored to its
lower-left corner after the box-aspect adjustment, so callout maps stay put
under resizing. Rectangular frames preserve both the requested extent and the
projection scale; circular frames expand the shorter projected dimension to
keep the projection faithful (passaspect='auto'to instead fit the exact
extent with distortion).
snippet
import ultraplot as uplt
fig, ax = uplt.subplots(proj="robin", refwidth=4)
ax.format(land=True, landcolor="gray8", oceancolor="blue9")
# `shape` controls the inset frame; `target` controls the indicator on the parent
inax = ax.hawkeye(
(0.97, 0.97), size=0.23, anchor="ur", proj="merc",
extent=(103.76, 103.90, 1.27, 1.41),
shape="circle", target="circle", connectors="line",
)
inax.format(land=True, landcolor="gray9", oceancolor="blue9")- Automatic text alignment (
Axes.auto_align_text): Added a KD-tree-based
relaxation solver that repositions text and annotations so they stop
overlapping each other, the plotted data, and the axes edges, then pulls them
back toward where you put them. Because it runs at draw time, the layout stays
valid across resizing and changing data limits. Opt individual labels in with
avoid_overlap=True, enable it globally with the newtext.alignrc setting,
and tune it withtext.align.pad,text.align.maxiter, and
text.align.arrows(which draws a connector back to each displaced label).
snippet
import ultraplot as uplt
fig, ax = uplt.subplots()
ax.scatter(x, y)
for xi, yi, name in zip(x, y, names):
ax.text(xi, yi, name)
# Relax the labels apart; `arrows=True` connects moved labels to their points
ax.auto_align_text(arrows=True)- Computer Modern math symbols (
mathtext.cm_symbols): Added a middle ground
between font-matched math and fulltext.usetex. With the setting on,
ordinary letters and numbers keep the active document font, while\mathcal
routes throughcmsy10and big operators (\sum,\prod,\int,\oint,
\bigcup,\bigoplus) route throughcmex10for an authentic Computer Modern
look — no external LaTeX required. Math is parsed when the figure is drawn,
so set this globally rather than inside a context block.
snippet
import ultraplot as uplt
expr = r"$\mathcal{ABCXYZ}\quad\sum_{i=0}^{n}\quad\prod_{j=1}^{m}\quad\int_a^b\quad\oint_C$"
uplt.rc["mathtext.cm_symbols"] = True
fig, ax = uplt.subplots(refwidth=6, refheight=1.1)
ax.text(0.02, 0.5, expr, transform="axes", va="center", fontsize=24)
ax.format(title="Computer Modern math symbols", titleloc="left")- Geographic axes in mixed subplot layouts (
abcanchor+ geo aspect): Gave
map users explicit control over the fixed-aspect-vs-slot trade-off that arises
when a map shares a GridSpec with Cartesian axes. The newabcanchoroption
chooses whether an a-b-c label attaches to the visible map boundary
('axes', the default) or to the original GridSpec slot ('slot', useful for
a regular label grid across mixed subplot types). Maps can also be stretched to
fill their slot withaspect='auto', or made the figure's layout reference so
the whole figure resizes around them.
snippet
import ultraplot as uplt
layout = [[1, 1, 1, 2, 2, 2], [3, 3, 4, 4, 5, 5]]
fig, axs = uplt.subplots(layout, refwidth=2.4, proj={4: "cyl"}, share=False)
axs[3].format(
lonlim=(0, 1),
latlim=(0, 1),
abcanchor="slot", # align the map's a-b-c label with the Cartesian slots
)
fig.format(abc="A.", abcloc="left")-
Shared row/column label spacing: When figure-level row or column labels and
a shared spanning axis label sit on the same side, the row/column labels are
now placed nearer the axes and the spanning label outside them. The gap is
controlled by the newleftlabel.sharedpad,rightlabel.sharedpad,
bottomlabel.sharedpad, andtoplabel.sharedpadsettings, which can also be
passed toformat(e.g.fig.format(leftlabelsharedpad='2em')).snippet
import ultraplot as uplt fig, axs = uplt.subplots(ncols=2, nrows=2, share=True, span=True) fig.format( leftlabels=("Row A", "Row B"), ylabel="shared y label", leftlabelsharedpad="2em", # gap between the row labels and the spanning label )
-
Keyword-alias cleanup (
_alias_kwargs): Extracted the keyword/alias
resolution helpers out of theinternalsgrab-bag into a dedicated
internals/kwargs.py, and added an@_alias_kwargsdecorator that folds
synonym keywords into their canonical names with the same precedence and
conflict warning as the old_not_noneboilerplate.Figure.__init__is the
first adopter. As a user-visible upshot, the shared style docstrings (line,
patch, pcolor/contour, text) now lead each numpydoc field with the canonical
parameter name instead of a pile of aliases, making the parameter tables much
easier to scan.
Bug Fixes
- Title centering: Fixed the horizontal centering of titles (#766).
ListedColormapdeprecation: Fixed aListedColormapNdeprecation
warning from newer matplotlib (#769).- Cross-product deprecation: Fixed a deprecation in the cross-product
computation (#777).
Maintenance & Internals
- Deprecation cleanup: Removed deprecated items and unused imports (#763).
- Docs: Simplified the docs and expanded the insets, projections, fonts, and
subplots guides with the new features above. - History hygiene: Reverted a set of misplaced hawkeye commits from
main
before re-landing the feature cleanly (#772).
Commits
83b0e73c8[Feature] Add Hawkeye option (#771)ceab2bfc4Add circular inset options9175fafab[Feature] Text alignment (#754)fa26deeceRoute selected mathtext glyphs to Computer Modern (#744)384a6259f[Feature] Abc anchor and Geo aspect (#767)8b174295bReorder visual hierarchy when side-cap labels are given (#765)1be6eeacdRefactor/alias kwargs (#775)7a5ddfb5eFix centering of titles (#766)4ab628987Fix ListedColormap N deprecation (#769)c3d1e2d7bDeprecation fix for cross product computation. (#777)410de735dRemove unused imports (#763)50cbc622fRemove deprecated items6e39447fcSimplify docsd26943592Revert misplaced hawkeye commits from main (#772)
What's Changed
- [Chore] Remove unused imports by @cvanelteren in #763
- Reorder visual hierarchy when side-cap labels are given by @cvanelteren in #765
- Fix centering of titles by @cvanelteren in #766
- [Feature] Text alignment by @cvanelteren in #754
- Fix ListedColormap N deprecation by @cvanelteren in https://github.com...
UltraPlot v2.4.1: figure state fixes and subplot manager groundwork
Maintenance release. Bug fixes and internal restructuring; no new public API.
Fixes
Figure.clear()no longer leaves stale state behind. A cleared figure kept handing out destroyed axes viasubplotgridand_iter_axes, held on to the old gridspec and subplot counter, leaked figure panels, and raisedAttributeErrorfrom the nextformat(suptitle=...).clear()(and itsclf()alias) now resets subplots, panels, layout flags, and the figure-level label artists, so a cleared figure is reusable. (#760)- Sensible defaults for missing font symbols. (#752)
Internal
- Subplot creation, gridspec ownership, and projection parsing moved out of
Figureinto a dedicatedSubplotManager, the first step towardFigureas a thin interface over focused collaborators (#677). Public API is unchanged, but the privateFigure._subplot_dict,_subplot_counter, and_gridspecattributes are gone — useFigure.subplotgrid/Figure.gridspecinstead. (#759, #698) ultraplot.uinow derives projection keywords fromSubplotManager, fixinguplt.subplot(proj=...)silently routing the projection to the figure. (#760)
Release plumbing
- Zenodo archiving is handled by the Zenodo GitHub integration; the version and DOI are no longer hand-maintained in
CITATION.cff. (#761)
What's Changed
- [hotfix] add defaults for missing symbols by @cvanelteren in #752
- Chore: refactor figure with new subplot manager (#698) by @cvanelteren in #759
- Zenodo fix by @cvanelteren in #761
- Zenodo fix 2 by @cvanelteren in #762
- Fix Figure.clear leaving stale state and decouple ui kwarg routing by @cvanelteren in #760
Full Changelog: v2.4.0...v2.4.1
UltraPlot v2.4.0: Taylor diagrams, inset colorbars, improved semantic legends
UltraPlot v2.4.0
New Features
- Taylor Diagram Projection (TaylorAxes): Added a brand new polar-style axes projection for Taylor diagrams (
proj='taylor').- Implemented helper plotting methods:
plot_corrandscatter_corrto plot points using correlation coefficient and standard-deviation coordinates. - Added documentation guide, examples gallery, and integration test coverage.
- Implemented helper plotting methods:
Code Snippet
models = ("Control", "Physics A", "Physics B", "Ensemble")
correlation = np.array([0.73, 0.84, 0.91, 0.96])
stddev = np.array([0.82, 1.18, 1.05, 0.93])
colors = ("blue7", "orange7", "green7", "violet7")
fig, ax = uplt.subplots(proj="taylor", refwidth=4.2)
ax.format(
title="Model skill summary",
xlabel="Standard deviation",
ylabel="",
corrlabel="Correlation",
rlim=(0, 1.5),
rlines=0.25,
corrlines=(1, 0.95, 0.9, 0.8, 0.6, 0.4, 0.2, 0),
)
# Centered RMS-difference contours around the reference point at (corr=1, std=1).
theta = np.linspace(0, np.pi / 2, 160)
radius = np.linspace(0, 1.5, 160)
theta_grid, radius_grid = np.meshgrid(theta, radius)
rms = np.sqrt(1 + radius_grid**2 - 2 * radius_grid * np.cos(theta_grid))
contours = ax.contour(
theta_grid,
radius_grid,
rms,
levels=(0.25, 0.5, 0.75, 1.0, 1.25),
cmap="tokyo",
lw=0.9,
ls="--",
)
ax.clabel(contours, levels=(0.5, 1.0), inline=True, fontsize=8, fmt="%.1f")
ax.plot_corr(1, 1, marker="*", markersize=12, color="red7", label="Reference")
for name, corr, std, color in zip(models, correlation, stddev, colors):
ax.scatter_corr(
corr,
std,
s=75,
color=color,
edgecolor="white",
lw=0.8,
zorder=4,
label=name,
)
ax.legend(loc="b", ncols=3, frame=False)
fig.show()
-
Side-Attached Inset Colorbars: Enabled colorbars to attach to the sides of inset axes.
- Side colorbar requests now map dynamically to side-appropriate default orientations relative to the inset.
- Added support for stacking and aligned placement similar to standard axes colorbars.
Code Snippet
import ultraplot as uplt fig, ax = uplt.subplots() inset = ax.inset([0.5, 0.5, 0.4, 0.4]) # Attach colorbar directly to the side of the inset axes rather than standard subplot panels inset.colorbar(mappable, loc="right", label="Value")
-
Axes Styling Enhancements:
- Allowed
axesec/axesedgecolorandaxeslw/axeslinewidthaliases to control global axes boundary/frame styling. - Added the parameter mapping
size/sizesaliases to scatter-plotsparameter for matching collection interfaces.
- Allowed
Bug Fixes
- Frame Style Retention: Preserved explicit axes frame styling across layout reformatting passes.
- Title Space Calculation: Fixed space reserving calculations for external container titles (
ExternalAxesContainer) to prevent unwanted overlapping when using ABC-style sub-labels. - Legend Handle Formatting: Fixed single-point Line2D handlers in legends to correctly hide line connectors for marker-only plots.
- Single Axis Title Sharing: Prevented alignment crashes on figures when sharing titles for single Cartesian/Geographic axis systems.
Maintenance & Internals
- Actions updates: Bumped
actions/checkout,actions/cache, andcodecov/codecov-actionin CI. - Metadata: Cleaned up styling/formatting in
CITATION.cff.
What's Changed
- Allow colorbars to attach to inset axes by @cvanelteren in #738
- Abc title space fix by @cvanelteren in #741
- Preserve explicit axes frame styling across reformatting by @cvanelteren in #742
- Implement TaylorAxes by @cvanelteren in #743
- Bump the github-actions group with 3 updates by @dependabot[bot] in #748
- Align the parameters to be more in line with user expectation by @cvanelteren in #746
- Automatic inference for semantic legends for marker/scatter size by @cvanelteren in #749
Full Changelog: v2.3.0...v2.4.0
What's Changed
- Allow colorbars to attach to inset axes by @cvanelteren in #738
- Abc title space fix by @cvanelteren in #741
- Preserve explicit axes frame styling across reformatting by @cvanelteren in #742
- Implement TaylorAxes by @cvanelteren in #743
- Bump the github-actions group with 3 updates by @dependabot[bot] in #748
- Align the parameters to be more in line with user expectation by @cvanelteren in #746
- Automatic inference for semantic legends for marker/scatter size by @cvanelteren in #749
Full Changelog: v2.3.0...v2.4.0
UltraPlot v2.3.0: Enhanced semantic legends, improved polar labels placements, and geo label fixing
This release introduces significant enhancements to the semantic legend system, improved geographic plotting formatting, and various bug fixes and performance improvements.
Enhanced Semantic Legends
The semantic legend system has been unified and expanded. You can now create legends from semantic mappings with even more control over marker styles, including custom paths, CapStyle, JoinStyle, and arbitrary transforms.
Example: Custom Marker Styles
import matplotlib.transforms as mtransforms
import numpy as np
from matplotlib.markers import CapStyle, JoinStyle, MarkerStyle
from matplotlib.path import Path
import ultraplot as uplt
star = Path.unit_regular_star(6)
circle = Path.unit_circle()
star_path = Path.unit_regular_star(5)
cut_star = Path(
vertices=np.concatenate([circle.vertices, star.vertices[::-1, ...]]),
codes=np.concatenate([circle.codes, star.codes]),
)
fig, ax = uplt.subplots()
# upper left legend with custom mark
ax.catlegend(
["star", "cus_star"],
marker=[star_path, cut_star],
markersize=10,
add=True,
loc="ul",
title="Paths",
ncols=1,
)
# upper right legend with advanced CapStyle and JoinStyle
ax.catlegend(
["butt / round", "round / miter", "projecting / bevel"],
marker="1",
markersize=10,
markeredgecolor=list("gbr"),
markeredgewidth=4,
markerfacecoloralt="none",
marker_capstyle=[
CapStyle.butt,
CapStyle.round,
CapStyle.projecting,
],
marker_joinstyle=[
JoinStyle.round,
JoinStyle.miter,
JoinStyle.bevel,
],
marker_transform=[mtransforms.Affine2D().rotate_deg(x) for x in [0, 30, 60]],
title="Cap & Join Style",
add=True,
loc="ur",
ncols=1,
)
# center geolegend with different styles
ax.geolegend(
["rect", "tri", "hex", "AU"],
facecolor=["tab:red", "r", "k", "tab:blue"],
ec=["k", "g", "orange", "bright pink"],
loc="c",
title="geolegend",
ew=[0.5, 2, 1, 0.5],
markersize=10,
ncols=4,
handletextpad=0.1,
columnspacing=0.7,
)
# lower left legend with TeX symbols and rotation transform
ax.catlegend(
["\\infty", "\\sum", "\\int"],
marker=[r"$\infty$", r"$\sum$", r"$\int$"],
s=[6, 18, 9], # ms/markersize=[6,8,10]
title="TeX symbols\nwith rotation",
marker_transform=[mtransforms.Affine2D().rotate_deg(x) for x in [30, 90, 45]],
add=True,
loc="ll",
ncols=1,
)
# lower right legend with different fill style
ax.catlegend(
["top", "bottom", "left", "right"],
marker="o",
markersize=10,
mfc=["r", "g", "b", "c"],
markerfacecoloralt="lightsteelblue",
markeredgecolor=["k", "r", "y", "b"],
fillstyle=["top", "bottom", "left", "right"],
title="Half filled",
add=True,
loc="lr",
ncols=1,
)
ax.axis("off")
fig.show()Geographic Plotting Improvements
Fixed an issue where geographic grid label styling options (like labelsize) were silently ignored when formatting through SubplotGrid.format() or Figure.format().
Example: Geographic Formatting
import ultraplot as uplt
import cartopy.crs as ccrs
fig, axs = uplt.subplots(proj="merc", ncols=2)
# styling labelsize now works correctly through Figure.format
fig.format(
labels=True,
labelsize=14,
labelweight="bold",
grid=True,
coast=True
)
fig.show()Polar Label Improvements
Polar axes now support curved polar-aware axis labels via thetalabel and rlabel. These labels follow the outer theta arc or a radial spoke, respect sector and annular layouts, and stay correctly offset under theta transforms and redraws. This work also finishes the removal of generic x/y label handling from polar formatting.
Example: Polar Axis Labels
import ultraplot as uplt
fig, ax = uplt.subplots(proj="polar")
ax.format(
thetalim=(0, 120),
rlim=(0.3, 1.0),
thetalabel="Azimuth",
rlabel="Radius",
thetalabelloc=60,
rlabelloc="left",
)
fig.show()Bug Fixes and General Improvements
Various bug fixes including resolved int/list size errors in bar plots and consistent style application ordering.
Example: Bar Plot fix for pandas Series
import ultraplot as uplt
import pandas as pd
import numpy as np
data = pd.Series(np.random.rand(5), index=list("abcde"))
fig, ax = uplt.subplots()
ax.bar(data, color="blue7") # Previously might trigger size error
ax.format(title="Fixed Pandas Series Bar Plot")
fig.show()What's Changed
- Example/semantic legend rm suffix (#735) by @lukas-schoen-qut
- Add example of semantic plot to gallery (#734) by @lukas-schoen-qut
- Unify semantic legend params. (#727) by @lukas-schoen-qut
- Fix ordering of applying styles (#725) by @lukas-schoen-qut
- Fix int/list has no size error, for bar plot of pd.Series (#732) by @lukas-schoen-qut
- Change rectangle to non-square for geolegend (#730) by @lukas-schoen-qut
- Fix duplicate import in colors.py (#728) by @lukas-schoen-qut
- Fix geographic grid label styling in SubplotGrid/Figure.format (#724) by @lukas-schoen-qut
- Add polar-aware
thetalabel/rlabelsupport and remove generic x/y label handling from polar format by @lukas-schoen-qut
What's Changed
- Chore: remove hardcoded comp with main for running tests by @cvanelteren in #700
- Improve docs search ranking for API queries by @cvanelteren in #701
- Fix: panel axis upgraded when sharing axes. by @cvanelteren in #704
- Fix uncertainty legend glyphs for errorbar-based mean plots by @cvanelteren in #705
- Fix scaling of title by @cvanelteren in #709
- Bump softprops/action-gh-release from 2 to 3 in the github-actions group by @dependabot[bot] in #710
- Temporarily disable the Zenodo release job by @cvanelteren in #712
- Fix bar tick labels for xarray DataArray with string coordinate by @kinyatoride in #711
- Add extra ultraplot styles by @cvanelteren in #719
- Suppress sharing warnings when no sharing is possible by @kinyatoride in #715
- Fix render backend issues for animating graphs by @cvanelteren in #720
- Feature: figure semantic legends by @cvanelteren in #707
- docs: add AI contribution policy by @cvanelteren in #662
- Fix tick visibility leaking from styles in alternative axes by @cvanelteren in #721
- Feat true black dark bg by @cvanelteren in #722
- Fix GeoAxes grid label formatting through SubplotGrid and Figure format dispatch by @cvanelteren in #724
- [pre-commit.ci] pre-commit autoupdate by @pre-commit-ci[bot] in #733
- Fix duplicate import in colors.py by @gepcel in #728
- Change rectangle to non-square for geolegend by @gepcel in #730
- Fix int/list has no size error, for bar plot of pd.Series by @gepcel in #732
- Fix ordering of applying styles by @cvanelteren in #725
- Unify semantic legend params. by @gepcel in #727
- Add example of semantic plot to gallery by @cvanelteren in #734
- Example/semantic legend rm suffix by @cvanelteren in #735
- Add polar-aware rlabel and thetalabel support by @kinyatoride in #714
New Contributors
- @kinyatoride made their first contribution in #711
Full Changelog: v2.2.0...v2.3.0
UltraPlot 2.2.0: Precision Placement — colorbars that span, norms that flex, labels that stay
UltraPlot v2.2.0
What's New
Spanning colorbars across subplot slots
Colorbars can now span a specific range of columns or rows using the span parameter, rather than stretching across the entire figure edge. This gives much finer control over colorbar placement in multi-panel figures.
Example
import ultraplot as uplt
import numpy as np
rng = np.random.default_rng(42)
data = rng.random((20, 20))
fig, axs = uplt.subplots(nrows=2, ncols=3, share=False)
for ax in axs:
m = ax.pcolormesh(data, cmap="batlow")
# A single colorbar spanning only the first two columns
fig.colorbar(m, loc="bottom", span=(1, 2), label="Shared metric")
axs.format(
suptitle="Spanning colorbar across selected columns",
abc="[a.]",
grid=False,
)Flexible normalization inputs
Norms can now be specified as strings alongside vmin/vmax kwargs, or as compact tuple/list specs like ('linear', 0.1, 0.9). Previously, passing a string norm with explicit vmin/vmax raised an error.
Example
import ultraplot as uplt
import numpy as np
rng = np.random.default_rng(0)
data = rng.random((30, 30))
fig, axs = uplt.subplots(ncols=3, share=False)
# String norm with explicit vmin/vmax kwargs
axs[0].pcolormesh(data, norm="linear", vmin=0.2, vmax=0.8, cmap="fire")
axs[0].format(title="String + vmin/vmax")
# Tuple form bundles everything together
axs[1].pcolormesh(data, norm=("linear", 0.2, 0.8), cmap="fire")
axs[1].format(title="Tuple form")
# Works with log norms too
axs[2].pcolormesh(data + 0.01, norm=("log", 0.01, 1), cmap="fire")
axs[2].format(title="Log tuple form")
axs.format(suptitle="Flexible norm specifications", abc="[a.]", grid=False)Bug Fixes
Title border path effects properly cleared
Disabling titleborder=False now correctly removes the stroke effect from title text. Previously, calling ax.format(titleborder=False) after a title border had been applied would leave the border visible.
Example
import ultraplot as uplt
import numpy as np
rng = np.random.default_rng(0)
fig, axs = uplt.subplots(ncols=2)
for ax in axs:
ax.pcolormesh(rng.random((20, 20)), cmap="batlow")
# Left: border on (default for inset titles)
axs[0].format(title="With border", titleloc="upper left", titleborder=True)
# Right: border explicitly off — now correctly removed
axs[1].format(title="Without border", titleloc="upper left", titleborder=False)
axs.format(suptitle="Title border toggle fix", grid=False)Outer legends no longer hide shared tick labels
Adding an outer legend (loc='r') no longer suppresses y-tick labels on neighboring axes when using sharey='labs'. The hidden panel backing the legend was incorrectly being counted as a sharing participant.
Example
import ultraplot as uplt
import numpy as np
x = np.linspace(0, 4 * np.pi, 200)
fig, axs = uplt.subplots(ncols=3, sharey="labs")
for i, ax in enumerate(axs):
for j in range(3):
ax.plot(x, np.sin(x + j) * (i + 1), label=f"Wave {j+1}")
# Outer legend on the middle panel — y-tick labels stay visible on all axes
axs[1].legend(loc="r")
axs.format(
suptitle="Outer legend with shared y-labels",
xlabel="Phase",
ylabel="Amplitude",
abc="[a.]",
)Other Changes
- Zenodo publishing fix — corrected metadata for DOI generation (#686)
- Figure initialization refactor — internal cleanup of figure setup (#687)
- What's New page generation fix — documentation build improvements (#697)
Full Changelog: v2.1.9...v2.2.0
What's Changed
- Hotfix/publish zenodo fix by @cvanelteren in #686
- Refactor init figure by @cvanelteren in #687
- Feature/span cbar slot based by @cvanelteren in #688
- Fix patheffects affecting recall of titleborder by @cvanelteren in #691
- Fix outer legend hiding y-tick labels with sharey='labs' (#694) by @cvanelteren in #696
- Fix/whats new page generation by @cvanelteren in #697
- Fix/norm inputs by @cvanelteren in #693
Full Changelog: v2.1.9...v2.2.0
UltraPlot v2.1.9: bugs, nans, and improved title sharing.
With v2.1.9 we add nan support for curved_quiver, and allow for using axes slicing to set titles.
Flexible title setting through axes slicing
We intend to enhance capabilities to offer strong and emphatic controls to the user. The format method gives a succinct localized entry point to format matplotlib axes. We extend the functionality that we added to colorbars and legend by now allowing titles to be spannend across subgroupings.
snippet
import ultraplot as uplt
fig, ax =uplt.subplots(ncols = 3, nrows = 2)
ax[0, :2].format(title = "Hello world!")
fig.show()What's Changed
- Bump the github-actions group with 2 updates by @dependabot[bot] in #671
- [pre-commit.ci] pre-commit autoupdate by @pre-commit-ci[bot] in #674
- Feature: Add nan support for curved_quiver by @cvanelteren in #676
- Use format() for shared subplot slice titles by @cvanelteren in #652
- Fix: axes aspect shifting on pixel snapping after drawn by @cvanelteren in #680
- Fix regression of spanning colorbars by @cvanelteren in #681
Full Changelog: v2.1.5...v2.1.9
What's Changed
- Chore: redo zenodo sync by @cvanelteren in #685
Full Changelog: v2.1.8...v2.1.9