From 422fdccdd526c7162afae89220fef6dd18df8e18 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 14 Jan 2026 07:06:44 +1000 Subject: [PATCH 01/29] Add gallery infrastructure and examples Includes: - Configuration updates in conf.py for sphinx-gallery - New gallery examples in docs/examples/ - Updates to documentation styling (custom.css/js) and index.rst --- README.rst | 1 + docs/_static/custom.css | 58 +++++++ docs/_static/custom.js | 158 ++++++++++++++++-- docs/conf.py | 53 +++++- docs/examples/README.txt | 6 + docs/examples/colors/01_cycle_colormap.py | 41 +++++ docs/examples/colors/02_diverging_colormap.py | 46 +++++ docs/examples/colors/README.txt | 4 + docs/examples/geo/01_robin_tracks.py | 37 ++++ docs/examples/geo/02_orthographic_views.py | 46 +++++ docs/examples/geo/03_projections_features.py | 44 +++++ docs/examples/geo/README.txt | 4 + docs/examples/layouts/01_shared_axes_abc.py | 43 +++++ .../layouts/02_complex_layout_insets.py | 63 +++++++ docs/examples/layouts/03_spanning_labels.py | 44 +++++ docs/examples/layouts/README.txt | 5 + .../legends_colorbars/01_multi_colorbars.py | 45 +++++ .../02_legend_inset_colorbar.py | 40 +++++ docs/examples/legends_colorbars/README.txt | 4 + docs/examples/plot_types/01_curved_quiver.py | 68 ++++++++ docs/examples/plot_types/02_network_graph.py | 57 +++++++ docs/examples/plot_types/03_lollipop.py | 39 +++++ .../examples/plot_types/04_datetime_series.py | 50 ++++++ docs/examples/plot_types/05_box_violin.py | 38 +++++ docs/examples/plot_types/README.txt | 4 + docs/index.rst | 15 +- environment.yml | 1 + 27 files changed, 996 insertions(+), 18 deletions(-) create mode 100644 docs/examples/README.txt create mode 100644 docs/examples/colors/01_cycle_colormap.py create mode 100644 docs/examples/colors/02_diverging_colormap.py create mode 100644 docs/examples/colors/README.txt create mode 100644 docs/examples/geo/01_robin_tracks.py create mode 100644 docs/examples/geo/02_orthographic_views.py create mode 100644 docs/examples/geo/03_projections_features.py create mode 100644 docs/examples/geo/README.txt create mode 100644 docs/examples/layouts/01_shared_axes_abc.py create mode 100644 docs/examples/layouts/02_complex_layout_insets.py create mode 100644 docs/examples/layouts/03_spanning_labels.py create mode 100644 docs/examples/layouts/README.txt create mode 100644 docs/examples/legends_colorbars/01_multi_colorbars.py create mode 100644 docs/examples/legends_colorbars/02_legend_inset_colorbar.py create mode 100644 docs/examples/legends_colorbars/README.txt create mode 100644 docs/examples/plot_types/01_curved_quiver.py create mode 100644 docs/examples/plot_types/02_network_graph.py create mode 100644 docs/examples/plot_types/03_lollipop.py create mode 100644 docs/examples/plot_types/04_datetime_series.py create mode 100644 docs/examples/plot_types/05_box_violin.py create mode 100644 docs/examples/plot_types/README.txt diff --git a/README.rst b/README.rst index e2b40653a..a92fc3dbf 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,7 @@ Checkout our examples ===================== Below is a gallery showing random examples of what UltraPlot can do, for more examples checkout our extensive `docs `_. +View the full gallery here: `Gallery `_. .. list-table:: :widths: 33 33 33 diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 3732c6131..835939865 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -200,6 +200,64 @@ max-height: calc(100vh - 150px); } +.gallery-filter-controls { + margin: 1rem 0 2rem; + padding: 1rem 1.2rem; + border-radius: 16px; + background: linear-gradient( + 135deg, + rgba(41, 128, 185, 0.08), + rgba(41, 128, 185, 0.02) + ); + box-shadow: + 0 10px 24px rgba(41, 128, 185, 0.18), + 0 2px 6px rgba(41, 128, 185, 0.08); +} + +.gallery-filter-bar { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.gallery-filter-button { + border: 1px solid #c5c5c5; + background-color: #ffffff; + color: #333333; + padding: 0.35rem 0.85rem; + border-radius: 999px; + font-size: 0.9em; + cursor: pointer; + transition: + background-color 0.2s ease, + color 0.2s ease, + border-color 0.2s ease; +} + +.gallery-filter-button.is-active { + background-color: #2980b9; + border-color: #2980b9; + color: #ffffff; +} + +.gallery-section-hidden { + display: none; +} + +section#layouts > h1, +section#layouts > p, +section#legends-and-colorbars > h1, +section#legends-and-colorbars > p, +section#geoaxes > h1, +section#geoaxes > p, +section#plot-types > h1, +section#plot-types > p, +section#colors-and-cycles > h1, +section#colors-and-cycles > p { + display: none; +} + /* Responsive adjustments */ @media screen and (max-width: 1200px) { .right-toc { diff --git a/docs/_static/custom.js b/docs/_static/custom.js index ef54f6847..16a140e82 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -28,9 +28,7 @@ document.addEventListener("DOMContentLoaded", function () { const tocList = toc.querySelector(".right-toc-list"); const tocContent = toc.querySelector(".right-toc-content"); - const tocToggleBtn = toc.querySelector( - ".right-toc-toggle-btn", - ); + const tocToggleBtn = toc.querySelector(".right-toc-toggle-btn"); // Set up the toggle button tocToggleBtn.addEventListener("click", function () { @@ -118,8 +116,7 @@ document.addEventListener("DOMContentLoaded", function () { link.textContent = headerText.trim(); link.className = - "right-toc-link right-toc-level-" + - header.tagName.toLowerCase(); + "right-toc-link right-toc-level-" + header.tagName.toLowerCase(); item.appendChild(link); tocList.appendChild(item); @@ -141,9 +138,7 @@ document.addEventListener("DOMContentLoaded", function () { let smallestDistanceFromTop = Infinity; headerElements.forEach((header) => { - const distance = Math.abs( - header.getBoundingClientRect().top, - ); + const distance = Math.abs(header.getBoundingClientRect().top); if (distance < smallestDistanceFromTop) { smallestDistanceFromTop = distance; currentSection = header.id; @@ -152,9 +147,7 @@ document.addEventListener("DOMContentLoaded", function () { tocLinks.forEach((link) => { link.classList.remove("active"); - if ( - link.getAttribute("href") === `#${currentSection}` - ) { + if (link.getAttribute("href") === `#${currentSection}`) { link.classList.add("active"); } }); @@ -163,6 +156,149 @@ document.addEventListener("DOMContentLoaded", function () { } }); +document.addEventListener("DOMContentLoaded", function () { + const galleryRoot = document.querySelector("#ultraplot-gallery"); + if (galleryRoot) { + const gallerySections = [ + "layouts", + "legends-and-colorbars", + "geoaxes", + "plot-types", + "colors-and-cycles", + ]; + gallerySections.forEach((sectionId) => { + const heading = document.querySelector( + `section#${sectionId} > h1, section#${sectionId} > h2`, + ); + if (heading) { + heading.classList.add("no-toc"); + } + }); + } + + const thumbContainers = Array.from( + document.querySelectorAll(".sphx-glr-thumbcontainer"), + ); + if (thumbContainers.length < 6) { + return; + } + + const topicMap = { + layouts: { label: "Layouts", slug: "layouts" }, + "legends and colorbars": { + label: "Legends & Colorbars", + slug: "legends-colorbars", + }, + geoaxes: { label: "GeoAxes", slug: "geoaxes" }, + "plot types": { label: "Plot Types", slug: "plot-types" }, + "colors and cycles": { label: "Colors", slug: "colors" }, + }; + + const topics = []; + const topicOrder = new Set(); + const originalSections = new Set(); + + function normalize(text) { + return text + .toLowerCase() + .replace(/[^\w\s-]/g, "") + .replace(/\s+/g, " ") + .trim(); + } + + thumbContainers.forEach((thumb) => { + const section = thumb.closest("section"); + const heading = section ? section.querySelector("h1, h2") : null; + const key = heading ? normalize(heading.textContent || "") : ""; + const info = topicMap[key] || { label: "Other", slug: "other" }; + thumb.dataset.topic = info.slug; + if (section) { + originalSections.add(section); + } + if (!topicOrder.has(info.slug) && info.slug !== "other") { + topicOrder.add(info.slug); + topics.push(info); + } + }); + + if (topics.length === 0) { + return; + } + + const firstSection = thumbContainers[0].closest("section"); + const parent = + (firstSection && firstSection.parentNode) || + document.querySelector(".rst-content"); + if (!parent) { + return; + } + + const controls = document.createElement("div"); + controls.className = "gallery-filter-controls"; + + const filterBar = document.createElement("div"); + filterBar.className = "gallery-filter-bar"; + + function makeButton(label, slug) { + const button = document.createElement("button"); + button.type = "button"; + button.className = "gallery-filter-button"; + button.textContent = label; + button.dataset.topic = slug; + return button; + } + + const buttons = [ + makeButton("All", "all"), + ...topics.map((topic) => makeButton(topic.label, topic.slug)), + ]; + + const counts = {}; + thumbContainers.forEach((thumb) => { + const topic = thumb.dataset.topic || "other"; + counts[topic] = (counts[topic] || 0) + 1; + }); + counts.all = thumbContainers.length; + + buttons.forEach((button) => { + const topic = button.dataset.topic; + const count = counts[topic] || 0; + button.textContent = `${button.textContent} (${count})`; + filterBar.appendChild(button); + }); + + const unified = document.createElement("div"); + unified.className = "sphx-glr-thumbnails gallery-unified"; + thumbContainers.forEach((thumb) => unified.appendChild(thumb)); + + controls.appendChild(filterBar); + controls.appendChild(unified); + parent.insertBefore(controls, firstSection); + + originalSections.forEach((section) => { + section.classList.add("gallery-section-hidden"); + }); + document.body.classList.add("gallery-filter-active"); + + function setFilter(slug) { + buttons.forEach((button) => { + button.classList.toggle("is-active", button.dataset.topic === slug); + }); + thumbContainers.forEach((thumb) => { + const matches = slug === "all" || thumb.dataset.topic === slug; + thumb.style.display = matches ? "" : "none"; + }); + } + + buttons.forEach((button) => { + button.addEventListener("click", () => { + setFilter(button.dataset.topic); + }); + }); + + setFilter("all"); +}); + // Debounce function to limit scroll event firing function debounce(func, wait) { let timeout; diff --git a/docs/conf.py b/docs/conf.py index e26d68230..5691be644 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,14 +12,14 @@ # -- Imports and paths -------------------------------------------------------------- # Import statements -import os -import sys import datetime +import os import subprocess -from pathlib import Path +import sys # Surpress warnings from cartopy when downloading data inside docs env import warnings +from pathlib import Path try: from cartopy.io import DownloadWarning @@ -38,6 +38,7 @@ if not hasattr(sphinx.util, "console"): # Create a compatibility layer import sys + import sphinx.util from sphinx.util import logging @@ -62,6 +63,18 @@ def __getattr__(self, name): # Print available system fonts from matplotlib.font_manager import fontManager +from sphinx_gallery.sorting import ExplicitOrder, FileNameSortKey + + +def _reset_ultraplot(gallery_conf, fname): + """ + Reset UltraPlot rc state between gallery examples. + """ + try: + import ultraplot as uplt + except Exception: + return + uplt.rc.reset() # -- Project information ------------------------------------------------------- @@ -144,6 +157,7 @@ def __getattr__(self, name): "sphinx_copybutton", # add copy button to code "_ext.notoc", "nbsphinx", # parse rst books + "sphinx_gallery.gen_gallery", ] @@ -165,6 +179,11 @@ def __getattr__(self, name): "_templates", "_themes", "*.ipynb", + "gallery/**/*.codeobj.json", + "gallery/**/*.ipynb", + "gallery/**/*.md5", + "gallery/**/*.py", + "gallery/**/*.zip", "**.ipynb_checkpoints" ".DS_Store", "trash", "tmp", @@ -290,6 +309,28 @@ def __getattr__(self, name): nbsphinx_execute = "auto" +# Sphinx gallery configuration +sphinx_gallery_conf = { + "doc_module": ("ultraplot",), + "examples_dirs": ["examples"], + "gallery_dirs": ["gallery"], + "filename_pattern": r"^((?!sgskip).)*$", + "min_reported_time": 1, + "plot_gallery": "True", + "reset_modules": ("matplotlib", "seaborn", _reset_ultraplot), + "subsection_order": ExplicitOrder( + [ + "examples/layouts", + "examples/legends_colorbars", + "examples/geo", + "examples/plot_types", + "examples/colors", + ] + ), + "within_subsection_order": FileNameSortKey, + "nested_sections": False, +} + # The name of the Pygments (syntax highlighting) style to use. # The light-dark theme toggler overloads this, but set default anyway pygments_style = "none" @@ -309,13 +350,11 @@ def __getattr__(self, name): # html_theme = "sphinx_rtd_theme" html_theme_options = { "logo_only": True, - "display_version": False, "collapse_navigation": True, "navigation_depth": 4, "prev_next_buttons_location": "bottom", # top and bottom "includehidden": True, "titles_only": True, - "display_toc": True, "sticky_navigation": True, } @@ -330,7 +369,9 @@ def __getattr__(self, name): # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. -# html_sidebars = {} +html_sidebars = { + "gallery/index": ["globaltoc.html", "searchbox.html"], +} # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 diff --git a/docs/examples/README.txt b/docs/examples/README.txt new file mode 100644 index 000000000..e57dc8e0a --- /dev/null +++ b/docs/examples/README.txt @@ -0,0 +1,6 @@ +UltraPlot Gallery +================= + +Curated examples that highlight what UltraPlot does beyond base Matplotlib: +complex layouts, advanced legends and colorbars, GeoAxes, and specialized plot types. +Each script renders a publication-style figure and becomes a gallery entry. diff --git a/docs/examples/colors/01_cycle_colormap.py b/docs/examples/colors/01_cycle_colormap.py new file mode 100644 index 000000000..c0715559b --- /dev/null +++ b/docs/examples/colors/01_cycle_colormap.py @@ -0,0 +1,41 @@ +""" +Colormap-driven cycles +====================== + +Generate a publication-style line stack using a colormap cycle. + +Why UltraPlot here? +------------------- +UltraPlot exposes ``Cycle`` for colormap-driven property cycling, making it easy +to coordinate color and style across a line family. This is more ergonomic than +manual cycler setup in Matplotlib. + +Key functions: :py:class:`ultraplot.Cycle`, :py:meth:`ultraplot.axes.PlotAxes.plot`. + +See also +-------- +* :doc:`Cycles ` +* :doc:`Colormaps ` +""" + +import numpy as np + +import ultraplot as uplt + +x = np.linspace(0, 2 * np.pi, 300) +phases = np.linspace(0, 1.2, 7) +cycle = uplt.Cycle("Sunset", len(phases), left=0.1, right=0.9) + +fig, ax = uplt.subplots(refwidth=3.4) +for i, phase in enumerate(phases): + y = np.sin(x + phase) * np.exp(-0.08 * x * i) + ax.plot(x, y, lw=2, cycle=cycle, cycle_kw={"N": len(phases)}) + +ax.format( + title="Colormap-driven property cycle", + xlabel="x", + ylabel="Amplitude", + grid=False, +) + +fig.show() diff --git a/docs/examples/colors/02_diverging_colormap.py b/docs/examples/colors/02_diverging_colormap.py new file mode 100644 index 000000000..1eea6fd30 --- /dev/null +++ b/docs/examples/colors/02_diverging_colormap.py @@ -0,0 +1,46 @@ +""" +Diverging colormap +================== + +Use a diverging colormap with centered normalization. + +Why UltraPlot here? +------------------- +UltraPlot can automatically detect diverging datasets (spanning negative and +positive values) and apply a diverging colormap with a centered normalizer. +This ensures the "zero" point is always at the center of the colormap. + +Key functions: :py:class:`ultraplot.colors.DivergingNorm`, :py:meth:`ultraplot.axes.PlotAxes.pcolormesh`. + +See also +-------- +* :doc:`Colormaps ` +* :doc:`Normalizers ` +""" + +import numpy as np + +import ultraplot as uplt + +# Generate data with negative and positive values +x = np.linspace(-5, 5, 100) +y = np.linspace(-5, 5, 100) +X, Y = np.meshgrid(x, y) +Z = np.sin(X) * np.cos(Y) + 0.5 * np.cos(X * 2) + +fig, axs = uplt.subplots(ncols=2, refwidth=3) + +# 1. Automatic diverging +# UltraPlot detects Z spans -1 to +1 and uses the default diverging map +m1 = axs[0].pcolormesh(X, Y, Z, cmap="Div", colorbar="b") +axs[0].format(title="Automatic diverging", xlabel="x", ylabel="y") + +# 2. Manual control +# Use a specific diverging map and center it at a custom value +m2 = axs[1].pcolormesh( + X, Y, Z + 0.5, cmap="ColdHot", div=True, vcenter=0.5, colorbar="b" +) +axs[1].format(title="Manual center at 0.5", xlabel="x", ylabel="y") + +axs.format(suptitle="Diverging colormaps and normalizers") +fig.show() diff --git a/docs/examples/colors/README.txt b/docs/examples/colors/README.txt new file mode 100644 index 000000000..33c598ac3 --- /dev/null +++ b/docs/examples/colors/README.txt @@ -0,0 +1,4 @@ +Colors and Cycles +================= + +Colormaps and property cycles tuned for publication figures. diff --git a/docs/examples/geo/01_robin_tracks.py b/docs/examples/geo/01_robin_tracks.py new file mode 100644 index 000000000..fc2b92493 --- /dev/null +++ b/docs/examples/geo/01_robin_tracks.py @@ -0,0 +1,37 @@ +""" +Robinson projection tracks +========================== + +Global tracks plotted on a Robinson projection without external datasets. + +Why UltraPlot here? +------------------- +UltraPlot creates GeoAxes with a single ``proj`` keyword and formats +geographic gridlines and features with ``format``. This avoids the verbose +cartopy setup typically needed in Matplotlib. + +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.axes.GeoAxes.format`. + +See also +-------- +* :doc:`Geographic projections ` +""" + +import cartopy.crs as ccrs +import numpy as np + +import ultraplot as uplt + +lon = np.linspace(-180, 180, 300) +lat_a = 25 * np.sin(np.deg2rad(lon * 1.5)) +lat_b = -15 * np.cos(np.deg2rad(lon * 1.2)) + +fig, ax = uplt.subplots(proj="robin", proj_kw={"lon0": 0}, refwidth=4) +ax.plot(lon, lat_a, transform=ccrs.PlateCarree(), lw=2, label="Track A") +ax.plot(lon, lat_b, transform=ccrs.PlateCarree(), lw=2, label="Track B") +ax.scatter([-140, -40, 60, 150], [10, -20, 30, -5], transform=ccrs.PlateCarree()) + +ax.format(title="Global trajectories", lonlines=60, latlines=30) +ax.legend(loc="lower left", frame=False) + +fig.show() diff --git a/docs/examples/geo/02_orthographic_views.py b/docs/examples/geo/02_orthographic_views.py new file mode 100644 index 000000000..3ab521845 --- /dev/null +++ b/docs/examples/geo/02_orthographic_views.py @@ -0,0 +1,46 @@ +""" +Orthographic comparison +======================= + +Two orthographic views of the same signal to emphasize projection control. + +Why UltraPlot here? +------------------- +UltraPlot handles multiple projections in one figure with a consistent API +and shared formatting calls. This makes side-by-side map comparisons simple. + +Key functions: :py:func:`ultraplot.figure.Figure.subplot`, :py:meth:`ultraplot.axes.GeoAxes.format`. + +See also +-------- +* :doc:`Geographic projections ` +""" + +import cartopy.crs as ccrs +import numpy as np + +import ultraplot as uplt + +lon = np.linspace(-180, 180, 220) +lat = 20 * np.sin(np.deg2rad(lon * 2.2)) + +fig = uplt.figure(refwidth=3, share=0) +ax1 = fig.subplot(121, proj="ortho", proj_kw={"lon0": -100, "lat0": 35}) +ax2 = fig.subplot(122, proj="ortho", proj_kw={"lon0": 80, "lat0": -15}) + +for ax, title in zip([ax1, ax2], ["Western Hemisphere", "Eastern Hemisphere"]): + ax.plot(lon, lat, transform=ccrs.PlateCarree(), lw=2, color="cherry red") + ax.scatter(lon[::40], lat[::40], transform=ccrs.PlateCarree(), s=30) + ax.format( + lonlines=60, + latlines=30, + title=title, + land=True, + ocean=True, + oceancolor="ocean blue", + landcolor="mushroom", + ) + +fig.format(suptitle="Orthographic views of a global track") + +fig.show() diff --git a/docs/examples/geo/03_projections_features.py b/docs/examples/geo/03_projections_features.py new file mode 100644 index 000000000..6d916535b --- /dev/null +++ b/docs/examples/geo/03_projections_features.py @@ -0,0 +1,44 @@ +""" +Map projections and features +============================ + +Compare different map projections and add geographic features. + +Why UltraPlot here? +------------------- +UltraPlot's :class:`~ultraplot.axes.GeoAxes` supports many projections via +``proj`` and makes adding features like land, ocean, and borders trivial +via :meth:`~ultraplot.axes.GeoAxes.format`. + +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.axes.GeoAxes.format`. + +See also +-------- +* :doc:`Geographic projections ` +""" + +import ultraplot as uplt + +# Projections to compare +projs = ["mollweide", "ortho", "kav7"] + +fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3) + +# Format all axes with features +# land=True, coast=True, etc. are shortcuts for adding cartopy features +axs.format( + land=True, + landcolor="bisque", + ocean=True, + oceancolor="azure", + coast=True, + borders=True, + labels=True, + suptitle="Projections and features", +) + +axs[0].format(title="Mollweide") +axs[1].format(title="Orthographic") +axs[2].format(title="Kavrayskiy VII") + +fig.show() diff --git a/docs/examples/geo/README.txt b/docs/examples/geo/README.txt new file mode 100644 index 000000000..edbf0a140 --- /dev/null +++ b/docs/examples/geo/README.txt @@ -0,0 +1,4 @@ +GeoAxes +======= + +Geographic projections with clean defaults and minimal boilerplate. diff --git a/docs/examples/layouts/01_shared_axes_abc.py b/docs/examples/layouts/01_shared_axes_abc.py new file mode 100644 index 000000000..d3334d132 --- /dev/null +++ b/docs/examples/layouts/01_shared_axes_abc.py @@ -0,0 +1,43 @@ +""" +Shared axes and ABC labels +========================= + +A multi-panel layout with shared limits, shared labels, and automatic panel labels. + +Why UltraPlot here? +------------------- +UltraPlot shares limits and labels across a grid with a single ``share``/``span`` +configuration, and adds panel letters automatically. This keeps complex layouts +consistent without the manual axis management required in base Matplotlib. + +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. + +See also +-------- +* :doc:`Subplots and layouts ` +""" + +import numpy as np + +import ultraplot as uplt + +rng = np.random.default_rng(12) +x = np.linspace(0, 10, 300) + +fig, axs = uplt.subplots(nrows=2, ncols=3, share="limits", span=True, refwidth=1.7) +for i, ax in enumerate(axs): + noise = 0.15 * rng.standard_normal(x.size) + y = np.sin(x + i * 0.4) + 0.2 * np.cos(2 * x) + 0.1 * i + noise + ax.plot(x, y, lw=2) + ax.scatter(x[::30], y[::30], s=18, alpha=0.65) + +axs.format( + abc=True, + abcloc="ul", + xlabel="Time (s)", + ylabel="Signal", + suptitle="Shared axes with consistent limits and panel lettering", + grid=False, +) + +fig.show() diff --git a/docs/examples/layouts/02_complex_layout_insets.py b/docs/examples/layouts/02_complex_layout_insets.py new file mode 100644 index 000000000..72bf9e9c1 --- /dev/null +++ b/docs/examples/layouts/02_complex_layout_insets.py @@ -0,0 +1,63 @@ +""" +Complex layout with insets +========================= + +A mixed layout using blank slots, insets, and multiple plot types. + +Why UltraPlot here? +------------------- +UltraPlot accepts nested layout arrays directly and keeps spacing consistent +across panels and insets. You get a publication-style multi-panel figure without +manual GridSpec bookkeeping. + +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.axes.Axes.inset_axes`. + +See also +-------- +* :doc:`Subplots and layouts ` +* :doc:`Insets and panels ` +""" + +import numpy as np + +import ultraplot as uplt + +rng = np.random.default_rng(7) +layout = [[1, 1, 2, 2], [0, 3, 3, 0], [4, 4, 5, 5]] +fig, axs = uplt.subplots(layout, share=0, refwidth=1.4) + +# Panel A: time series with inset zoom. +x = np.linspace(0, 20, 400) +y = np.sin(x) + 0.3 * np.cos(2.5 * x) + 0.15 * rng.standard_normal(x.size) +axs[0].plot(x, y, lw=2) +axs[0].format(title="Signal with local variability", ylabel="Amplitude") +inset = axs[0].inset_axes([0.58, 0.52, 0.35, 0.35], zoom=0) +mask = (x > 6) & (x < 10) +inset.plot(x[mask], y[mask], lw=1.6, color="black") +inset.format(xlabel="Zoom", ylabel="Amp", grid=False) + +# Panel B: stacked bar chart. +categories = np.arange(1, 6) +vals = rng.uniform(0.6, 1.2, (3, categories.size)).cumsum(axis=0) +axs[1].bar(categories, vals[0], label="Group A") +axs[1].bar(categories, vals[1] - vals[0], bottom=vals[0], label="Group B") +axs[1].bar(categories, vals[2] - vals[1], bottom=vals[1], label="Group C") +axs[1].format(title="Stacked composition", xlabel="Sample", ylabel="Value") +axs[1].legend(loc="right", ncols=1, frame=False) + +# Panel C: heatmap with colorbar. +grid = rng.standard_normal((40, 60)) +image = axs[2].imshow(grid, cmap="Fire", aspect="auto") +axs[2].format(title="Spatial field", xlabel="Longitude", ylabel="Latitude") +axs[2].colorbar(image, loc="r", label="Intensity") + +# Panel D: scatter with trend line. +x = rng.uniform(0, 1, 120) +y = 0.8 * x + 0.2 * rng.standard_normal(x.size) +axs[3].scatter(x, y, s=30, alpha=0.7) +axs[3].plot([0, 1], [0, 0.8], lw=2, color="black", linestyle="--") +axs[3].format(title="Relationship", xlabel="Predictor", ylabel="Response") + +axs.format(abc=True, abcloc="ul", suptitle="Complex layout with insets and mixed plots") + +fig.show() diff --git a/docs/examples/layouts/03_spanning_labels.py b/docs/examples/layouts/03_spanning_labels.py new file mode 100644 index 000000000..ee107e439 --- /dev/null +++ b/docs/examples/layouts/03_spanning_labels.py @@ -0,0 +1,44 @@ +""" +Spanning labels with shared axes +=============================== + +Demonstrate shared labels across a row of related subplots. + +Why UltraPlot here? +------------------- +UltraPlot can span labels across subplot groups while keeping axis limits shared. +This avoids manual ``fig.supxlabel`` placement and reduces label clutter. + +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. + +See also +-------- +* :doc:`Subplots and layouts ` +""" + +import numpy as np + +import ultraplot as uplt + +rng = np.random.default_rng(21) +x = np.linspace(0, 5, 300) + +fig, axs = uplt.subplots(ncols=3, share="labels", span=True, refwidth=2.1) +for i, ax in enumerate(axs): + trend = (i + 1) * 0.2 + y = np.exp(-0.4 * x) * np.sin(2 * x + i * 0.6) + trend + y += 0.05 * rng.standard_normal(x.size) + ax.plot(x, y, lw=2) + ax.fill_between(x, y - 0.15, y + 0.15, alpha=0.2) + ax.set_title(f"Condition {i + 1}") + +axs.format( + xlabel="Time (days)", + ylabel="Normalized response", + abc=True, + abcloc="ul", + suptitle="Spanning labels with shared axes", + grid=False, +) + +fig.show() diff --git a/docs/examples/layouts/README.txt b/docs/examples/layouts/README.txt new file mode 100644 index 000000000..9c488156d --- /dev/null +++ b/docs/examples/layouts/README.txt @@ -0,0 +1,5 @@ +Layouts +======= + +Multi-panel, publication-style layouts that emphasize axis sharing, labels, +and panel annotations. diff --git a/docs/examples/legends_colorbars/01_multi_colorbars.py b/docs/examples/legends_colorbars/01_multi_colorbars.py new file mode 100644 index 000000000..51201dff8 --- /dev/null +++ b/docs/examples/legends_colorbars/01_multi_colorbars.py @@ -0,0 +1,45 @@ +""" +Multi-panel colorbars +===================== + +Column-specific and shared colorbars in a 2x2 layout. + +Why UltraPlot here? +------------------- +UltraPlot places colorbars by row/column with ``fig.colorbar`` so multi-panel +figures can share scales without manual axes placement. This mirrors the +publication layouts often seen in journals. + +Key functions: :py:meth:`ultraplot.figure.Figure.colorbar`, :py:meth:`ultraplot.axes.PlotAxes.pcolormesh`. + +See also +-------- +* :doc:`Colorbars and legends ` +""" + +import numpy as np + +import ultraplot as uplt + +x = np.linspace(-3, 3, 160) +y = np.linspace(-2, 2, 120) +X, Y = np.meshgrid(x, y) + +fig, axs = uplt.subplots(nrows=2, ncols=2, share=0, refwidth=2.1) +data_left = np.sin(X) * np.cos(Y) +data_right = np.cos(0.5 * X) * np.sin(1.2 * Y) + +m0 = axs[0, 0].pcolormesh(X, Y, data_left, cmap="Stellar", shading="auto") +m1 = axs[1, 0].pcolormesh(X, Y, data_left * 0.8, cmap="Stellar", shading="auto") +m2 = axs[0, 1].pcolormesh(X, Y, data_right, cmap="Dusk", shading="auto") +m3 = axs[1, 1].pcolormesh(X, Y, data_right * 1.1, cmap="Dusk", shading="auto") + +axs.format(xlabel="x", ylabel="y", abc=True, abcloc="ul", grid=False) +axs[0, 0].set_title("Field A") +axs[0, 1].set_title("Field B") + +fig.colorbar(m0, loc="b", col=1, label="Column 1 intensity") +fig.colorbar(m2, loc="b", col=2, label="Column 2 intensity") +fig.colorbar(m3, loc="r", rows=(1, 2), label="Shared scale") + +fig.show() diff --git a/docs/examples/legends_colorbars/02_legend_inset_colorbar.py b/docs/examples/legends_colorbars/02_legend_inset_colorbar.py new file mode 100644 index 000000000..aa913330d --- /dev/null +++ b/docs/examples/legends_colorbars/02_legend_inset_colorbar.py @@ -0,0 +1,40 @@ +""" +Legend with inset colorbar +========================== + +Combine a multi-line legend with a compact inset colorbar. + +Why UltraPlot here? +------------------- +UltraPlot supports inset colorbars via simple location codes while keeping +legends lightweight and aligned. This keeps focus on the data without resorting +to manual axes transforms. + +Key functions: :py:meth:`ultraplot.axes.PlotAxes.legend`, :py:meth:`ultraplot.axes.Axes.colorbar`. + +See also +-------- +* :doc:`Colorbars and legends ` +""" + +import numpy as np + +import ultraplot as uplt + +rng = np.random.default_rng(3) +x = np.linspace(0, 4 * np.pi, 400) + +fig, ax = uplt.subplots(refwidth=3.4) +for i, phase in enumerate([0.0, 0.6, 1.2, 1.8]): + ax.plot(x, np.sin(x + phase), lw=2, label=f"Phase {i + 1}") + +scatter_x = rng.uniform(0, x.max(), 80) +scatter_y = np.sin(scatter_x) + 0.2 * rng.standard_normal(scatter_x.size) +depth = np.linspace(0, 1, scatter_x.size) +points = ax.scatter(scatter_x, scatter_y, c=depth, cmap="Fire", s=40, alpha=0.8) + +ax.format(xlabel="Time (s)", ylabel="Amplitude", title="Signals with phase offsets") +ax.legend(loc="upper right", ncols=2, frame=False) +ax.colorbar(points, loc="ll", label="Depth") + +fig.show() diff --git a/docs/examples/legends_colorbars/README.txt b/docs/examples/legends_colorbars/README.txt new file mode 100644 index 000000000..0792605e3 --- /dev/null +++ b/docs/examples/legends_colorbars/README.txt @@ -0,0 +1,4 @@ +Legends and Colorbars +===================== + +Showcase precise placement of legends and colorbars across complex layouts. diff --git a/docs/examples/plot_types/01_curved_quiver.py b/docs/examples/plot_types/01_curved_quiver.py new file mode 100644 index 000000000..cbff06fde --- /dev/null +++ b/docs/examples/plot_types/01_curved_quiver.py @@ -0,0 +1,68 @@ +""" +Curved quiver around a cylinder +=============================== + +Streamline-style arrows showing flow deflection around a cylinder. + +Why UltraPlot here? +------------------- +``curved_quiver`` is an UltraPlot extension that draws smooth, curved arrows +for vector fields while preserving color mapping. This is not available in +base Matplotlib. + +Key functions: :py:meth:`ultraplot.axes.PlotAxes.curved_quiver`, :py:meth:`ultraplot.figure.Figure.colorbar`. + +See also +-------- +* :doc:`2D plot types ` +""" + +import numpy as np + +import ultraplot as uplt + +x = np.linspace(-2.2, 2.2, 26) +y = np.linspace(-1.6, 1.6, 22) +X, Y = np.meshgrid(x, y) + +# Potential flow around a cylinder (radius a=0.5). +U0 = 1.0 +a = 0.5 +R2 = X**2 + Y**2 +R2 = np.where(R2 == 0, np.finfo(float).eps, R2) +U = U0 * (1 - a**2 * (X**2 - Y**2) / (R2**2)) +V = -2 * U0 * a**2 * X * Y / (R2**2) +speed = np.sqrt(U**2 + V**2) + +fig, ax = uplt.subplots(refwidth=3.2) +m = ax.curved_quiver( + X, + Y, + U, + V, + color=speed, + arrow_at_end=True, + scale=100, + arrowsize=1.4, + density=20, + grains=20, + cmap="sciviscoloreven", +) +values = m.lines.get_array() +if values is not None and len(values) > 0: + colors = m.lines.get_cmap()(m.lines.norm(values)) + alpha = (values - values.min()) / (values.max() - values.min() + 1e-12) + colors[:, -1] = 0.15 + 0.85 * alpha + m.lines.set_color(colors) + m.arrows.set_alpha(0.6) +theta = np.linspace(0, 2 * np.pi, 200) +ax.plot(a * np.cos(theta), a * np.sin(theta), color="black", lw=2) +ax.format( + title="Flow around a cylinder", + xlabel="x", + ylabel="y", + aspect=1, +) +fig.colorbar(m.lines, ax=ax, label="Speed") + +fig.show() diff --git a/docs/examples/plot_types/02_network_graph.py b/docs/examples/plot_types/02_network_graph.py new file mode 100644 index 000000000..c02b59d74 --- /dev/null +++ b/docs/examples/plot_types/02_network_graph.py @@ -0,0 +1,57 @@ +""" +Network graph styling +===================== + +Render a network with node coloring by degree and clean styling. + +Why UltraPlot here? +------------------- +UltraPlot wraps NetworkX drawing with a single ``ax.graph`` call and applies +sensible defaults for size, alpha, and aspect. This removes a lot of boilerplate +around layout and styling. + +Key functions: :py:meth:`ultraplot.axes.PlotAxes.graph`, :py:meth:`ultraplot.figure.Figure.colorbar`. + +See also +-------- +* :doc:`Networks ` +""" + +import networkx as nx +import numpy as np + +import ultraplot as uplt + +g = nx.karate_club_graph() +degrees = np.array([g.degree(n) for n in g.nodes()]) + +fig, ax = uplt.subplots(refwidth=3.2) +nodes, edges, labels = ax.graph( + g, + layout="spring", + layout_kw={"seed": 4}, + node_kw={ + "node_color": degrees, + "cmap": "viko", + "edgecolors": "black", + "linewidths": 0.6, + "node_size": 128, + }, + edge_kw={ + "alpha": 0.4, + "width": [np.random.rand() * 4 for _ in range(len(g.edges()))], + }, + label_kw={"font_size": 7}, +) +ax.format(title="Network connectivity", grid=False) +ax.margins(0.15) +fig.colorbar( + nodes, + ax=ax, + loc="r", + label="Node degree", + length=0.33, + align="top", +) + +fig.show() diff --git a/docs/examples/plot_types/03_lollipop.py b/docs/examples/plot_types/03_lollipop.py new file mode 100644 index 000000000..7e655e19c --- /dev/null +++ b/docs/examples/plot_types/03_lollipop.py @@ -0,0 +1,39 @@ +""" +Lollipop comparisons +==================== + +Vertical and horizontal lollipop charts in a publication layout. + +Why UltraPlot here? +------------------- +UltraPlot adds lollipop plot methods that mirror bar plotting while exposing +simple styling for stems and markers. This plot type is not built into +Matplotlib. + +Key functions: :py:meth:`ultraplot.axes.PlotAxes.lollipop`, :py:meth:`ultraplot.axes.PlotAxes.lollipoph`. + +See also +-------- +* :doc:`1D plot types ` +""" + +import numpy as np +import pandas as pd + +import ultraplot as uplt + +rng = np.random.default_rng(11) +categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon", "Zeta"] +values = np.sort(rng.uniform(0.4, 1.3, len(categories))) +data = pd.Series(values, index=categories, name="score") + +fig, axs = uplt.subplots(ncols=2, share=0, refwidth=2.8) +axs[0].lollipop(data, stemcolor="black", marker="o", color="C0") +axs[0].format(title="Vertical lollipop", xlabel="Category", ylabel="Score") + +axs[1].lollipoph(data, stemcolor="black", marker="o", color="C1") +axs[1].format(title="Horizontal lollipop", xlabel="Score", ylabel="Category") + +axs.format(abc=True, abcloc="ul", suptitle="Lollipop charts for ranked metrics") + +fig.show() diff --git a/docs/examples/plot_types/04_datetime_series.py b/docs/examples/plot_types/04_datetime_series.py new file mode 100644 index 000000000..0c7656bae --- /dev/null +++ b/docs/examples/plot_types/04_datetime_series.py @@ -0,0 +1,50 @@ +""" +Calendar-aware datetime series +============================== + +Plot cftime datetimes with UltraPlot's automatic locators and formatters. + +Why UltraPlot here? +------------------- +UltraPlot includes CFTime converters and locators so climate calendars plot +cleanly without manual conversions. This is a common pain point in Matplotlib. + +Key functions: :py:class:`ultraplot.ticker.AutoCFDatetimeLocator`, :py:class:`ultraplot.ticker.AutoCFDatetimeFormatter`. + +See also +-------- +* :doc:`Cartesian plots ` +""" + +import cftime +import matplotlib.units as munits +import numpy as np + +import ultraplot as uplt + +dates = [ + cftime.DatetimeNoLeap(2000 + i // 12, (i % 12) + 1, 1, calendar="noleap") + for i in range(18) +] +values = np.cumsum(np.random.default_rng(5).normal(0.0, 0.6, len(dates))) + +date_type = type(dates[0]) +if date_type not in munits.registry: + munits.registry[date_type] = uplt.ticker.CFTimeConverter() + +fig, ax = uplt.subplots(refwidth=3.6) +ax.plot(dates, values, lw=2, marker="o") + +locator = uplt.ticker.AutoCFDatetimeLocator(calendar="noleap") +formatter = uplt.ticker.AutoCFDatetimeFormatter(locator, calendar="noleap") +ax.xaxis.set_major_locator(locator) +ax.xaxis.set_major_formatter(formatter) + +ax.format( + xlabel="Simulation time", + ylabel="Anomaly (a.u.)", + title="No-leap calendar time series", + xrotation=25, +) + +fig.show() diff --git a/docs/examples/plot_types/05_box_violin.py b/docs/examples/plot_types/05_box_violin.py new file mode 100644 index 000000000..8b626273f --- /dev/null +++ b/docs/examples/plot_types/05_box_violin.py @@ -0,0 +1,38 @@ +""" +Box and violin plots +==================== + +Standard box and violin plots with automatic customization. + +Why UltraPlot here? +------------------- +UltraPlot wraps :meth:`matplotlib.axes.Axes.boxplot` and :meth:`matplotlib.axes.Axes.violinplot` +with more convenient arguments (like ``fillcolor``, ``alpha``) and automatically applies +cycle colors to the boxes/violins. + +Key functions: :py:meth:`ultraplot.axes.PlotAxes.boxplot`, :py:meth:`ultraplot.axes.PlotAxes.violinplot`. + +See also +-------- +* :doc:`1D statistics ` +""" + +import numpy as np + +import ultraplot as uplt + +# Generate sample data +data = [np.random.normal(0, std, 100) for std in range(1, 6)] + +fig, axs = uplt.subplots(ncols=2, refwidth=3) + +# Box plot +axs[0].boxplot(data, lw=1.5, fillcolor="gray4", medianlw=2) +axs[0].format(title="Box plot", xlabel="Distribution", ylabel="Value") + +# Violin plot +axs[1].violinplot(data, lw=1, fillcolor="gray6", fillalpha=0.5) +axs[1].format(title="Violin plot", xlabel="Distribution", ylabel="Value") + +axs.format(suptitle="Statistical distributions") +fig.show() diff --git a/docs/examples/plot_types/README.txt b/docs/examples/plot_types/README.txt new file mode 100644 index 000000000..469ac0f6b --- /dev/null +++ b/docs/examples/plot_types/README.txt @@ -0,0 +1,4 @@ +Plot Types +========== + +Specialized plot types unique to UltraPlot workflows. diff --git a/docs/index.rst b/docs/index.rst index bd55c3882..09613b8b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,6 +5,11 @@ **UltraPlot** is a succinct wrapper around `matplotlib `__ for creating **beautiful, publication-quality graphics** with ease. +**Gallery** +############ +See UltraPlot in action with curated, publication-style examples. +:doc:`Browse the gallery ` + πŸš€ **Key Features** | Create More, Code Less ################### βœ” **Simplified Subplot Management** – Create multi-panel plots effortlessly. @@ -13,7 +18,7 @@ for creating **beautiful, publication-quality graphics** with ease. πŸ“Š **Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. -πŸ“Œ **Get Started** β†’ :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` +πŸ“Œ **Get Started** β†’ :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` | :doc:`Gallery ` -------------------------------------- @@ -121,6 +126,7 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen install why usage + gallery/index .. toctree:: :maxdepth: 1 @@ -143,6 +149,13 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen fonts configuration +.. toctree:: + :maxdepth: 1 + :caption: Gallery + :hidden: + + gallery/index + .. toctree:: :maxdepth: 1 :caption: Reference diff --git a/environment.yml b/environment.yml index 764a47f36..6e1ae20b1 100644 --- a/environment.yml +++ b/environment.yml @@ -17,6 +17,7 @@ dependencies: - pip - pint - sphinx + - sphinx-gallery - nbsphinx - jupytext - sphinx-copybutton From 1a7932ef64ff786d3f42605fbb812ec61b59bc1d Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 14 Jan 2026 07:11:26 +1000 Subject: [PATCH 02/29] Fix kwarg error in diverging colormap example Replaced 'div=True' with 'diverging=True' as 'div' is not a supported alias in pcolormesh kwargs. --- docs/examples/colors/02_diverging_colormap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/colors/02_diverging_colormap.py b/docs/examples/colors/02_diverging_colormap.py index 1eea6fd30..6dc578e73 100644 --- a/docs/examples/colors/02_diverging_colormap.py +++ b/docs/examples/colors/02_diverging_colormap.py @@ -38,7 +38,7 @@ # 2. Manual control # Use a specific diverging map and center it at a custom value m2 = axs[1].pcolormesh( - X, Y, Z + 0.5, cmap="ColdHot", div=True, vcenter=0.5, colorbar="b" + X, Y, Z + 0.5, cmap="ColdHot", diverging=True, vcenter=0.5, colorbar="b" ) axs[1].format(title="Manual center at 0.5", xlabel="x", ylabel="y") From fe368553d674211483198cf497ac2220410d2168 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 14 Jan 2026 08:09:33 +1000 Subject: [PATCH 03/29] Added more intricate examples --- docs/examples/colors/02_diverging_colormap.py | 10 ++++++++-- docs/examples/geo/03_projections_features.py | 2 +- docs/examples/layouts/01_shared_axes_abc.py | 10 ++++++---- docs/examples/layouts/03_spanning_labels.py | 11 +++++++++-- docs/examples/plot_types/05_box_violin.py | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/examples/colors/02_diverging_colormap.py b/docs/examples/colors/02_diverging_colormap.py index 6dc578e73..0766bc4a6 100644 --- a/docs/examples/colors/02_diverging_colormap.py +++ b/docs/examples/colors/02_diverging_colormap.py @@ -32,13 +32,19 @@ # 1. Automatic diverging # UltraPlot detects Z spans -1 to +1 and uses the default diverging map -m1 = axs[0].pcolormesh(X, Y, Z, cmap="Div", colorbar="b") +m1 = axs[0].pcolormesh(X, Y, Z, cmap="Div", colorbar="b", center_levels=True) axs[0].format(title="Automatic diverging", xlabel="x", ylabel="y") # 2. Manual control # Use a specific diverging map and center it at a custom value m2 = axs[1].pcolormesh( - X, Y, Z + 0.5, cmap="ColdHot", diverging=True, vcenter=0.5, colorbar="b" + X, + Y, + Z + 0.5, + cmap="ColdHot", + diverging=True, + colorbar="b", + center_levels=True, ) axs[1].format(title="Manual center at 0.5", xlabel="x", ylabel="y") diff --git a/docs/examples/geo/03_projections_features.py b/docs/examples/geo/03_projections_features.py index 6d916535b..10e7c82b7 100644 --- a/docs/examples/geo/03_projections_features.py +++ b/docs/examples/geo/03_projections_features.py @@ -20,7 +20,7 @@ import ultraplot as uplt # Projections to compare -projs = ["mollweide", "ortho", "kav7"] +projs = ["moll", "ortho", "kav7"] fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3) diff --git a/docs/examples/layouts/01_shared_axes_abc.py b/docs/examples/layouts/01_shared_axes_abc.py index d3334d132..b474a2b1d 100644 --- a/docs/examples/layouts/01_shared_axes_abc.py +++ b/docs/examples/layouts/01_shared_axes_abc.py @@ -10,7 +10,7 @@ configuration, and adds panel letters automatically. This keeps complex layouts consistent without the manual axis management required in base Matplotlib. -Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. +Key functions: :py:func:`ultraplot.ui.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. See also -------- @@ -24,7 +24,10 @@ rng = np.random.default_rng(12) x = np.linspace(0, 10, 300) -fig, axs = uplt.subplots(nrows=2, ncols=3, share="limits", span=True, refwidth=1.7) +layout = [[1, 2, 3], [1, 2, 4], [1, 2, 5]] +fig, axs = uplt.subplots( + layout, +) for i, ax in enumerate(axs): noise = 0.15 * rng.standard_normal(x.size) y = np.sin(x + i * 0.4) + 0.2 * np.cos(2 * x) + 0.1 * i + noise @@ -32,8 +35,7 @@ ax.scatter(x[::30], y[::30], s=18, alpha=0.65) axs.format( - abc=True, - abcloc="ul", + abc="[A.]", xlabel="Time (s)", ylabel="Signal", suptitle="Shared axes with consistent limits and panel lettering", diff --git a/docs/examples/layouts/03_spanning_labels.py b/docs/examples/layouts/03_spanning_labels.py index ee107e439..496191b60 100644 --- a/docs/examples/layouts/03_spanning_labels.py +++ b/docs/examples/layouts/03_spanning_labels.py @@ -23,7 +23,8 @@ rng = np.random.default_rng(21) x = np.linspace(0, 5, 300) -fig, axs = uplt.subplots(ncols=3, share="labels", span=True, refwidth=2.1) +layout = [[1, 2, 5], [3, 4, 5]] +fig, axs = uplt.subplots(layout) for i, ax in enumerate(axs): trend = (i + 1) * 0.2 y = np.exp(-0.4 * x) * np.sin(2 * x + i * 0.6) + trend @@ -32,8 +33,13 @@ ax.fill_between(x, y - 0.15, y + 0.15, alpha=0.2) ax.set_title(f"Condition {i + 1}") -axs.format( +# Share first 2 plots top left +axs[:2].format( xlabel="Time (days)", +) +axs[1, :2].format(xlabel="Time 2 (days)") +axs[-1].format(xlabel="Time 3 (days)") +axs.format( ylabel="Normalized response", abc=True, abcloc="ul", @@ -41,4 +47,5 @@ grid=False, ) + fig.show() diff --git a/docs/examples/plot_types/05_box_violin.py b/docs/examples/plot_types/05_box_violin.py index 8b626273f..5f2adaf87 100644 --- a/docs/examples/plot_types/05_box_violin.py +++ b/docs/examples/plot_types/05_box_violin.py @@ -31,7 +31,7 @@ axs[0].format(title="Box plot", xlabel="Distribution", ylabel="Value") # Violin plot -axs[1].violinplot(data, lw=1, fillcolor="gray6", fillalpha=0.5) +axs[1].violinplot(data, lw=1, fillcolor="gray6") axs[1].format(title="Violin plot", xlabel="Distribution", ylabel="Value") axs.format(suptitle="Statistical distributions") From 1153861ab689c5e2db8230671f244830a15ab6b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:19:10 +0000 Subject: [PATCH 04/29] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index 3e7cca168..cf524e2c7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,6 +76,7 @@ def _reset_ultraplot(gallery_conf, fname): return uplt.rc.reset() + # -- Project information ------------------------------------------------------- # The basic info project = "UltraPlot" From acefe254c8246fb0a88de4dd292d5c79d2f7cebb Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 14 Jan 2026 09:38:46 +1000 Subject: [PATCH 05/29] Update deps --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 904673230..299d6b06e 100644 --- a/environment.yml +++ b/environment.yml @@ -32,6 +32,6 @@ dependencies: - pyarrow - cftime - m2r2 - - lxml-html-clean + - lxml_html_clean - pip: - git+https://github.com/ultraplot/UltraTheme.git From 4dd3d3ac25f7a206afae76b8481f4c37fd968d89 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 02:34:28 +1000 Subject: [PATCH 06/29] Fix gallery filter and whats new toc --- docs/_static/custom.css | 117 +++++++++++++++++++++ docs/_static/custom.js | 145 ++++++++++++++++---------- docs/_templates/whatsnew_sidebar.html | 15 +++ 3 files changed, 223 insertions(+), 54 deletions(-) create mode 100644 docs/_templates/whatsnew_sidebar.html diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 835939865..800569a5c 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -136,6 +136,22 @@ color: #606060; } +.right-toc-subtoggle { + background: none; + border: none; + color: #2980b9; + cursor: pointer; + font-size: 0.9em; + margin-right: 0.3em; + padding: 0; +} + +.right-toc-sublist { + list-style-type: none; + margin: 0.2em 0 0.4em 0; + padding-left: 1.2em; +} + /* Active TOC item highlighting */ .right-toc-link.active { background-color: rgba(41, 128, 185, 0.15); @@ -245,6 +261,91 @@ display: none; } +body.gallery-filter-active .sphx-glr-thumbnails:not(.gallery-unified) { + display: none; +} + +body.gallery-filter-active .gallery-section-header, +body.gallery-filter-active .gallery-section-description { + display: none; +} + +/* Hide gallery subsections from left TOC - more specific selector */ +body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 a[href="#layouts"], +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#legends-and-colorbars"], +body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 a[href="#geoaxes"], +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#plot-types"], +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#colors-and-cycles"] { + display: none !important; +} + +/* Also hide the nested lists under these sections - more specific selector */ +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#layouts"] + + ul, +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#legends-and-colorbars"] + + ul, +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#geoaxes"] + + ul, +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#plot-types"] + + ul, +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + a[href="#colors-and-cycles"] + + ul { + display: none !important; +} + +/* Hide the parent list items as well */ +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + li[class*="toctree-l1"]:has(a[href="#layouts"]), +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + li[class*="toctree-l1"]:has(a[href="#legends-and-colorbars"]), +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + li[class*="toctree-l1"]:has(a[href="#geoaxes"]), +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + li[class*="toctree-l1"]:has(a[href="#plot-types"]), +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + li[class*="toctree-l1"]:has(a[href="#colors-and-cycles"]) { + display: none !important; +} + +/* Hide the section containers themselves */ +.gallery-section { + margin: 1.5em 0; +} + section#layouts > h1, section#layouts > p, section#legends-and-colorbars > h1, @@ -258,6 +359,22 @@ section#colors-and-cycles > p { display: none; } +/* Style for gallery section headers */ +.gallery-section-header { + font-size: 1.5em; + font-weight: bold; + display: block; + margin: 1.5em 0 0.5em 0; + border-bottom: 2px solid #2980b9; + padding-bottom: 0.3em; + color: #2980b9; +} + +.gallery-section-description { + margin: 0 0 1em 0; + color: #555; +} + /* Responsive adjustments */ @media screen and (max-width: 1200px) { .right-toc { diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 16a140e82..089d43a2a 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -7,10 +7,15 @@ document.addEventListener("DOMContentLoaded", function () { const content = document.querySelector(".rst-content"); if (!content) return; + const isWhatsNew = document.body.classList.contains("whats_new"); + const headerSelector = isWhatsNew + ? "h2, h3" + : "h1:not(.document-title), h2, h3"; + // Find all headers in the main content - const headers = Array.from( - content.querySelectorAll("h1:not(.document-title), h2, h3"), - ).filter((header) => !header.classList.contains("no-toc")); + const headers = Array.from(content.querySelectorAll(headerSelector)).filter( + (header) => !header.classList.contains("no-toc"), + ); // Only create TOC if there are headers if (headers.length === 0) return; @@ -62,8 +67,8 @@ document.addEventListener("DOMContentLoaded", function () { // Generate unique IDs for headers that need them headers.forEach((header, index) => { - // If header already has a unique ID, use that - if (header.id && !usedIds.has(header.id)) { + // If header already has an ID, keep it + if (header.id) { usedIds.add(header.id); return; } @@ -102,25 +107,49 @@ document.addEventListener("DOMContentLoaded", function () { usedIds.add(uniqueId); }); - // Add entries for each header - headers.forEach((header) => { - const item = document.createElement("li"); - const link = document.createElement("a"); + if (isWhatsNew) { + headers.forEach((header) => { + const tag = header.tagName.toLowerCase(); + const rawText = header.textContent || ""; + const cleanText = rawText + .replace(/\s*\uf0c1\s*$/, "") + .replace(/\s*[ΒΆΒ§#†‑]\s*$/, "") + .trim(); + const isReleaseHeading = tag === "h2" && /^v\d/i.test(cleanText || ""); + + if (isReleaseHeading) { + const item = document.createElement("li"); + const link = document.createElement("a"); + + link.href = "#" + header.id; + + link.textContent = cleanText; + link.className = "right-toc-link right-toc-level-h2"; + item.appendChild(link); + tocList.appendChild(item); + } + }); + } else { + // Add entries for each header + headers.forEach((header) => { + const item = document.createElement("li"); + const link = document.createElement("a"); - link.href = "#" + header.id; + link.href = "#" + header.id; - // Get clean text without icons - let headerText = header.textContent || ""; - headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); - headerText = headerText.replace(/\s*[ΒΆΒ§#†‑]\s*$/, ""); + // Get clean text without icons + let headerText = header.textContent || ""; + headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); + headerText = headerText.replace(/\s*[ΒΆΒ§#†‑]\s*$/, ""); - link.textContent = headerText.trim(); - link.className = - "right-toc-link right-toc-level-" + header.tagName.toLowerCase(); + link.textContent = headerText.trim(); + link.className = + "right-toc-link right-toc-level-" + header.tagName.toLowerCase(); - item.appendChild(link); - tocList.appendChild(item); - }); + item.appendChild(link); + tocList.appendChild(item); + }); + } // Add TOC to page document.body.appendChild(toc); @@ -157,7 +186,7 @@ document.addEventListener("DOMContentLoaded", function () { }); document.addEventListener("DOMContentLoaded", function () { - const galleryRoot = document.querySelector("#ultraplot-gallery"); + const galleryRoot = document.querySelector(".sphx-glr-thumbcontainer"); if (galleryRoot) { const gallerySections = [ "layouts", @@ -168,7 +197,7 @@ document.addEventListener("DOMContentLoaded", function () { ]; gallerySections.forEach((sectionId) => { const heading = document.querySelector( - `section#${sectionId} > h1, section#${sectionId} > h2`, + `section#${sectionId} .gallery-section-header`, ); if (heading) { heading.classList.add("no-toc"); @@ -183,51 +212,54 @@ document.addEventListener("DOMContentLoaded", function () { return; } - const topicMap = { - layouts: { label: "Layouts", slug: "layouts" }, - "legends and colorbars": { + const topicList = [ + { id: "layouts", label: "Layouts", slug: "layouts" }, + { + id: "legends_colorbars", label: "Legends & Colorbars", slug: "legends-colorbars", }, - geoaxes: { label: "GeoAxes", slug: "geoaxes" }, - "plot types": { label: "Plot Types", slug: "plot-types" }, - "colors and cycles": { label: "Colors", slug: "colors" }, - }; - - const topics = []; - const topicOrder = new Set(); - const originalSections = new Set(); + { id: "geo", label: "GeoAxes", slug: "geoaxes" }, + { id: "plot_types", label: "Plot Types", slug: "plot-types" }, + { id: "colors", label: "Colors", slug: "colors" }, + ]; + const topicMap = Object.fromEntries( + topicList.map((topic) => [topic.id, topic]), + ); + const originalThumbnails = new Set(); - function normalize(text) { - return text - .toLowerCase() - .replace(/[^\w\s-]/g, "") - .replace(/\s+/g, " ") - .trim(); + function getTopicInfo(thumb) { + const link = thumb.querySelector("a.reference.internal"); + if (!link) { + return { label: "Other", slug: "other" }; + } + const href = link.getAttribute("href") || ""; + const path = new URL(href, window.location.href).pathname; + const match = path.match(/\/gallery\/([^/]+)\//); + const key = match ? match[1] : ""; + return topicMap[key] || { label: "Other", slug: "other" }; } thumbContainers.forEach((thumb) => { - const section = thumb.closest("section"); - const heading = section ? section.querySelector("h1, h2") : null; - const key = heading ? normalize(heading.textContent || "") : ""; - const info = topicMap[key] || { label: "Other", slug: "other" }; + const info = getTopicInfo(thumb); thumb.dataset.topic = info.slug; - if (section) { - originalSections.add(section); - } - if (!topicOrder.has(info.slug) && info.slug !== "other") { - topicOrder.add(info.slug); - topics.push(info); + const group = thumb.closest(".sphx-glr-thumbnails"); + if (group) { + originalThumbnails.add(group); } }); + const topics = topicList.filter((topic) => + thumbContainers.some((thumb) => thumb.dataset.topic === topic.slug), + ); + if (topics.length === 0) { return; } - const firstSection = thumbContainers[0].closest("section"); + const firstGroup = thumbContainers[0].closest(".sphx-glr-thumbnails"); const parent = - (firstSection && firstSection.parentNode) || + (firstGroup && firstGroup.parentNode) || document.querySelector(".rst-content"); if (!parent) { return; @@ -273,11 +305,16 @@ document.addEventListener("DOMContentLoaded", function () { controls.appendChild(filterBar); controls.appendChild(unified); - parent.insertBefore(controls, firstSection); + parent.insertBefore(controls, firstGroup); - originalSections.forEach((section) => { - section.classList.add("gallery-section-hidden"); + originalThumbnails.forEach((group) => { + group.classList.add("gallery-section-hidden"); }); + document + .querySelectorAll(".gallery-section-header, .gallery-section-description") + .forEach((node) => { + node.classList.add("gallery-section-hidden"); + }); document.body.classList.add("gallery-filter-active"); function setFilter(slug) { diff --git a/docs/_templates/whatsnew_sidebar.html b/docs/_templates/whatsnew_sidebar.html new file mode 100644 index 000000000..f9b751e4b --- /dev/null +++ b/docs/_templates/whatsnew_sidebar.html @@ -0,0 +1,15 @@ + + From c4c04a72453849451879682e91189c5e8fb4acb0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 02:35:42 +1000 Subject: [PATCH 07/29] Simplify whats new side toc --- docs/_static/custom.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 089d43a2a..9a86471df 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -8,9 +8,7 @@ document.addEventListener("DOMContentLoaded", function () { if (!content) return; const isWhatsNew = document.body.classList.contains("whats_new"); - const headerSelector = isWhatsNew - ? "h2, h3" - : "h1:not(.document-title), h2, h3"; + const headerSelector = isWhatsNew ? "h2" : "h1:not(.document-title), h2, h3"; // Find all headers in the main content const headers = Array.from(content.querySelectorAll(headerSelector)).filter( @@ -124,7 +122,7 @@ document.addEventListener("DOMContentLoaded", function () { link.href = "#" + header.id; link.textContent = cleanText; - link.className = "right-toc-link right-toc-level-h2"; + link.className = "right-toc-link right-toc-level-h1"; item.appendChild(link); tocList.appendChild(item); } From c37bb96023813739b5b254fd9b3d68fed7bd2d05 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 07:50:01 +1000 Subject: [PATCH 08/29] Fix whats new sidebar toc --- docs/_static/custom.css | 18 ++++++++++++++++++ docs/_static/custom.js | 24 +++++++++++++++++++++++- docs/_templates/whatsnew_sidebar.html | 8 +++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 800569a5c..785cf45a8 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -270,6 +270,24 @@ body.gallery-filter-active .gallery-section-description { display: none; } +body.whats_new .wy-menu-vertical li.toctree-l1.current > ul { + display: none; +} + +body.whats_new .wy-menu-vertical li.toctree-l2, +body.whats_new .wy-menu-vertical li.toctree-l3, +body.whats_new .wy-menu-vertical li.toctree-l4 { + display: none; +} + +body.whats_new .wy-menu-vertical a[href^="#"] { + display: none; +} + +body.whats_new .wy-menu-vertical li:has(> a[href^="#"]) { + display: none; +} + /* Hide gallery subsections from left TOC - more specific selector */ body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 a[href="#layouts"], body.wy-body-for-nav diff --git a/docs/_static/custom.js b/docs/_static/custom.js index 9a86471df..cc724f29c 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -4,10 +4,32 @@ document.addEventListener("DOMContentLoaded", function () { return; } + const isWhatsNewPage = + document.body.classList.contains("whats_new") || + window.location.pathname.endsWith("/whats_new.html") || + window.location.pathname.endsWith("/whats_new/"); + + if (isWhatsNewPage) { + const nav = document.querySelector(".wy-menu-vertical"); + if (nav) { + nav.querySelectorAll('li[class*="toctree-l"]').forEach((item) => { + if (!item.className.match(/toctree-l1/)) { + item.remove(); + } + }); + nav.querySelectorAll('a[href*="#"]').forEach((link) => { + const li = link.closest("li"); + if (li && !li.className.match(/toctree-l1/)) { + li.remove(); + } + }); + } + } + const content = document.querySelector(".rst-content"); if (!content) return; - const isWhatsNew = document.body.classList.contains("whats_new"); + const isWhatsNew = isWhatsNewPage; const headerSelector = isWhatsNew ? "h2" : "h1:not(.document-title), h2, h3"; // Find all headers in the main content diff --git a/docs/_templates/whatsnew_sidebar.html b/docs/_templates/whatsnew_sidebar.html index f9b751e4b..693938a1a 100644 --- a/docs/_templates/whatsnew_sidebar.html +++ b/docs/_templates/whatsnew_sidebar.html @@ -11,5 +11,11 @@ From f661dd62779d3a74063edeed9526ec4a621cdbb0 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 07:50:12 +1000 Subject: [PATCH 09/29] Downgrade whats new headings in release import --- docs/_scripts/fetch_releases.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/_scripts/fetch_releases.py b/docs/_scripts/fetch_releases.py index f07eb678e..681d0b3da 100644 --- a/docs/_scripts/fetch_releases.py +++ b/docs/_scripts/fetch_releases.py @@ -20,6 +20,8 @@ def format_release_body(text): # Convert Markdown to RST using m2r2 formatted_text = convert(text) + formatted_text = _downgrade_headings(formatted_text) + # Convert PR references (remove "by @user in ..." but keep the link) formatted_text = re.sub( r" by @\w+ in (https://github.com/[^\s]+)", r" (\1)", formatted_text @@ -28,6 +30,35 @@ def format_release_body(text): return formatted_text.strip() +def _downgrade_headings(text): + """ + Downgrade all heading levels by one to avoid H1/H2 collisions in the TOC. + """ + adornment_map = { + "=": "-", + "-": "~", + "~": "^", + "^": '"', + '"': "'", + "'": "`", + } + lines = text.splitlines() + for idx in range(len(lines) - 1): + title = lines[idx] + underline = lines[idx + 1] + if not title.strip(): + continue + if not underline: + continue + char = underline[0] + if char not in adornment_map: + continue + if underline.strip(char): + continue + lines[idx + 1] = adornment_map[char] * len(underline) + return "\n".join(lines) + + def fetch_all_releases(): """Fetches all GitHub releases across multiple pages.""" releases = [] From 9247b9d92a44b511ff337c3bfb6e779df61a225e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 07:50:25 +1000 Subject: [PATCH 10/29] Improve curved quiver example styling --- docs/examples/plot_types/01_curved_quiver.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/examples/plot_types/01_curved_quiver.py b/docs/examples/plot_types/01_curved_quiver.py index cbff06fde..59b293361 100644 --- a/docs/examples/plot_types/01_curved_quiver.py +++ b/docs/examples/plot_types/01_curved_quiver.py @@ -46,17 +46,25 @@ arrowsize=1.4, density=20, grains=20, - cmap="sciviscoloreven", + cmap="viko", ) +m.lines.set_clim(0.0, 1.0) values = m.lines.get_array() if values is not None and len(values) > 0: - colors = m.lines.get_cmap()(m.lines.norm(values)) - alpha = (values - values.min()) / (values.max() - values.min() + 1e-12) - colors[:, -1] = 0.15 + 0.85 * alpha + normed = np.clip(m.lines.norm(values), 0.05, 0.95) + colors = m.lines.get_cmap()(normed) + colors[:, -1] = 0.15 + 0.85 * normed m.lines.set_color(colors) m.arrows.set_alpha(0.6) theta = np.linspace(0, 2 * np.pi, 200) -ax.plot(a * np.cos(theta), a * np.sin(theta), color="black", lw=2) +facecolor = ax.get_facecolor() +ax.fill( + a * np.cos(theta), + a * np.sin(theta), + color=facecolor, + zorder=5, +) +ax.plot(a * np.cos(theta), a * np.sin(theta), color="black", lw=2, zorder=6) ax.format( title="Flow around a cylinder", xlabel="x", @@ -66,3 +74,4 @@ fig.colorbar(m.lines, ax=ax, label="Speed") fig.show() +uplt.show(block=1) From 6edf141f5afadcb20d038ad67a24468c63dae173 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 07:55:55 +1000 Subject: [PATCH 11/29] Fix typo in environment.yml --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 299d6b06e..904673230 100644 --- a/environment.yml +++ b/environment.yml @@ -32,6 +32,6 @@ dependencies: - pyarrow - cftime - m2r2 - - lxml_html_clean + - lxml-html-clean - pip: - git+https://github.com/ultraplot/UltraTheme.git From eaa88b30ff46a4e3fab6b34a8066041fddeef41c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 14:46:37 +1000 Subject: [PATCH 12/29] Run image compare single thread --- .github/workflows/build-ultraplot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-ultraplot.yml b/.github/workflows/build-ultraplot.yml index 577ca9e2c..21bade8b2 100644 --- a/.github/workflows/build-ultraplot.yml +++ b/.github/workflows/build-ultraplot.yml @@ -113,7 +113,7 @@ jobs: mkdir -p results python -c "import ultraplot as plt; plt.config.Configurator()._save_yaml('ultraplot.yml')" - pytest -x -W ignore -n auto\ + pytest -x -W ignore \ --mpl \ --mpl-baseline-path=./ultraplot/tests/baseline \ --mpl-results-path=./results/ \ From c6a3a52d6187bfdcaffbf02fa9887e84efef1580 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Thu, 15 Jan 2026 14:49:31 +1000 Subject: [PATCH 13/29] Fix legend span inference with panels (#469) * Fix legend span inference with panels Legend span inference used panel-inflated indices after prior legends added panel rows/cols, yielding invalid gridspec indices for list refs. Decode subplot indices to non-panel grid before computing span and add regression tests for multi-legend ordering. * Restore tests * Document legend span decode fallback Add a brief note that decoding panel indices can fail for panel or nested subplot specs, so we fall back to raw indices. * Add legend span/selection regression tests Cover best-axis selection for left/right/top/bottom and the decode-index fallback path to raise coverage around Figure.legend panel inference. * Extend legend coverage for edge ref handling Add tests that cover span inference with invalid ref entries, best-axis fallback on inset locations, and the empty-iterable ref fallback path. --- ultraplot/figure.py | 30 ++++++++ ultraplot/tests/test_legend.py | 133 ++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 1 deletion(-) diff --git a/ultraplot/figure.py b/ultraplot/figure.py index d7f33f8e3..e78870889 100644 --- a/ultraplot/figure.py +++ b/ultraplot/figure.py @@ -2644,6 +2644,14 @@ def colorbar( continue ss = ss.get_topmost_subplotspec() r1, r2, c1, c2 = ss._get_rows_columns() + gs = ss.get_gridspec() + if gs is not None: + try: + r1, r2 = gs._decode_indices(r1, r2, which="h") + c1, c2 = gs._decode_indices(c1, c2, which="w") + except ValueError: + # Non-panel decode can fail for panel or nested specs. + pass r_min = min(r_min, r1) r_max = max(r_max, r2) c_min = min(c_min, c1) @@ -2685,6 +2693,14 @@ def colorbar( continue ss = ss.get_topmost_subplotspec() r1, r2, c1, c2 = ss._get_rows_columns() + gs = ss.get_gridspec() + if gs is not None: + try: + r1, r2 = gs._decode_indices(r1, r2, which="h") + c1, c2 = gs._decode_indices(c1, c2, which="w") + except ValueError: + # Non-panel decode can fail for panel or nested specs. + pass if side == "right": val = c2 # Maximize column index @@ -2840,6 +2856,13 @@ def legend( continue ss = ss.get_topmost_subplotspec() r1, r2, c1, c2 = ss._get_rows_columns() + gs = ss.get_gridspec() + if gs is not None: + try: + r1, r2 = gs._decode_indices(r1, r2, which="h") + c1, c2 = gs._decode_indices(c1, c2, which="w") + except ValueError: + pass r_min = min(r_min, r1) r_max = max(r_max, r2) c_min = min(c_min, c1) @@ -2881,6 +2904,13 @@ def legend( continue ss = ss.get_topmost_subplotspec() r1, r2, c1, c2 = ss._get_rows_columns() + gs = ss.get_gridspec() + if gs is not None: + try: + r1, r2 = gs._decode_indices(r1, r2, which="h") + c1, c2 = gs._decode_indices(c1, c2, which="w") + except ValueError: + pass if side == "right": val = c2 # Maximize column index diff --git a/ultraplot/tests/test_legend.py b/ultraplot/tests/test_legend.py index a37f2ff0a..f9157ddd0 100644 --- a/ultraplot/tests/test_legend.py +++ b/ultraplot/tests/test_legend.py @@ -7,6 +7,7 @@ import pytest import ultraplot as uplt +from ultraplot.axes import Axes as UAxes @pytest.mark.mpl_image_compare @@ -613,7 +614,137 @@ def test_ref_with_manual_axes_no_subplotspec(): ax1 = fig.add_axes([0.1, 0.1, 0.4, 0.4]) ax2 = fig.add_axes([0.5, 0.1, 0.4, 0.4]) ax1.plot([0, 1], [0, 1], label="line") - # ref=[ax1, ax2]. loc='upper right' (inset). leg = fig.legend(ref=[ax1, ax2], loc="upper right") assert leg is not None + + +def _decode_panel_span(panel_ax, axis): + ss = panel_ax.get_subplotspec().get_topmost_subplotspec() + r1, r2, c1, c2 = ss._get_rows_columns() + gs = ss.get_gridspec() + if axis == "rows": + r1, r2 = gs._decode_indices(r1, r2, which="h") + return int(r1), int(r2) + if axis == "cols": + c1, c2 = gs._decode_indices(c1, c2, which="w") + return int(c1), int(c2) + raise ValueError(f"Unknown axis {axis!r}.") + + +def _anchor_axis(ref): + if np.iterable(ref) and not isinstance(ref, (str, UAxes)): + return next(iter(ref)) + return ref + + +@pytest.mark.parametrize( + "first_loc, first_ref, second_loc, second_ref, span_axis", + [ + ("b", lambda axs: axs[0], "r", lambda axs: axs[:, 1], "rows"), + ("r", lambda axs: axs[:, 2], "b", lambda axs: axs[1, :], "cols"), + ("t", lambda axs: axs[2], "l", lambda axs: axs[:, 0], "rows"), + ("l", lambda axs: axs[:, 0], "t", lambda axs: axs[1, :], "cols"), + ], +) +def test_legend_span_inference_with_multi_panels( + first_loc, first_ref, second_loc, second_ref, span_axis +): + fig, axs = uplt.subplots(nrows=3, ncols=3) + axs.plot([0, 1], [0, 1], label="line") + + fig.legend(ref=first_ref(axs), loc=first_loc) + fig.legend(ref=second_ref(axs), loc=second_loc) + + side_map = {"l": "left", "r": "right", "t": "top", "b": "bottom"} + anchor = _anchor_axis(second_ref(axs)) + panel_ax = anchor._panel_dict[side_map[second_loc]][-1] + span = _decode_panel_span(panel_ax, span_axis) + assert span == (0, 2) + + +def test_legend_best_axis_selection_right_left(): + fig, axs = uplt.subplots(nrows=1, ncols=3) + axs.plot([0, 1], [0, 1], label="line") + ref = [axs[0, 0], axs[0, 2]] + + fig.legend(ref=ref, loc="r", rows=1) + assert len(axs[0, 2]._panel_dict["right"]) == 1 + assert len(axs[0, 0]._panel_dict["right"]) == 0 + + fig.legend(ref=ref, loc="l", rows=1) + assert len(axs[0, 0]._panel_dict["left"]) == 1 + assert len(axs[0, 2]._panel_dict["left"]) == 0 + + +def test_legend_best_axis_selection_top_bottom(): + fig, axs = uplt.subplots(nrows=2, ncols=1) + axs.plot([0, 1], [0, 1], label="line") + ref = [axs[0, 0], axs[1, 0]] + + fig.legend(ref=ref, loc="t", cols=1) + assert len(axs[0, 0]._panel_dict["top"]) == 1 + assert len(axs[1, 0]._panel_dict["top"]) == 0 + + fig.legend(ref=ref, loc="b", cols=1) + assert len(axs[1, 0]._panel_dict["bottom"]) == 1 + assert len(axs[0, 0]._panel_dict["bottom"]) == 0 + + +def test_legend_span_decode_fallback(monkeypatch): + fig, axs = uplt.subplots(nrows=2, ncols=2) + axs.plot([0, 1], [0, 1], label="line") + ref = axs[:, 0] + + gs = axs[0, 0].get_subplotspec().get_topmost_subplotspec().get_gridspec() + + def _raise_decode(*args, **kwargs): + raise ValueError("forced") + + monkeypatch.setattr(gs, "_decode_indices", _raise_decode) + leg = fig.legend(ref=ref, loc="r") + assert leg is not None + + +def test_legend_span_inference_skips_invalid_ref_axes(): + class DummyNoSpec: + pass + + class DummyNullSpec: + def get_subplotspec(self): + return None + + fig, axs = uplt.subplots(nrows=1, ncols=2) + axs[0].plot([0, 1], [0, 1], label="line") + ref = [DummyNoSpec(), DummyNullSpec(), axs[0]] + + leg = fig.legend(ax=axs[0], ref=ref, loc="r") + assert leg is not None + assert len(axs[0]._panel_dict["right"]) == 1 + + +def test_legend_best_axis_fallback_with_inset_loc(): + fig, axs = uplt.subplots(nrows=1, ncols=2) + axs.plot([0, 1], [0, 1], label="line") + + leg = fig.legend(ref=axs, loc="upper left", rows=1) + assert leg is not None + + +def test_legend_best_axis_fallback_empty_iterable_ref(): + class LegendProxy: + def __init__(self, ax): + self._ax = ax + + def __iter__(self): + return iter(()) + + def legend(self, *args, **kwargs): + return self._ax.legend(*args, **kwargs) + + fig, ax = uplt.subplots() + ax.plot([0, 1], [0, 1], label="line") + proxy = LegendProxy(ax) + + leg = fig.legend(ref=proxy, loc="upper left", rows=1) + assert leg is not None From 92c37eb12fb43c9d031269b45018a02f0bb5fb86 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 14 Jan 2026 07:06:44 +1000 Subject: [PATCH 14/29] Add gallery infrastructure and examples Includes: - Configuration updates in conf.py for sphinx-gallery - New gallery examples in docs/examples/ - Updates to documentation styling (custom.css/js) and index.rst --- docs/_static/custom.css | 6 ++++++ docs/examples/layouts/01_shared_axes_abc.py | 4 ++++ docs/examples/layouts/03_spanning_labels.py | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 785cf45a8..6c19f3197 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -261,6 +261,7 @@ display: none; } +<<<<<<< HEAD body.gallery-filter-active .sphx-glr-thumbnails:not(.gallery-unified) { display: none; } @@ -364,6 +365,8 @@ body.wy-body-for-nav margin: 1.5em 0; } +======= +>>>>>>> 0f95f74c (Add gallery infrastructure and examples) section#layouts > h1, section#layouts > p, section#legends-and-colorbars > h1, @@ -377,6 +380,7 @@ section#colors-and-cycles > p { display: none; } +<<<<<<< HEAD /* Style for gallery section headers */ .gallery-section-header { font-size: 1.5em; @@ -393,6 +397,8 @@ section#colors-and-cycles > p { color: #555; } +======= +>>>>>>> 0f95f74c (Add gallery infrastructure and examples) /* Responsive adjustments */ @media screen and (max-width: 1200px) { .right-toc { diff --git a/docs/examples/layouts/01_shared_axes_abc.py b/docs/examples/layouts/01_shared_axes_abc.py index b474a2b1d..718852093 100644 --- a/docs/examples/layouts/01_shared_axes_abc.py +++ b/docs/examples/layouts/01_shared_axes_abc.py @@ -10,7 +10,11 @@ configuration, and adds panel letters automatically. This keeps complex layouts consistent without the manual axis management required in base Matplotlib. +<<<<<<< HEAD Key functions: :py:func:`ultraplot.ui.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. +======= +Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. +>>>>>>> 0f95f74c (Add gallery infrastructure and examples) See also -------- diff --git a/docs/examples/layouts/03_spanning_labels.py b/docs/examples/layouts/03_spanning_labels.py index 496191b60..28269278a 100644 --- a/docs/examples/layouts/03_spanning_labels.py +++ b/docs/examples/layouts/03_spanning_labels.py @@ -23,8 +23,12 @@ rng = np.random.default_rng(21) x = np.linspace(0, 5, 300) +<<<<<<< HEAD layout = [[1, 2, 5], [3, 4, 5]] fig, axs = uplt.subplots(layout) +======= +fig, axs = uplt.subplots(ncols=3, share="labels", span=True, refwidth=2.1) +>>>>>>> 0f95f74c (Add gallery infrastructure and examples) for i, ax in enumerate(axs): trend = (i + 1) * 0.2 y = np.exp(-0.4 * x) * np.sin(2 * x + i * 0.6) + trend @@ -33,6 +37,7 @@ ax.fill_between(x, y - 0.15, y + 0.15, alpha=0.2) ax.set_title(f"Condition {i + 1}") +<<<<<<< HEAD # Share first 2 plots top left axs[:2].format( xlabel="Time (days)", @@ -40,6 +45,10 @@ axs[1, :2].format(xlabel="Time 2 (days)") axs[-1].format(xlabel="Time 3 (days)") axs.format( +======= +axs.format( + xlabel="Time (days)", +>>>>>>> 0f95f74c (Add gallery infrastructure and examples) ylabel="Normalized response", abc=True, abcloc="ul", From 1a33fbeae7e367a54399fde3fb1f9da8771e460a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 12:59:37 +1000 Subject: [PATCH 15/29] Update the examples --- docs/colorbars_legends.py | 4 ++-- docs/examples/plot_types/05_box_violin.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/colorbars_legends.py b/docs/colorbars_legends.py index 8e8002975..51ed495b4 100644 --- a/docs/colorbars_legends.py +++ b/docs/colorbars_legends.py @@ -500,8 +500,8 @@ # Plot data on all axes state = np.random.RandomState(51423) data = (state.rand(20, 4) - 0.5).cumsum(axis=0) -for ax in axs: - ax.plot(data, cycle="mplotcolors", labels=list("abcd")) +axs[0, :].plot(data, cycle="538", labels=list("abcd")) +axs[1, :].plot(data, cycle="accent", labels=list("abcd")) # Legend 1: Content from Row 2 (ax=axs[1, :]), Location below Row 1 (ref=axs[0, :]) # This places a legend describing the bottom row data underneath the top row. diff --git a/docs/examples/plot_types/05_box_violin.py b/docs/examples/plot_types/05_box_violin.py index 5f2adaf87..ba5c2b2a5 100644 --- a/docs/examples/plot_types/05_box_violin.py +++ b/docs/examples/plot_types/05_box_violin.py @@ -22,17 +22,18 @@ import ultraplot as uplt # Generate sample data -data = [np.random.normal(0, std, 100) for std in range(1, 6)] +data = np.array([np.random.normal(0, std, 100) for std in range(1, 6)]) fig, axs = uplt.subplots(ncols=2, refwidth=3) # Box plot -axs[0].boxplot(data, lw=1.5, fillcolor="gray4", medianlw=2) +axs[0].boxplot(data.T, lw=1.5, cycle="qual1", medianlw=2) axs[0].format(title="Box plot", xlabel="Distribution", ylabel="Value") # Violin plot -axs[1].violinplot(data, lw=1, fillcolor="gray6") +axs[1].violinplot(data.T, lw=1, cycle="flatui") axs[1].format(title="Violin plot", xlabel="Distribution", ylabel="Value") axs.format(suptitle="Statistical distributions") +uplt.show(block=1) fig.show() From 95cda8fac24eae8af90c4c939914d25b57f7e1c8 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 14:52:03 +1000 Subject: [PATCH 16/29] Update tests --- docs/index.rst | 1 - ultraplot/tests/test_legend.py | 14 +++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 09613b8b0..5eda69228 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -126,7 +126,6 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen install why usage - gallery/index .. toctree:: :maxdepth: 1 diff --git a/ultraplot/tests/test_legend.py b/ultraplot/tests/test_legend.py index f9157ddd0..48a2f3d91 100644 --- a/ultraplot/tests/test_legend.py +++ b/ultraplot/tests/test_legend.py @@ -1,9 +1,4 @@ -#!/usr/bin/env python3 -""" -Test legends. -""" import numpy as np -import pandas as pd import pytest import ultraplot as uplt @@ -472,13 +467,10 @@ def test_legend_column_without_span(): def test_legend_multiple_sides_with_span(): """Test multiple legends on different sides with span control.""" fig, axs = uplt.subplots(nrows=3, ncols=3) - axs[0, 0].plot([], [], label="test") + axs.plot([0, 1], [0, 1], label="line") - # Create legends on all 4 sides with different spans - leg_bottom = fig.legend(ax=axs[0, 0], span=(1, 2), loc="bottom") - leg_top = fig.legend(ax=axs[1, 0], span=(2, 3), loc="top") - leg_right = fig.legend(ax=axs[0, 0], rows=(1, 2), loc="right") - leg_left = fig.legend(ax=axs[0, 1], rows=(2, 3), loc="left") + fig.legend(ref=first_ref(axs), loc=first_loc) + fig.legend(ref=second_ref(axs), loc=second_loc) assert leg_bottom is not None assert leg_top is not None From bb264baee6f05277de1168e3600ced37dfc3c1f7 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 15:00:57 +1000 Subject: [PATCH 17/29] Fix some tests --- ultraplot/tests/test_legend.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ultraplot/tests/test_legend.py b/ultraplot/tests/test_legend.py index 48a2f3d91..8071485e8 100644 --- a/ultraplot/tests/test_legend.py +++ b/ultraplot/tests/test_legend.py @@ -1,4 +1,5 @@ import numpy as np +import pandas as pd import pytest import ultraplot as uplt @@ -469,8 +470,11 @@ def test_legend_multiple_sides_with_span(): fig, axs = uplt.subplots(nrows=3, ncols=3) axs.plot([0, 1], [0, 1], label="line") - fig.legend(ref=first_ref(axs), loc=first_loc) - fig.legend(ref=second_ref(axs), loc=second_loc) + # Create legends on all 4 sides with different spans + leg_bottom = fig.legend(ref=axs[0, 0], span=(1, 2), loc="bottom") + leg_top = fig.legend(ref=axs[1, 0], span=(2, 3), loc="top") + leg_right = fig.legend(ref=axs[0, 0], rows=(1, 2), loc="right") + leg_left = fig.legend(ref=axs[0, 1], rows=(2, 3), loc="left") assert leg_bottom is not None assert leg_top is not None From 821e4d230569e372b5c5fa988c28113d4547dc1a Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 15:14:45 +1000 Subject: [PATCH 18/29] change alpha ridgeline --- docs/examples/layouts/01_shared_axes_abc.py | 4 ---- docs/examples/layouts/03_spanning_labels.py | 9 --------- 2 files changed, 13 deletions(-) diff --git a/docs/examples/layouts/01_shared_axes_abc.py b/docs/examples/layouts/01_shared_axes_abc.py index 718852093..b474a2b1d 100644 --- a/docs/examples/layouts/01_shared_axes_abc.py +++ b/docs/examples/layouts/01_shared_axes_abc.py @@ -10,11 +10,7 @@ configuration, and adds panel letters automatically. This keeps complex layouts consistent without the manual axis management required in base Matplotlib. -<<<<<<< HEAD Key functions: :py:func:`ultraplot.ui.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. -======= -Key functions: :py:func:`ultraplot.subplots`, :py:meth:`ultraplot.gridspec.SubplotGrid.format`. ->>>>>>> 0f95f74c (Add gallery infrastructure and examples) See also -------- diff --git a/docs/examples/layouts/03_spanning_labels.py b/docs/examples/layouts/03_spanning_labels.py index 28269278a..38db1fca8 100644 --- a/docs/examples/layouts/03_spanning_labels.py +++ b/docs/examples/layouts/03_spanning_labels.py @@ -23,12 +23,8 @@ rng = np.random.default_rng(21) x = np.linspace(0, 5, 300) -<<<<<<< HEAD layout = [[1, 2, 5], [3, 4, 5]] fig, axs = uplt.subplots(layout) -======= -fig, axs = uplt.subplots(ncols=3, share="labels", span=True, refwidth=2.1) ->>>>>>> 0f95f74c (Add gallery infrastructure and examples) for i, ax in enumerate(axs): trend = (i + 1) * 0.2 y = np.exp(-0.4 * x) * np.sin(2 * x + i * 0.6) + trend @@ -36,19 +32,14 @@ ax.plot(x, y, lw=2) ax.fill_between(x, y - 0.15, y + 0.15, alpha=0.2) ax.set_title(f"Condition {i + 1}") - -<<<<<<< HEAD # Share first 2 plots top left axs[:2].format( xlabel="Time (days)", ) axs[1, :2].format(xlabel="Time 2 (days)") axs[-1].format(xlabel="Time 3 (days)") -axs.format( -======= axs.format( xlabel="Time (days)", ->>>>>>> 0f95f74c (Add gallery infrastructure and examples) ylabel="Normalized response", abc=True, abcloc="ul", From 9fd5490190a7a7fdd5dc7773e9230a31633069ba Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:30:33 +1000 Subject: [PATCH 19/29] Fixed layout --- docs/_static/custom.js | 15 +++++++++++++++ docs/index.rst | 1 + ultraplot/axes/plot.py | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/_static/custom.js b/docs/_static/custom.js index cc724f29c..bca643396 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -206,6 +206,21 @@ document.addEventListener("DOMContentLoaded", function () { }); document.addEventListener("DOMContentLoaded", function () { + const navLinks = document.querySelectorAll( + ".wy-menu-vertical a.reference.internal", + ); + navLinks.forEach((link) => { + const href = link.getAttribute("href") || ""; + const isGalleryLink = href.includes("gallery/"); + const isGalleryIndex = href.includes("gallery/index"); + if (isGalleryLink && !isGalleryIndex) { + const item = link.closest("li"); + if (item) { + item.remove(); + } + } + }); + const galleryRoot = document.querySelector(".sphx-glr-thumbcontainer"); if (galleryRoot) { const gallerySections = [ diff --git a/docs/index.rst b/docs/index.rst index 5eda69228..09613b8b0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -126,6 +126,7 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen install why usage + gallery/index .. toctree:: :maxdepth: 1 diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index a5deb571a..b24fd98c9 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -5374,7 +5374,7 @@ def _apply_ridgeline( hist=False, bins="auto", fill=True, - alpha=0.7, + alpha=1.0, linewidth=1.5, edgecolor="black", facecolor=None, @@ -5416,7 +5416,7 @@ def _apply_ridgeline( Only used when hist=True. fill : bool, default: True Whether to fill the area under each curve. - alpha : float, default: 0.7 + alpha : float, default: 1.0 Transparency of filled areas. linewidth : float, default: 1.5 Width of the ridge lines. From 03fb44b2d00e1ff55f67a33878c59aa06e2acb7e Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:32:19 +1000 Subject: [PATCH 20/29] Minor tweaks --- docs/examples/layouts/02_complex_layout_insets.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/examples/layouts/02_complex_layout_insets.py b/docs/examples/layouts/02_complex_layout_insets.py index 72bf9e9c1..b3a0bc3bc 100644 --- a/docs/examples/layouts/02_complex_layout_insets.py +++ b/docs/examples/layouts/02_complex_layout_insets.py @@ -23,7 +23,7 @@ import ultraplot as uplt rng = np.random.default_rng(7) -layout = [[1, 1, 2, 2], [0, 3, 3, 0], [4, 4, 5, 5]] +layout = [[1, 1, 2, 2], [3, 3, 3, 4]] fig, axs = uplt.subplots(layout, share=0, refwidth=1.4) # Panel A: time series with inset zoom. @@ -61,3 +61,5 @@ axs.format(abc=True, abcloc="ul", suptitle="Complex layout with insets and mixed plots") fig.show() + +uplt.show(block=1) From 9c3f526d15c7b52e8d0edcb235d92e8fc3ce3e52 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:32:46 +1000 Subject: [PATCH 21/29] Add ridge plot example --- docs/examples/plot_types/06_ridge_plot.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/examples/plot_types/06_ridge_plot.py diff --git a/docs/examples/plot_types/06_ridge_plot.py b/docs/examples/plot_types/06_ridge_plot.py new file mode 100644 index 000000000..e8704dd37 --- /dev/null +++ b/docs/examples/plot_types/06_ridge_plot.py @@ -0,0 +1,24 @@ +""" +Ridge Plot +========== + +""" + +import numpy as np + +import ultraplot as uplt + +# Generate sample data +np.random.seed(19680801) +n_datasets = 10 +n_points = 50 +data = [np.random.randn(n_points) + i for i in range(n_datasets)] +labels = [f"Dataset {i+1}" for i in range(n_datasets)] + +# Create a figure and axes +fig, ax = uplt.subplots(figsize=(8, 6)) + +# Create the ridgeline plot +ax.ridgeline(data, labels=labels, overlap=0.1, cmap="managua") +ax.format(title="Example Ridge Plot", xlabel="Value", ylabel="Dataset") +fig.show() From c02cdb4338bcc4c7410945ad46a42dbaa76921f5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:34:35 +1000 Subject: [PATCH 22/29] Remove debug --- docs/examples/layouts/02_complex_layout_insets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/examples/layouts/02_complex_layout_insets.py b/docs/examples/layouts/02_complex_layout_insets.py index b3a0bc3bc..5f387a27b 100644 --- a/docs/examples/layouts/02_complex_layout_insets.py +++ b/docs/examples/layouts/02_complex_layout_insets.py @@ -61,5 +61,3 @@ axs.format(abc=True, abcloc="ul", suptitle="Complex layout with insets and mixed plots") fig.show() - -uplt.show(block=1) From 799afcbde96e17a58afbb5ede5447fda89ea4b72 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:35:26 +1000 Subject: [PATCH 23/29] Remove double gallery from toc --- docs/index.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 09613b8b0..6b0a7cce6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -149,13 +149,6 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen fonts configuration -.. toctree:: - :maxdepth: 1 - :caption: Gallery - :hidden: - - gallery/index - .. toctree:: :maxdepth: 1 :caption: Reference From ec92ca5e57766bf71919f430b3e8ed6bbe5d71ec Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 16:49:26 +1000 Subject: [PATCH 24/29] Dummy README to trick gallery formation --- docs/examples/colors/README.txt | 4 ---- docs/examples/geo/README.txt | 4 ---- docs/examples/layouts/README.txt | 5 ----- docs/examples/legends_colorbars/README.txt | 4 ---- docs/examples/plot_types/README.txt | 4 ---- 5 files changed, 21 deletions(-) diff --git a/docs/examples/colors/README.txt b/docs/examples/colors/README.txt index 33c598ac3..e69de29bb 100644 --- a/docs/examples/colors/README.txt +++ b/docs/examples/colors/README.txt @@ -1,4 +0,0 @@ -Colors and Cycles -================= - -Colormaps and property cycles tuned for publication figures. diff --git a/docs/examples/geo/README.txt b/docs/examples/geo/README.txt index edbf0a140..e69de29bb 100644 --- a/docs/examples/geo/README.txt +++ b/docs/examples/geo/README.txt @@ -1,4 +0,0 @@ -GeoAxes -======= - -Geographic projections with clean defaults and minimal boilerplate. diff --git a/docs/examples/layouts/README.txt b/docs/examples/layouts/README.txt index 9c488156d..e69de29bb 100644 --- a/docs/examples/layouts/README.txt +++ b/docs/examples/layouts/README.txt @@ -1,5 +0,0 @@ -Layouts -======= - -Multi-panel, publication-style layouts that emphasize axis sharing, labels, -and panel annotations. diff --git a/docs/examples/legends_colorbars/README.txt b/docs/examples/legends_colorbars/README.txt index 0792605e3..e69de29bb 100644 --- a/docs/examples/legends_colorbars/README.txt +++ b/docs/examples/legends_colorbars/README.txt @@ -1,4 +0,0 @@ -Legends and Colorbars -===================== - -Showcase precise placement of legends and colorbars across complex layouts. diff --git a/docs/examples/plot_types/README.txt b/docs/examples/plot_types/README.txt index 469ac0f6b..e69de29bb 100644 --- a/docs/examples/plot_types/README.txt +++ b/docs/examples/plot_types/README.txt @@ -1,4 +0,0 @@ -Plot Types -========== - -Specialized plot types unique to UltraPlot workflows. From 202aee4283a6656228e95021eb8541c5bf226cc5 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 17:35:18 +1000 Subject: [PATCH 25/29] Remove left over code from merge --- docs/_static/custom.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 6c19f3197..785cf45a8 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -261,7 +261,6 @@ display: none; } -<<<<<<< HEAD body.gallery-filter-active .sphx-glr-thumbnails:not(.gallery-unified) { display: none; } @@ -365,8 +364,6 @@ body.wy-body-for-nav margin: 1.5em 0; } -======= ->>>>>>> 0f95f74c (Add gallery infrastructure and examples) section#layouts > h1, section#layouts > p, section#legends-and-colorbars > h1, @@ -380,7 +377,6 @@ section#colors-and-cycles > p { display: none; } -<<<<<<< HEAD /* Style for gallery section headers */ .gallery-section-header { font-size: 1.5em; @@ -397,8 +393,6 @@ section#colors-and-cycles > p { color: #555; } -======= ->>>>>>> 0f95f74c (Add gallery infrastructure and examples) /* Responsive adjustments */ @media screen and (max-width: 1200px) { .right-toc { From fef8a82475a000cd55c57e1d7667e31ac1f7d66b Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 17:39:06 +1000 Subject: [PATCH 26/29] Consolidate css --- docs/_static/custom.css | 115 +++++++++++----------------------------- 1 file changed, 32 insertions(+), 83 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 785cf45a8..145657869 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -38,21 +38,6 @@ height: 100%; } -/* .right-toc { - position: fixed; - top: 90px; - right: 20px; - width: 280px; - font-size: 0.9em; - max-height: calc(100vh - 150px); - background-color: #f8f9fa; - z-index: 100; - border-radius: 6px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - border-left: 3px solid #2980b9; -} */ - .right-toc-header { display: flex; justify-content: space-between; @@ -288,74 +273,40 @@ body.whats_new .wy-menu-vertical li:has(> a[href^="#"]) { display: none; } -/* Hide gallery subsections from left TOC - more specific selector */ -body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 a[href="#layouts"], -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - a[href="#legends-and-colorbars"], -body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 a[href="#geoaxes"], -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - a[href="#plot-types"], -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - a[href="#colors-and-cycles"] { - display: none !important; -} - -/* Also hide the nested lists under these sections - more specific selector */ +/* Hide gallery subsections from left TOC */ body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 - a[href="#layouts"] - + ul, + a:is( + [href="#layouts"], + [href="#legends-and-colorbars"], + [href="#geoaxes"], + [href="#plot-types"], + [href="#colors-and-cycles"] + ), body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 - a[href="#legends-and-colorbars"] + a:is( + [href="#layouts"], + [href="#legends-and-colorbars"], + [href="#geoaxes"], + [href="#plot-types"], + [href="#colors-and-cycles"] + ) + ul, body.wy-body-for-nav .wy-menu-vertical .wy-menu-vertical-2 - a[href="#geoaxes"] - + ul, -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - a[href="#plot-types"] - + ul, -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - a[href="#colors-and-cycles"] - + ul { - display: none !important; -} - -/* Hide the parent list items as well */ -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - li[class*="toctree-l1"]:has(a[href="#layouts"]), -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - li[class*="toctree-l1"]:has(a[href="#legends-and-colorbars"]), -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - li[class*="toctree-l1"]:has(a[href="#geoaxes"]), -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - li[class*="toctree-l1"]:has(a[href="#plot-types"]), -body.wy-body-for-nav - .wy-menu-vertical - .wy-menu-vertical-2 - li[class*="toctree-l1"]:has(a[href="#colors-and-cycles"]) { + li[class*="toctree-l1"]:has( + :is( + a[href="#layouts"], + a[href="#legends-and-colorbars"], + a[href="#geoaxes"], + a[href="#plot-types"], + a[href="#colors-and-cycles"] + ) + ) { display: none !important; } @@ -364,16 +315,14 @@ body.wy-body-for-nav margin: 1.5em 0; } -section#layouts > h1, -section#layouts > p, -section#legends-and-colorbars > h1, -section#legends-and-colorbars > p, -section#geoaxes > h1, -section#geoaxes > p, -section#plot-types > h1, -section#plot-types > p, -section#colors-and-cycles > h1, -section#colors-and-cycles > p { +:is( + section#layouts, + section#legends-and-colorbars, + section#geoaxes, + section#plot-types, + section#colors-and-cycles + ) + > :is(h1, p) { display: none; } From 9405c48fe705834c3e2c498d9b3774033b4e88a7 Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Thu, 15 Jan 2026 18:02:45 +1000 Subject: [PATCH 27/29] Update 03_projections_features.py --- docs/examples/geo/03_projections_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/geo/03_projections_features.py b/docs/examples/geo/03_projections_features.py index 10e7c82b7..4e5ca522e 100644 --- a/docs/examples/geo/03_projections_features.py +++ b/docs/examples/geo/03_projections_features.py @@ -22,7 +22,7 @@ # Projections to compare projs = ["moll", "ortho", "kav7"] -fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3) +fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3, share = 0) # Format all axes with features # land=True, coast=True, etc. are shortcuts for adding cartopy features From dcdc9c1f104f050a253df7c8efdef1974c036ac4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:30:14 +0000 Subject: [PATCH 28/29] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/examples/geo/03_projections_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/geo/03_projections_features.py b/docs/examples/geo/03_projections_features.py index 4e5ca522e..b931a17a0 100644 --- a/docs/examples/geo/03_projections_features.py +++ b/docs/examples/geo/03_projections_features.py @@ -22,7 +22,7 @@ # Projections to compare projs = ["moll", "ortho", "kav7"] -fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3, share = 0) +fig, axs = uplt.subplots(ncols=3, proj=projs, refwidth=3, share=0) # Format all axes with features # land=True, coast=True, etc. are shortcuts for adding cartopy features From 262f57cec0943a52f1bfff4328b19dc5887f9d42 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Thu, 15 Jan 2026 20:39:56 +1000 Subject: [PATCH 29/29] Adjusted curved quiver plot --- docs/examples/plot_types/01_curved_quiver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/plot_types/01_curved_quiver.py b/docs/examples/plot_types/01_curved_quiver.py index 59b293361..b9d6e1a02 100644 --- a/docs/examples/plot_types/01_curved_quiver.py +++ b/docs/examples/plot_types/01_curved_quiver.py @@ -42,8 +42,9 @@ V, color=speed, arrow_at_end=True, - scale=100, - arrowsize=1.4, + scale=30, + arrowsize=0.7, + linewidth=0.4, density=20, grains=20, cmap="viko", @@ -74,4 +75,3 @@ fig.colorbar(m.lines, ax=ax, label="Speed") fig.show() -uplt.show(block=1)