Releases: Ultraplot/UltraPlot
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)π UltraPlot v1.70.0: Smart Layouts, Better Maps, and Scientific Publishing Support
High-Level Overview: This release focuses on intelligent layout management, geographic plotting enhancements, and publication-ready features. Geographic plots receive improved boundary label handling and rotation capabilities, while new Copernicus Publications standard widths support scientific publishing workflows. Various bug fixes and documentation improvements round out this release.
Major Changes:
1. Geographic Plot Enhancements
# Improved boundary labels and rotation
fig, ax = uplt.subplots(projection="cyl")
ax.format(
lonlim=(-180, 180),
latlim=(-90, 90),
lonlabelrotation=45, # new parameter
labels=True,
land=True,
)
# Boundary labels now remain visible and can be rotated2. Copernicus Publications Support
# New standard figure widths for scientific publishing
fig = uplt.figure(journal = "cop1")
# Automatically sets appropriate width for Copernicus Publications3. Legend Placement Improvements
import numpy as np
import ultraplot as uplt
np.random.seed(0)
fig, ax = uplt.subplots(ncols=2, nrows=2)
handles = []
for idx, axi in enumerate(ax):
noise = np.random.randn(100) * idx
angle = np.random.rand() * 2 * np.pi
t = np.linspace(0, 2 * np.pi, noise.size)
y = np.sin(t * angle) + noise[1]
(h,) = axi.plot(t, y, label=f"$f_{idx}$")
handles.append(h)
# New: spanning legends
fig.legend(handles=handles, ax=ax[0, :], span=(1, 2), loc="b")
fig.show()What's Changed
- Bump actions/checkout from 5 to 6 in the github-actions group by @dependabot[bot] in #415
- [pre-commit.ci] pre-commit autoupdate by @pre-commit-ci[bot] in #416
- Add placement of legend to axes within a figure by @cvanelteren in #418
- There's a typo about zerotrim in doc. by @gepcel in #420
- Fix references in documentation for clarity by @gepcel in #421
- fix links to apply_norm by @cvanelteren in #423
- [Feature] add lon lat labelrotation by @cvanelteren in #426
- Fix: Boundary labels now visible when setting lonlim/latlim by @cvanelteren in #429
- Add Copernicus Publications figure standard widths by @Holmgren825 in #433
- Fix 2D indexing for gridpec by @cvanelteren in #435
- Fix GeoAxes panel alignment with aspect-constrained projections by @cvanelteren in #432
- Bump the github-actions group with 2 updates by @dependabot[bot] in #444
- Fix dualx alignment on log axes by @cvanelteren in #443
- Subset label sharing and implicit slice labels for axis groups by @cvanelteren in #440
- Preserve log formatter when setting log scales by @cvanelteren in #437
- Feature: added inference of labels for spanning legends by @cvanelteren in #447
New Contributors
- @gepcel made their first contribution in #420
- @Holmgren825 made their first contribution in #433
Full Changelog: v1.66.0...v1.70.0
New feature: External Contexts, and bug splats π
Release Notes
This release introduces two key improvements to enhance compatibility and consistency.
External Contexts
UltraPlot provides sensible defaults by controlling matplotlib's internal mechanics and applying overrides when needed. While this approach works well in isolation, it can create conflicts when integrating with external libraries.
We've introduced a new external context that disables UltraPlot-specific features when working with third-party libraries. Currently, this context prevents conflicts with internally generated labels in Seaborn plots. We plan to extend this functionality to support broader library compatibility in future releases.
Example usage with Seaborn:
import seaborn as sns
import ultraplot as uplt
# Load example dataset
tips = sns.load_dataset("tips")
# Use external context to avoid label conflicts
fig, ax = uplt.subplots()
with ax.external():
sns.lineplot(data=tips, x="size", y="total_bill", hue="day", ax = ax)Standardized Binning Functions
We've standardized the default aggregation function across all binning operations to use sum. This change affects hexbin, which previously defaulted to averaging values. All binning functions now consistently use sum as the default, though you can specify any custom aggregation function via the reduce_C_function parameter.
What's Changed
- Hotfix: unsharing causes excessive draw in jupyter by @cvanelteren in #411
- Hotfix: bar labels cause limit to reset for unaffected axis. by @cvanelteren in #413
- fix: change default
reduce_C_functiontonp.sumforhexbinby @beckermr in #408 - Add external context mode for axes by @cvanelteren in #406
Full Changelog: v1.65.1...v1.66.0
Hot-fix: add minor issue where boxpct was not parsed properly
What's Changed
- Bump the github-actions group with 2 updates by @dependabot[bot] in #398
- Fix missing s on input parsing for boxpercentiles by @cvanelteren in #400
Full Changelog: v1.65.0...v1.65.1
Enhanced Grid Layouts and Multi-Span Colorbars
π¨ UltraPlot v1.65 release notes
This release introduces substantial improvements to subplot layout flexibility and configuration management for scientific visualization.
Key Features
Non-Rectangular Grid Layouts with Side Labels (#376)
Asymmetric subplot arrangements now support proper axis labeling, enabling complex multi-panel figures without manual positioning workarounds.
Multi-Span Colorbars (#394)
Colorbars can span multiple subplots, eliminating redundant color scales in comparative visualizations.
RC-Configurable Color Cycles (#378)
Cycle objects can be set via rc configuration, enabling consistent color schemes across figures and projects.
Improved Label Sharing (#372, #387)
Enhanced logic for axis label sharing in complex grid configurations with expanded test coverage.
Infrastructure
- Automatic version checking (#377). Users can now get informed when a new version is available by setting
uplt.rc["ultraplot.check_for_latest_version"] = Truewhich will drop a warning if a newer version is available. - Demo gallery unit tests (#386)
- Optimized CI/CD workflow (#388, #389, #390, #391)
Impact
These changes address common pain points in creating publication-quality multi-panel figures, particularly for comparative analyses requiring consistent styling and efficient use of figure space.
What's Changed
- Allow non-rectangular grids to use side labels by @cvanelteren in #376
- Test/update label sharing tests by @cvanelteren in #372
- Add version checker for UltraPlot by @cvanelteren in #377
- Feature: allow cycle objects to be set on rc by @cvanelteren in #378
- Add unittest for demos by @cvanelteren in #386
- Increase timeout on GHA by @cvanelteren in #388
- bump to 60 minutes by @cvanelteren in #389
- Skip test_demos on gha by @cvanelteren in #391
- Hotfix: minor update in sharing logic by @cvanelteren in #387
- Housekeeping for
ultraplot-build.ymlby @cvanelteren in #390 - Feature: Allow multi-span colorbars by @cvanelteren in #394
Full Changelog: v1.63.0...v1.65.0