Releases: Ultraplot/UltraPlot
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
UltraPlot v2.1.5: Choropleth, Custom labels semantic plots, and bug fixes
UltraPlot v2.1.5
The biggest additions are richer semantic size legends and first-class
choropleth support for geographic axes, alongside typing, plotting, CI, and
documentation improvements.
Highlights
Custom labels for Axes.sizelegend
sizelegend can now describe marker magnitudes in domain language instead of
just echoing the raw numeric levels.
Snippet
import numpy as np
import ultraplot as uplt
np.random.seed(42)
cities = [
"Tokyo",
"Delhi",
"Shanghai",
"Sao Paulo",
"Mumbai",
"Cairo",
"Beijing",
"Dhaka",
"Osaka",
"Lagos",
"Istanbul",
"London",
]
population = np.array(
[37.4, 32.9, 29.2, 22.4, 21.7, 21.3, 20.9, 23.2, 19.1, 16.6, 15.8, 9.5]
)
gdp_pc = np.array([42, 8, 23, 12, 7, 4, 22, 3, 38, 3, 14, 55])
growth = np.array([0.2, 2.8, 0.5, 0.7, 1.1, 1.9, 0.4, 3.1, 0.1, 3.5, 1.4, 0.8])
fig, ax = uplt.subplots(refwidth=4.5, refaspect=1.1)
ax.scatter(
gdp_pc,
growth,
s=population * 12,
c="cherry red",
edgecolor="gray8",
linewidth=0.5,
alpha=0.85,
absolute_size=True,
)
for i, city in enumerate(cities):
offset = (5, 5)
if city == "Osaka":
offset = (5, -10)
elif city == "Beijing":
offset = (-5, 8)
ax.annotate(
city,
(gdp_pc[i], growth[i]),
fontsize=6,
textcoords="offset points",
xytext=offset,
color="gray8",
)
ax.sizelegend(
[10 * 12, 20 * 12, 35 * 12],
labels={10 * 12: "10M", 20 * 12: "20M", 35 * 12: "35M"},
title="Population",
loc="ur",
frameon=False,
color="gray6",
edgecolor="gray8",
)
ax.format(
title="Megacities: Wealth vs Growth",
xlabel="GDP per capita (k USD)",
ylabel="Annual growth rate (%)",
xgrid=True,
ygrid=True,
xlim=(-2, 62),
ylim=(-0.3, 4.2),
)
fig.show()GeoAxes.choropleth for thematic maps
You can now color countries and polygon features directly from numeric values
while keeping the same UltraPlot formatting and colorbar workflow used on
cartesian plots.
Snippet
import numpy as np
import ultraplot as uplt
values = {
"United States of America": 83.6,
"Canada": 81.7,
"Mexico": 75.1,
"Brazil": 75.9,
"Argentina": 76.7,
"United Kingdom": 81.0,
"France": 82.5,
"Germany": 80.9,
"Italy": 83.5,
"Spain": 83.4,
"Norway": 83.2,
"Sweden": 83.0,
"Russia": 73.2,
"China": 78.2,
"Japan": 84.8,
"South Korea": 83.7,
"India": 70.8,
"Australia": 83.3,
"New Zealand": 82.1,
"South Africa": 64.9,
"Nigeria": 53.9,
"Egypt": 72.1,
"Saudi Arabia": 76.5,
"Turkey": 76.0,
"Indonesia": 71.9,
"Thailand": 78.7,
}
fig, ax = uplt.subplots(proj="merc", proj_kw={"lon0": 10}, refwidth=5.5)
m = ax.choropleth(
values,
country=True,
cmap="Glacial",
vmin=50,
vmax=88,
edgecolor="none",
linewidth=0,
colorbar="b",
colorbar_kw={"label": "Life expectancy (years)", "length": 0.7},
missing_kw={"facecolor": "gray8", "hatch": "///", "edgecolor": "gray5"},
)
ax.format(
title="Global Life Expectancy (2023)",
land=True,
landcolor="gray2",
ocean=True,
oceancolor="gray1",
coast=True,
coastcolor="gray4",
coastlinewidth=0.3,
borders=True,
borderscolor="gray4",
borderslinewidth=0.2,
longrid=False,
latgrid=False,
)
fig.show()Other changes
- Better static-analysis support for the lazy top-level API.
- Numeric scatter plots with explicit numeric colors now respect
cmap. - Shared boxplot tick labels no longer duplicate.
SubplotGridsingle-item 2D slices now keep returningSubplotGrid.- Helper and release-metadata coverage expanded and the CI flow was tightened.
What's Changed
- Honor cmap for numeric scatter colors by @cvanelteren in #616
- Fix release metadata and Zenodo flow by @cvanelteren in #620
- Publish Zenodo releases via API by @cvanelteren in #625
- Support Python 3.10 TOML loading by @cvanelteren in #626
- Bump dorny/paths-filter from 3 to 4 in the github-actions group by @dependabot[bot] in #624
- Add choropleth support to GeoAxes by @cvanelteren in #623
- CI: re-add Codecov upload by @cvanelteren in #633
- Fix duplicate shared boxplot tick labels by @cvanelteren in #630
- CI: restore PR Codecov uploads by @cvanelteren in #635
- add pytest tag by @cvanelteren in #637
- Increase coverage to 85% with targeted tests by @cvanelteren in #636
- Docs: cache jupytext conversion and restore incremental html by @cvanelteren in #603
- Improve gallery widget and thumbnail backgrounds by @cvanelteren in #644
- Support custom labels in sizelegend by @cvanelteren in #629
- fix: Refresh outdated contributor setup instructions by @JiwaniZakir in #646
- Refresh constructor registries after ticker reload by @cvanelteren in #645
- Honor patch linewidth rc for edgefix by @cvanelteren in #649
- Add typing block for inspection by @cvanelteren in #659
- Fix issue where single object returns object itself by @cvanelteren in #666
- Fix/gridspec indexing by @cvanelteren in #667
- Fix choropleth horizontal line artifacts on projected maps by @cvanelteren in #668
New Contributors
- @JiwaniZakir made their first contribution in #646
Full Changelog: v2.1.3...v2.1.5
What's Changed
- Honor cmap for numeric scatter colors by @cvanelteren in #616
- Fix release metadata and Zenodo flow by @cvanelteren in #620
- Publish Zenodo releases via API by @cvanelteren in #625
- Support Python 3.10 TOML loading by @cvanelteren in #626
- Bump dorny/paths-filter from 3 to 4 in the github-actions group by @dependabot[bot] in #624
- Add choropleth support to GeoAxes by @cvanelteren in #623
- CI: re-add Codecov upload by @cvanelteren in #633
- Fix duplicate shared boxplot tick labels by @cvanelteren in #630
- CI: restore PR Codecov uploads by @cvanelteren in #635
- add pytest tag by @cvanelteren in #637
- Increase coverage to 85% with targeted tests by @cvanelteren in #636
- Docs: cache jupytext conversion and restore incremental html by @cvanelteren in #603
- Improve gallery widget and thumbnail backgrounds by @cvanelteren in #644
- Support custom labels in sizelegend by @cvanelteren in #629
- fix: Refresh outdated contributor setup instructions by @JiwaniZakir in #646
- Refresh constructor registries after ticker reload by @cvanelteren in #645
- Honor patch linewidth rc for edgefix by @cvanelteren in #649
- Add typing block for inspection by @cvanelteren in #659
- Fix issue where single object returns object itself by @cvanelteren in #666
New Contributors
- @JiwaniZakir made their first contribution in #646
Full Changelog: v2.1.3...v2.1.5
v2.1.3
This is a small patch release focused on plotting and legend fixes.
Highlights
-
Restored
frame/frameonhandling for colorbars.
Outer colorbars now again respectframeas a backwards-compatible alias for outline visibility, and inset colorbars no longer fail during layout reflow whenframe=False. -
Preserved hatching in geometry legend proxies.
Legends generated from geographic geometry artists now carry hatch styling through to the legend handle, alongside facecolor, edgecolor, linewidth, and alpha. -
Enabled graph plotting on 3D axes.
This restores graph plotting support for 3D plots.
Other changes
- Updated GitHub Actions dependencies in the workflow configuration.
Included pull requests
#605Enable graph plotting on 3D axes#610Restore colorbar frame handling#612Preserve hatches in geometry legend proxies#604GitHub Actions dependency updates
Full Changelog: V2.1.2...v2.1.3
V2.1.2 Fix colorbar framing and extra legend entries on slicing
What's Changed
- Internal: cache inspect.signature used by pop_params by @cvanelteren in #596
- Bugfix: Deduplicate spanning axes in SubplotGrid slicing by @cvanelteren in #598
- Fix inset colorbar frame reflow for refaspect by @cvanelteren in #593
- Exclude ultraplot/demos.py from coverage reports by @cvanelteren in #602
- Fix contour level color mapping with explicit limits by @cvanelteren in #599
Full Changelog: V2.1.0...V2.1.2
V2.1.0: Tricontour fix projections
This release hotfixes two bugs.
- It fixes a bug where the dpi would be changed by external packages that create figures using matplotlib axes
- It fixes a bug where the projection was assumed to be
PlateCareefor tri-related functions
What's Changed
- Update build-states to new test-map.yml by @cvanelteren in #590
- Fix/preserve dpi draw without rendering by @cvanelteren in #591
- Fix cartopy tri default transform for Triangulation inputs by @cvanelteren in #595
Full Changelog: v2.0.1...V2.1.0
V2.0.1 : New Plot Types, Semantic Legends, More flexible Colorbars and Smarter Layouts
UltraPlot v2.0.1
UltraPlot v2.0.1 is our biggest release yet. Since v1.72.0, we have rebuilt core parts of the library around semantic legends, more reliable layout behavior, stronger guide architecture, and a much more stable CI pipeline. We also launched a brand-new documentation site at https://ultraplot.readthedocs.io/ with a gallery that gives a birdβs-eye view of Matplotlibβs key capabilities through UltraPlot. On the performance side, import times are significantly lower thanks to a new lazy-loading system. And for complex figure composition, sharing logic is now smarter about which axes should be linked, so multi-panel layouts behave more predictably with less manual tweaking.
snippet
import numpy as np
import ultraplot as uplt
rng = np.random.default_rng(7)
fig, ax = uplt.subplots(refwidth=4, refheight = 2)
t = np.linspace(0.0, 8.0 * np.pi, 700)
signal = 0.50 * np.sin(t) + 0.20 * np.sin(0.35 * t + 0.8)
trend = 0.55 * np.cos(0.50 * t)
ax.plot(t, signal, c="blue7", lw=2.2, label="Signal", zorder=-1)
ax.plot(t, trend, c="gray6", lw=1.4, ls="--", alpha=0.8, label="Trend", zorder=-1)
ax.fill_between(t, signal - 0.11, signal + 0.11, color="blue2", alpha=0.30, lw=0)
cats = np.array(["A", "B", "C"])
cat_markers = {"A": "o", "B": "s", "C": "^"}
cat_colors = {"A": "blue7", "B": "orange7", "C": "green7"}
n = 85
xp = np.sort(rng.choice(t, size=n, replace=False))
cp = rng.choice(cats, size=n, p=[0.35, 0.40, 0.25])
yp = np.interp(xp, t, signal)
amp = np.interp(xp, t, np.abs(signal))
sizes = 24 + 220 * amp
score = np.clip(0.15 + 0.85 * amp + 0.07 * rng.normal(size=n), 0, 1)
for cat in cats:
mask = cp == cat
ax.scatter(
xp[mask],
yp[mask],
c=score[mask],
cmap="viko",
vmin=0,
vmax=1,
s=sizes[mask],
marker=cat_markers[cat],
ec="black",
lw=0.45,
alpha=0.9,
)
ax.curvedtext(
t,
signal + 0.16,
"UltraPlot v2.0",
ha="center",
va="bottom",
color="black",
size=10,
weight="bold",
)
ax.format(
title="Semantic Legends + Curved Text + Smart Layout",
xlabel="Phase",
ylabel="Amplitude",
xlim=(0, 8 * np.pi),
ylim=(-1.2, 1.2),
grid=True,
gridalpha=0.22,
)
ax.catlegend(
cats,
colors=cat_colors,
markers=cat_markers,
line=False,
loc="l",
title="Category",
frameon=False,
handle_kw={"ms": 8.5, "ec": "black", "mew": 0.8},
ncols=1,
)
ax.sizelegend(
[25, 90, 180],
color="gray7",
loc="b",
align="l",
title="Magnitude",
frameon=False,
handle_kw={"ec": "black", "linewidths": 0.8},
)
ax.numlegend(
vmin=0,
vmax=1,
n=5,
cmap="viko",
loc="r",
align="b",
title="Score",
frameon=False,
handle_kw={"edgecolors": "black", "linewidths": 0.4},
ncols=1,
)
ax.entrylegend(
[
("Reference", {"line": True, "lw": 2.2, "ls": "-", "c": "blue7"}),
("Samples", {"line": False, "m": "o", "ms": 7, "fc": "white", "ec": "black"}),
],
loc="r",
align="t",
title="Glyph key",
frameon=False,
ncols=1,
)
inax = ax.inset((0.75, 0.75, 0.2, 0.2), zoom=0, projection="ortho")
inax.format(land=1, ocean=1, landcolor="mushroom", oceancolor="ocean blue")
fig.show()Highlights
- New Layout Solver. We have replaced the layout solver to provide snappier, and better layout handling to make the even tighter.
- New semantic legend system with categorical, size, numeric, and geographic legend builders (#586).
- New legend primitives: LegendEntry and improved legend handling for wedge/pie artists (#571).
- Major legend internals refactor via a dedicated UltraLegend builder (#570).
- Colorbar architecture refactor: colorbars are now decoupled from axes internals through UltraColorbar and UltraColorbarLayout (#529).
New Features
- Top-aligned ribbon flow plot type (#559).
- Curved annotation support (#550).
- Ridgeline histogram histtype support (#557).
- Compatibility-aware auto-share defaults (#560).
- PyCirclize integration for circular/network workflows (#495).
Layout, Rendering, and Geo Improvements
- Multiple UltraLayout fixes for spanning axes, gaps, and shared labels (#555, #532, #584).
- Improved inset colorbar frame handling (#554 and related follow-ups).
- Better suptitle spacing in non-bottom vertical alignments (#574).
- Polar tight-layout fixes (#534).
- Geo tick/label robustness improvements (#579, related geo labeling fixes).
- Opt-in subplot pixel snapping plus follow-up adjustments (#561, #567).
Stability, Tooling, and Compatibility
- Python 3.14 support (#385).
- Improved CI matrix coverage and determinism (#587, #580, #577, #545 and related CI fixes).
- pytest-mpl style/baseline stabilization and improved test selection behavior (#528, #533, #535).
- Docs and theme updates, including warnings cleanup and presentation improvements (#585, #552).
Upgrade Notes
- Legend and colorbar internals were significantly refactored. Public usage remains familiar, but extensions relying on internals should be
reviewed. - Semantic legends now have a clearer API surface and are ready for richer per-entry styling workflows.
Full Changelog
v1.72.0...v2.0.1
v1.72.0...v2.0.1
UltraPlot v1.72.0: Sankey diagrams and Ternary plots
This release is marked by the addition of Sankey diagrams and ternary plots (powered by mpltern).
Sankey diagrams
Sankey diagrams are flow charts that visualize the movement of quantities (like energy, money, or users) between different stages or categories, where the width of the connecting arrows is proportional to the flow's magnitude, making major transfers visually obvious. Named after Captain Sankey, they effectively show distributions, energy efficiency, material flows, user journeys, and budget breakdowns, helping to identify dominant paths within a system
Ternary plots
A ternary plot (also known as a ternary graph, triangle plot, or simplex plot) is a barycentric plot on an equilateral triangle. It is used to represent the relative proportions of three variables that sum to a constantβusually 100% or 1.0.
Because the three variables are interdependent (if you know the value of two, the third is automatically determined), a 3D dataset can be visualized in a 2D space without losing information. The plot is commonly used in field such as (evollutionary) game theory. We are harnessing the power of mpltern by wrapping their axes ax external. This gives the best of both worlds where the functionality of the ternary plot is provided by mpltern while allow thing formatting flexibility of UltraPlot.
Note that this feature is introduced now, but marked as experimental. The underlying changes are embedding a different axes inside a container, and there are likely for bugs to emerge from this -- so any feedback or reports are highly appreciated.
snippet
import mpltern
from mpltern.datasets import get_shanon_entropies, get_spiral
import ultraplot as uplt, numpy as np
import networkx as nx
t, l, r, v = get_shanon_entropies()
layout = [[1, 3], [2, 3]]
fig, ax = uplt.subplots(layout, projection=["cartesian", "ternary", "cartesian"], share = 0, hspace = 10)
# Show some noise
ax[0].imshow(np.random.rand(10, 10), cmap = "Fire", colorbar = "r",
colorbar_kw = dict(title = "Random\nnoise", length = 0.333, align = "t"),)
# Ternary plot mock data
vmin = 0.0
vmax = 1.0
levels = np.linspace(vmin, vmax, 7)
cs = ax[1].tripcolor(t, l, r, v, cmap="lapaz_r", shading="flat", vmin=vmin, vmax=vmax)
ax[1].plot(*get_spiral(), color="white", lw=1.25)
colorbar = ax[1].colorbar(
cs,
loc="b",
align="c",
title="Entropy",
length=0.33,
)
# Show a network
g = nx.random_geometric_graph(101, 0.2, seed = 1)
nc = []
min_deg = min(g.degree(), key=lambda x: x[1])[1]
max_deg = max(g.degree(), key=lambda x: x[1])[1]
for node in g.nodes():
intensity = (g.degree(node) - min_deg)/ (max_deg - min_deg)
nc.append(uplt.Colormap("plasma")(intensity))
ax[2].graph(g, node_kw = dict(node_size = 32, node_color = nc))
ax.format(title = ["Hello", "there", "world!"], abc = True)
fig.show()What's Changed
- Add two files and one folder from doc building to git ignoring by @gepcel in #482
- Refactor format in sensible blocks by @cvanelteren in #484
- Fix: Correct label size calculation in _update_outer_abc_loc by @cvanelteren in #485
- Fix: Isolate format() scope for insets and panel by @cvanelteren in #486
- Fix: Move locator back to top level by @cvanelteren in #490
- Fix baseline cache invalidation for PR comparisons by @cvanelteren in #492
- Fix/baseline cache refresh 2 by @cvanelteren in #493
- Feature: Sankey diagrams by @cvanelteren in #478
- Feature: Add container to encapsulate external axes by @cvanelteren in #422
- Fix font lazy load by @cvanelteren in #498
- Fix test_get_size_inches_rounding_and_reference_override by @cvanelteren in #499
- Remove -x from mpl pytest runs by @cvanelteren in #500
- Ci test selection by @cvanelteren in #502
- Update GitHub workflows by @cvanelteren in #505
- CI: make baseline comparison non-blocking by @cvanelteren in #508
- CI: remove redundant pytest run by @cvanelteren in #509
- Fix log formatter tickrange crash by @cvanelteren in #507
- Add return to
test_colorbar_log_formatter_no_tickrange_errorby @cvanelteren in #510 - CI: set default mpl image tolerance by @cvanelteren in #511
- Fix pytest-mpl default tolerance hook by @cvanelteren in #512
- Delegate external axes methods with guardrails by @cvanelteren in #514
- Fix ternary tri* delegation and add example by @cvanelteren in #513
Full Changelog: v1.71.0...v1.72.0
UltraPlot v1.71: Ridgelines and Smarter Legends β¨
This release focuses on two user-facing improvements: a new ridgeline plot type and more flexible figure-level legend placement.
Under the hood, import-time work shifted from eager loading to lazy loading,
cutting startup overhead by about 98%.
Highlights
- Ridgeline (joyplot) support for stacked distribution comparisons.
- Figure-level legends now accept
ref=for span inference and consistent placement. - External context mode for integration-heavy workflows where UltraPlot should
defer on-the-fly guide creation. - New Copernicus journal width presets to standardize publication sizing.
- Faster startup via lazy-loading of top-level imports.
snippet
from pathlib import Path
import numpy as np
import ultraplot as uplt
outdir = Path("release_assets/v1.71.0")
outdir.mkdir(parents=True, exist_ok=True)Ridgeline plots
Ridgeline plots (joyplots) are now built-in. This example uses KDE ridges with
a colormap and overlap control.
snippet
rng = np.random.default_rng(12)
data = [rng.normal(loc=mu, scale=0.9, size=1200) for mu in range(5)]
labels = [f"Group {i + 1}" for i in range(len(data))]
fig, ax = uplt.subplots(refwidth="11cm", refaspect=1.6)
ax.ridgeline(
data,
labels=labels,
cmap="viridis",
overlap=0.65,
alpha=0.8,
linewidth=1.1,
)
ax.format(
xlabel="Value",
ylabel="Group",
title="Ridgeline plot with colormap",
)
fig.savefig(outdir / "ridgeline.png", dpi=200)Figure-level legend placement with ref=
Figure legends can now infer their span from a reference axes or axes group.
This removes the need to manually calculate span, rows, or cols for many
layouts.
snippet
x = np.linspace(0, 2 * np.pi, 256)
layout = [[1, 2, 3], [1, 4, 5]]
fig, axs = uplt.subplots(layout)
cycle = uplt.Cycle("bmh")
for idx, axi in enumerate(axs):
axi.plot(
x,
np.sin((idx + 1) * x),
color=cycle.get_next()["color"],
label=f"sin({idx+1}x)",
)
axs.format(xlabel="x", ylabel=r"sin($\alpha x)")
# Place legend of the first 2 axes on the bottom of the last plot
fig.legend(ax=axs[:2], ref=axs[-1], loc="bottom", ncols=2, frame=False)
# Place legend of the last 2 plots on the bottom of the first column
fig.legend(ax=axs[-2:], ref=axs[:, 1], loc="left", ncols=1, frame=False)
# Collect all labels in a singular legend
fig.legend(ax=axs, loc="bottom", frameon=0)
fig.savefig(outdir / "legend_ref.png", dpi=200)