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/_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 = [] diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 3732c6131..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; @@ -136,6 +121,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); @@ -200,6 +201,147 @@ 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; +} + +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; +} + +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 */ +body.wy-body-for-nav + .wy-menu-vertical + .wy-menu-vertical-2 + 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: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 + 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; +} + +/* Hide the section containers themselves */ +.gallery-section { + margin: 1.5em 0; +} + +:is( + section#layouts, + section#legends-and-colorbars, + section#geoaxes, + section#plot-types, + section#colors-and-cycles + ) + > :is(h1, 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 ef54f6847..bca643396 100644 --- a/docs/_static/custom.js +++ b/docs/_static/custom.js @@ -4,13 +4,38 @@ 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 = isWhatsNewPage; + const headerSelector = isWhatsNew ? "h2" : "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; @@ -28,9 +53,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 () { @@ -64,8 +87,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; } @@ -104,26 +127,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 || ""); - link.href = "#" + header.id; + if (isReleaseHeading) { + const item = document.createElement("li"); + const link = document.createElement("a"); - // Get clean text without icons - let headerText = header.textContent || ""; - headerText = headerText.replace(/\s*\uf0c1\s*$/, ""); - headerText = headerText.replace(/\s*[¶§#†‡]\s*$/, ""); + link.href = "#" + header.id; - link.textContent = headerText.trim(); - link.className = - "right-toc-link right-toc-level-" + - header.tagName.toLowerCase(); + link.textContent = cleanText; + link.className = "right-toc-link right-toc-level-h1"; + 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"); - item.appendChild(link); - tocList.appendChild(item); - }); + link.href = "#" + header.id; + + // 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(); + + item.appendChild(link); + tocList.appendChild(item); + }); + } // Add TOC to page document.body.appendChild(toc); @@ -141,9 +187,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 +196,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 +205,172 @@ 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 = [ + "layouts", + "legends-and-colorbars", + "geoaxes", + "plot-types", + "colors-and-cycles", + ]; + gallerySections.forEach((sectionId) => { + const heading = document.querySelector( + `section#${sectionId} .gallery-section-header`, + ); + if (heading) { + heading.classList.add("no-toc"); + } + }); + } + + const thumbContainers = Array.from( + document.querySelectorAll(".sphx-glr-thumbcontainer"), + ); + if (thumbContainers.length < 6) { + return; + } + + const topicList = [ + { id: "layouts", label: "Layouts", slug: "layouts" }, + { + id: "legends_colorbars", + label: "Legends & Colorbars", + slug: "legends-colorbars", + }, + { 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 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 info = getTopicInfo(thumb); + thumb.dataset.topic = info.slug; + 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 firstGroup = thumbContainers[0].closest(".sphx-glr-thumbnails"); + const parent = + (firstGroup && firstGroup.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, firstGroup); + + 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) { + 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/_templates/whatsnew_sidebar.html b/docs/_templates/whatsnew_sidebar.html new file mode 100644 index 000000000..693938a1a --- /dev/null +++ b/docs/_templates/whatsnew_sidebar.html @@ -0,0 +1,21 @@ + + 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/conf.py b/docs/conf.py index db3db8baa..b3b97ac0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -63,6 +63,19 @@ 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 ------------------------------------------------------- # The basic info @@ -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" @@ -314,13 +355,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, } @@ -335,7 +374,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..0766bc4a6 --- /dev/null +++ b/docs/examples/colors/02_diverging_colormap.py @@ -0,0 +1,52 @@ +""" +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", 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, + colorbar="b", + center_levels=True, +) +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..e69de29bb 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..b931a17a0 --- /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 = ["moll", "ortho", "kav7"] + +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 +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..e69de29bb 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..b474a2b1d --- /dev/null +++ b/docs/examples/layouts/01_shared_axes_abc.py @@ -0,0 +1,45 @@ +""" +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.ui.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) + +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 + ax.plot(x, y, lw=2) + ax.scatter(x[::30], y[::30], s=18, alpha=0.65) + +axs.format( + abc="[A.]", + 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..5f387a27b --- /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], [3, 3, 3, 4]] +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..38db1fca8 --- /dev/null +++ b/docs/examples/layouts/03_spanning_labels.py @@ -0,0 +1,51 @@ +""" +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) + +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 + 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}") +# 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( + 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..e69de29bb 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..e69de29bb 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..b9d6e1a02 --- /dev/null +++ b/docs/examples/plot_types/01_curved_quiver.py @@ -0,0 +1,77 @@ +""" +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=30, + arrowsize=0.7, + linewidth=0.4, + density=20, + grains=20, + cmap="viko", +) +m.lines.set_clim(0.0, 1.0) +values = m.lines.get_array() +if values is not None and len(values) > 0: + 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) +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", + 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..ba5c2b2a5 --- /dev/null +++ b/docs/examples/plot_types/05_box_violin.py @@ -0,0 +1,39 @@ +""" +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.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.T, lw=1.5, cycle="qual1", medianlw=2) +axs[0].format(title="Box plot", xlabel="Distribution", ylabel="Value") + +# Violin plot +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() 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() diff --git a/docs/examples/plot_types/README.txt b/docs/examples/plot_types/README.txt new file mode 100644 index 000000000..e69de29bb diff --git a/docs/index.rst b/docs/index.rst index bd55c3882..6b0a7cce6 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 diff --git a/environment.yml b/environment.yml index 74375c513..904673230 100644 --- a/environment.yml +++ b/environment.yml @@ -17,6 +17,7 @@ dependencies: - pip - pint - sphinx + - sphinx-gallery - nbsphinx - jupytext - sphinx-copybutton 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. diff --git a/ultraplot/tests/test_legend.py b/ultraplot/tests/test_legend.py index f9157ddd0..8071485e8 100644 --- a/ultraplot/tests/test_legend.py +++ b/ultraplot/tests/test_legend.py @@ -1,7 +1,3 @@ -#!/usr/bin/env python3 -""" -Test legends. -""" import numpy as np import pandas as pd import pytest @@ -472,13 +468,13 @@ 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") + 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