diff --git a/.gitignore b/.gitignore index fc0755a70..9d39a53cb 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ docs/_static/ultraplotrc docs/_static/rctable.rst docs/_static/* !docs/_static/why_plots/ +!docs/_static/example_plots/ +!docs/_static/example_plots/data_aware_example.svg *.html docs/gallery/ docs/sg_execution_times.rst diff --git a/docs/_static/example_plots/data_aware_example.svg b/docs/_static/example_plots/data_aware_example.svg new file mode 100644 index 000000000..6d495e0f5 --- /dev/null +++ b/docs/_static/example_plots/data_aware_example.svg @@ -0,0 +1,1159 @@ + + + + + + + + image/svg+xml + + + Matplotlib v3.10.9, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/data_aware.py b/docs/data_aware.py new file mode 100644 index 000000000..af0671359 --- /dev/null +++ b/docs/data_aware.py @@ -0,0 +1,63 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_data_aware: +# +# Data-aware plotting +# =================== +# +# UltraPlot recognizes labelled data from pandas and xarray. Pass a pandas +# Series or DataFrame directly to an axes method and UltraPlot can use its +# index, column names, and axis metadata to choose coordinates and labels. +# This keeps the plotting call focused on the data instead of repeating its +# description in several formatting arguments. +# +# The example below starts with a small, labelled DataFrame. The date index +# becomes the horizontal coordinate, and the column names become legend labels. + +# %% +import numpy as np +import pandas as pd +import ultraplot as uplt + +dates = pd.date_range("2025-01-01", periods=24, freq="MS") +season = np.sin(np.linspace(0, 2 * np.pi, dates.size)) +data = pd.DataFrame( + { + "observed": 18 + 3 * season, + "smoothed": 18 + 2.5 * season, + }, + index=dates, +) +data.index.name = "Date" + +fig, ax = uplt.subplots() +ax.plot(data) +ax.format(title="Monthly temperature", ylabel="Temperature (°C)") +ax.legend(loc="ur", ncols=1) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# What UltraPlot inferred +# ------------------------ +# +# The DataFrame index supplies the x coordinates, while its column labels are +# available to the legend. You can still override any inferred value with the +# usual plotting or :meth:`~ultraplot.axes.Axes.format` keyword arguments. +# +# For data arrays, coordinate inference works the same way with xarray. See the +# detailed :ref:`1D integration guide ` and +# :ref:`2D integration guide ` for MultiIndex data, +# DataArrays, and labelled two-dimensional plots. diff --git a/docs/first_figure.py b/docs/first_figure.py new file mode 100644 index 000000000..4aacda4f8 --- /dev/null +++ b/docs/first_figure.py @@ -0,0 +1,338 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _first_figure: +# +# Your First UltraPlot Figure +# =========================== +# +# UltraPlot is designed to make creating complex Matplotlib figures simpler and +# more intuitive. This tutorial builds a two-panel figure in five stages. +# Because each stage is self-contained, you can run them individually while +# seeing exactly how UltraPlot removes standard Matplotlib boilerplate and +# automates layout generation. + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 1: Choose a layout +# ------------------------ +# +# In standard Matplotlib, you generally have to guess a ``figsize`` tuple, +# which requires tedious trial and error anytime you add or remove subplots. +# UltraPlot calculates the figure size dynamically. By setting ``refwidth=2.4``, +# you simply define the width of a single reference panel, and UltraPlot +# automatically scales the entire figure to fit your rows and columns perfectly. + +# %% +import ultraplot as uplt + +fig, axs = uplt.subplots(ncols=2, share=False, refwidth=2.4) +fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 2: Add data +# ----------------- +# +# While UltraPlot revolutionizes the figure layout, the actual plotting +# commands are completely identical to Matplotlib. Because UltraPlot axes +# are directly built upon Matplotlib axes, you can use standard methods like +# :meth:`~ultraplot.axes.PlotAxes.plot` and +# :meth:`~ultraplot.axes.PlotAxes.imshow` with absolutely zero learning curve. + +# %% +import numpy as np +import ultraplot as uplt + +rng = np.random.RandomState(2024) +x = np.linspace(0, 2 * np.pi, 100) +signal = np.sin(x) + 0.06 * rng.randn(x.size) +image = np.outer(np.sin(x / 2), np.cos(x / 3)) + +fig, axs = uplt.subplots(ncols=2, share=False, refwidth=2.4) +axs[0].plot(x, signal, label="signal") +mesh = axs[1].imshow(image, origin="lower", aspect="auto") +fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 3: Format the panels +# -------------------------- +# +# This is where UltraPlot drastically reduces boilerplate code. In Matplotlib, +# formatting these two axes would require six separate lines of code (using +# :meth:`~matplotlib.axes.Axes.set_title`, +# :meth:`~matplotlib.axes.Axes.set_xlabel`, and +# :meth:`~matplotlib.axes.Axes.set_ylabel`). UltraPlot introduces the unified +# :meth:`~ultraplot.axes.Axes.format` method, allowing you to configure titles, labels, +# limits, and styling all in a single, readable function call per axis. + +# %% +import numpy as np +import ultraplot as uplt + +rng = np.random.RandomState(2024) +x = np.linspace(0, 2 * np.pi, 100) +signal = np.sin(x) + 0.06 * rng.randn(x.size) +image = np.outer(np.sin(x / 2), np.cos(x / 3)) + +fig, axs = uplt.subplots(ncols=2, share=False, refwidth=2.4) +axs[0].plot(x, signal, label="signal") +mesh = axs[1].imshow(image, origin="lower", aspect="auto") +axs[1].colorbar(mesh, label="Intensity", loc="r") +axs[0].format(title="Signal", xlabel="angle", ylabel="value") +axs[1].format(title="Image", xlabel="column", ylabel="row") +fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 4: Add guides and figure formatting +# ----------------------------------------- +# +# Adding colorbars and legends in Matplotlib is notorious for ruining layouts—they +# often overlap with data or require tedious :class:`~matplotlib.gridspec.GridSpec` +# wrangling. UltraPlot +# solves this natively. By passing simple location strings like ``loc="t"`` (top) +# or ``loc="r"`` (right) to :meth:`~ultraplot.figure.Figure.legend` and +# :meth:`~ultraplot.axes.Axes.colorbar`, UltraPlot allocates dedicated space +# *outside* the subplots without shrinking or distorting your axes. + +# %% +import numpy as np +import ultraplot as uplt + +rng = np.random.RandomState(2024) +x = np.linspace(0, 2 * np.pi, 100) +signal = np.sin(x) + 0.06 * rng.randn(x.size) +image = np.outer(np.sin(x / 2), np.cos(x / 3)) + +fig, axs = uplt.subplots(ncols=2, share=False, refwidth=2.4) +axs[0].plot(x, signal, label="signal") +mesh = axs[1].imshow(image, origin="lower", aspect="auto", colorbar = "lr", colorbar_kw = dict(label = "Intensity")) +axs[0].format(title="Signal", xlabel="angle", ylabel="value") +axs[1].format(title="Image", xlabel="column", ylabel="row") +# +fig.format(suptitle="A first UltraPlot figure") +fig.legend(loc="b") +fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 5: Save the result +# ------------------------ +# +# Saving the figure is just as straightforward. Unlike standard Matplotlib, where +# you frequently have to pass ``bbox_inches="tight"`` to prevent labels and guides +# from being cut off, UltraPlot's automated layout engine guarantees that your +# saved file will automatically have perfectly tight margins. + +# %% +import numpy as np +import ultraplot as uplt + +rng = np.random.RandomState(2024) +x = np.linspace(0, 2 * np.pi, 100) +signal = np.sin(x) + 0.06 * rng.randn(x.size) +image = np.outer(np.sin(x / 2), np.cos(x / 3)) + +fig, axs = uplt.subplots(ncols=2, share=False, refwidth=2.4) +axs[0].plot(x, signal, label="signal") +mesh = axs[1].imshow(image, origin="lower", aspect="auto", colorbar = "ur", colorbar_kw = dict(label = "Intensity")) +axs[0].format(title="Signal", xlabel="angle", ylabel="value") +axs[1].format(title="Image", xlabel="column", ylabel="row") +fig.format(suptitle="A first UltraPlot figure") +fig.legend(loc="b") +fig.save("first_figure.png") + +# %% [raw] raw_mimetype="text/restructuredtext" +# Stage 6: The Grand Finale +# ------------------------- +# +# To truly see UltraPlot's power, let's create a complex, publication-ready +# figure. In standard Matplotlib, combining a custom mosaic layout, a geographic +# projection, an inset axes, and A-B-C panel labels usually results in hundreds +# of lines of fragile :class:`~matplotlib.gridspec.GridSpec` and transform code. +# +# UltraPlot condenses all of this into a highly readable, declarative script. +# Notice how we assign a projection to just one panel using a dictionary, +# add an inset using a simple location string, and auto-generate our panel +# labels with a single ``abc=True`` argument. + +# %% +import numpy as np +import ultraplot as uplt + +# 1. Generate synthetic scientific data +rng = np.random.RandomState(2024) +lon, lat = np.linspace(-180, 180, 100), np.linspace(-90, 90, 100) +lon2d, lat2d = np.meshgrid(lon, lat) +# A pseudo-spatial anomaly pattern +geo_data = np.cos(np.radians(lat2d)) * np.sin(np.radians(lon2d * 2)) +time = np.linspace(0, 10, 200) +series1 = np.sin(time) * np.exp(-time / 5) +series2 = np.cos(time) * np.exp(-time / 5) + +# The map spans two rows while the right-hand panels stack beside it. With the +# 4:1 column ratio below, the Robinson map keeps its native 2:1 aspect and the +# right-hand panels receive approximately square plotting areas. +layout = [[1, 2], [1, 3]] + +# 3. Create the figure +# Apply a Robinson projection only to the first panel. +fig, axs = uplt.subplots( + layout, + proj={1: 'robin'}, + share=0, + refnum=2, + refwidth=1.5, + wratios=(4, 1), + hspace='13em', +) + +# 4. Geographic data +m = axs[0].contourf( + lon, lat, geo_data, + cmap='marine', + levels=15, +) + +axs[0].format( + land=True, + borders=True, + labels=True, + lonlines=120, + latlines=45, + labelsize=12, + title='Global Spatial Anomaly', + title_kw={'fontsize': 14}, +) + +# A geographic callout is useful for showing a local-scale pattern without +# sacrificing the global view. These deterministic points mimic city readings. +paris = (2.3522, 48.8566) +paris_rng = np.random.RandomState(99) +paris_lon = paris[0] + paris_rng.normal(scale=0.65, size=36) +paris_lat = paris[1] + paris_rng.normal(scale=0.45, size=36) +paris_value = np.hypot(paris_lon - paris[0], paris_lat - paris[1]) +paris_ax = axs[0].hawkeye( + (0.43, 0.68), + size=0.45, + anchor='ur', + proj='merc', + extent=(-0.8, 5.5, 46.8, 50.8), + shape='circle', + target='circle', + connector='line', + color='red7', + indicator_kw={'linewidth': 1.4}, +) +paris_ax.format(land=True, landcolor='gray8', borders=True) +paris_ax.scatter( + paris_lon, + paris_lat, + c=paris_value, + cmap='fire', + markersize=24, + edgecolor='white', + linewidth=0.35, + transform='cyl', + absolute_size = True, +) +paris_ax.plot( + *paris, + marker='*', + markersize=9, + color='red7', + markeredgecolor='white', + markeredgewidth=0.6, + transform='cyl', +) + +axs[0].colorbar( + m, + loc='b', + label='Anomaly magnitude', + length=0.8, + labelsize = 14 +) + +# 5. Scatter data with inset +x = rng.rand(100) +y = x + rng.randn(100) * 0.2 + +axs[1].scatter( + x, y, + c=x, + cmap='fire', + markersize=15, + alpha=0.7, +) + +axs[1].format( + title='Correlation Profile', + xlabel='Predictor', + ylabel='Response', + xlocator=(0, 0.5, 1), + ylocator=(0, 0.5, 1), + xtickminor=False, + ytickminor=False, + ticklabelsize=10, + labelsize=12, + title_kw={'fontsize': 12}, +) + +ax_ins = axs[1].inset([0.55, 0.55, 0.35, 0.35], zoom=False) +ax_ins.hist(x, bins=10, color='gray5', edgecolor='black') +ax_ins.format( + titleloc='uc', + grid=False, + xtickminor=False, + ytickminor=False, +) + +# 6. Time series +axs[2].plot(time, series1, label='Model Alpha') +axs[2].plot(time, series2, label='Model Beta') + +axs[2].format( + title='Temporal Decay', + xlabel='Time (s)', + ylabel='Amplitude', + xlocator=(0, 5, 10), + ylocator=(-0.5, 0, 0.5, 1), + xtickminor=False, + ytickminor=False, + ticklabelsize=10, + labelsize=12, + title_kw={'fontsize': 12}, +) + +axs[2].legend( + loc='ur', + frame=False, + fontsize=6, + ncols = 1, +) + +# 7. Figure-wide formatting +fig.format( + suptitle='Putting It All Together', + suptitle_kw={'fontsize': 15}, + abc=True, + abcloc='ul', + abcstyle='(a)', + abc_kw={'fontsize': 11}, +) + +fig.save('complex_figure.png', dpi=150) diff --git a/docs/index.rst b/docs/index.rst index 5b5ec248d..b99854b6e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,146 +3,170 @@ :align: center **UltraPlot** is a succinct wrapper around `matplotlib `__ -for creating **beautiful, publication-quality graphics** with ease. +for creating **publication-quality graphics** with a small, familiar API. -Key Features -############ -Build polished figures quickly with pragmatic defaults. -**Simplified Subplot Management** – Create multi-panel plots effortlessly. +Start with a finished figure +############################ + +The quickest way to get oriented is to make one complete figure, then explore +the concepts behind it. Follow the :doc:`first figure ` walkthrough or browse +the :doc:`recipes` for focused examples. + +.. admonition:: Coming from Matplotlib? + :class: tip -**Smart Aesthetics** – Optimized colormaps, fonts, and styles out of the box. + Compare the same figure side by side in the + :doc:`Matplotlib comparison ` to see where UltraPlot adds + convenience while keeping Matplotlib objects and conventions in view. -**Versatile Plot Types** – Cartesian plots, insets, colormaps, and more. + :doc:`Make your first figure ` · + :doc:`Compare with Matplotlib ` + +Key features +############ -**Get Started** → :doc:`Installation guide ` | :doc:`Why UltraPlot? ` | :doc:`Usage ` | :doc:`Gallery ` +Build polished figures with pragmatic defaults and familiar Matplotlib +objects. UltraPlot supports multi-panel layouts, Cartesian and geographic +plots, colorbars and legends, and data-aware plotting workflows. --------------------------------------- +**Get started** → :doc:`Installation guide ` | +:doc:`Why UltraPlot? ` | :doc:`Usage ` | +:doc:`Gallery ` -User Guide -########## -A preview of what UltraPlot can do. For more see the sidebar! +Topics +###### .. grid:: 1 2 3 3 :gutter: 2 .. grid-item-card:: - :link: subplots.html - :shadow: md - :class-card: card-with-bottom-text + :link: subplots.html + :shadow: md + :class-card: card-with-bottom-text - **Subplots & Layouts** - ^^^ + **Subplots & Layouts** + ^^^ - .. image:: _static/example_plots/subplot_example.svg - :align: center + .. image:: _static/example_plots/subplot_example.svg + :align: center - Create complex multi-panel layouts effortlessly. + Create multi-panel layouts with shared axes and automatic spacing. .. grid-item-card:: - :link: cartesian.html - :shadow: md - :class-card: card-with-bottom-text - - **Cartesian Plots** - ^^^ + :link: cartesian.html + :shadow: md + :class-card: card-with-bottom-text - .. image:: _static/example_plots/cartesian_example.svg - :align: center + **Cartesian Plots** + ^^^ - .. container:: bottom-aligned-text + .. image:: _static/example_plots/cartesian_example.svg + :align: center - Easily generate clean, well-formatted plots. + Format ordinary plots while retaining Matplotlib's plotting methods. .. grid-item-card:: - :link: projections.html - :shadow: md - :class-card: card-with-bottom-text + :link: colorbars_legends.html + :shadow: md + :class-card: card-with-bottom-text - **Projections & Maps** - ^^^ + **Colorbars & Legends** + ^^^ - .. image:: _static/example_plots/projection_example.svg - :align: center + .. image:: _static/example_plots/colorbars_legends_example.svg + :align: center - .. container:: bottom-aligned-text - Built-in support for projections and geographic plots. + Place and align guides across individual subplots or a whole figure. .. grid-item-card:: - :link: colorbars_legends.html - :shadow: md - :class-card: card-with-bottom-text + :link: data_aware.html + :shadow: md + :class-card: card-with-bottom-text - **Colorbars & Legends** - ^^^ + **Data-aware plotting** + ^^^ - .. image:: _static/example_plots/colorbars_legends_example.svg - :align: center + .. image:: _static/example_plots/data_aware_example.svg + :align: center - Customize legends and colorbars with ease. + Plot labelled pandas and xarray data with metadata-aware labels and + coordinates. .. grid-item-card:: - :link: insets_panels.html - :shadow: md - :class-card: card-with-bottom-text + :link: projections.html + :shadow: md + :class-card: card-with-bottom-text - **Insets & Panels** - ^^^ + **Projections & Maps** + ^^^ - .. image:: _static/example_plots/panels_example.svg - :align: center + .. image:: _static/example_plots/projection_example.svg + :align: center - Add inset plots and panel-based layouts. + Explore geographic plotting when you need projections and map features. .. grid-item-card:: :link: colormaps.html :shadow: md :class-card: card-with-bottom-text - **Colormaps & Cycles** + **Colormaps & Styles** ^^^ .. image:: _static/example_plots/colormaps_example.svg - :align: center + :align: center - Use prebuilt colormaps and define your own color cycles. + Choose and customize colormaps for clear, consistent visual encoding. -Reference & More +Reference & more ################ -For more details, check the full :doc:`User guide ` and :doc:`API Reference `. + +For details, see the full :doc:`User guide ` and +:doc:`API Reference `. * :ref:`genindex` * :ref:`modindex` * :ref:`glossary` + .. toctree:: :maxdepth: 1 - :caption: Getting Started + :caption: Getting started :hidden: install - why + why_ultraplot + first_figure usage + recipes gallery/index .. toctree:: :maxdepth: 1 - :caption: User Guide + :caption: Guides :hidden: basics subplots cartesian + data_aware networks - projections colorbars_legends - insets_panels + colormaps 1dplots 2dplots + +.. toctree:: + :maxdepth: 1 + :caption: Advanced guides + :hidden: + + projections + insets_panels stats - colormaps + configuration + fonts cycles colors - fonts - configuration .. toctree:: :maxdepth: 1 @@ -150,15 +174,17 @@ For more details, check the full :doc:`User guide ` and :doc:`API Referen :hidden: api + keyword_aliases lazy_loading external-links + faq whats_new contributing about .. toctree:: :maxdepth: 1 - :caption: Dev Zone + :caption: Development :hidden: plot_comparison_results diff --git a/docs/keyword_aliases.rst b/docs/keyword_aliases.rst new file mode 100644 index 000000000..9a0f0188a --- /dev/null +++ b/docs/keyword_aliases.rst @@ -0,0 +1,49 @@ +Keyword vocabulary +================== + +UltraPlot accepts a broad set of keyword spellings so existing Matplotlib and +older UltraPlot code continues to work. New users only need a small canonical +front door. The table below is the vocabulary used throughout the beginner +recipes; aliases remain supported, but we do not teach every alias in every +example. + +Canonical names and supported aliases +------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 22 34 44 + + * - Area + - Canonical + - Also supported + * - Layout + - ``refwidth``, ``refheight``, ``refaspect`` + - ``axwidth``, ``axheight``, ``aspect`` + * - Layout + - ``figwidth``, ``figheight``, ``wratios``, ``hratios`` + - ``width``, ``height``, ``width_ratios``, ``height_ratios`` + * - Plot styling + - ``linewidth``, ``color`` + - ``lw``, ``linewidths``, ``c``, ``colors`` + * - Panel geometry + - ``span`` + - ``row``, ``rows``, ``col``, ``cols`` + * - Shared axes + - ``sharex``, ``sharey`` + - ``share`` (sets both) + * - Figure + - ``suptitle`` + - ``figtitle`` + * - Guides + - ``loc``, ``ncols`` + - ``location``, ``ncol`` + * - Export + - ``fig.save(...)`` + - ``fig.savefig(...)`` + +Prefer the canonical column when starting new code. This is a documentation +choice only: this page does not change runtime behavior or deprecate aliases. + +See :doc:`recipes` for short, copyable figures using this vocabulary and the +full API reference for the complete keyword signatures. diff --git a/docs/recipes.py b/docs/recipes.py new file mode 100644 index 000000000..c78e86028 --- /dev/null +++ b/docs/recipes.py @@ -0,0 +1,89 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_recipes: +# +# Common figure recipes +# ===================== +# +# These small, complete examples cover the patterns most figures need. Copy a +# recipe into a script or notebook and adapt the data and labels. + +# %% +import numpy as np +import ultraplot as uplt + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Labelled line +# ------------- +# Use ``label`` on each artist and format the axes in one place. + +# %% +x = np.linspace(0, 2 * np.pi, 200) +fig, ax = uplt.subplots(refwidth=3.2) +ax.plot(x, np.sin(x), label="sine") +ax.plot(x, np.cos(x), label="cosine") +ax.format(xlabel="angle", ylabel="value", title="Two signals") +ax.legend(loc="ur") + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Shared-axis grid +# ---------------- +# ``share="labels"`` keeps the grid readable while retaining tick labels. + +# %% +fig, axs = uplt.subplots(nrows=2, ncols=2, refwidth=2.0, share="labels") +for number, ax in enumerate(axs, start=1): + ax.plot(x, np.sin(x + number / 3), color=f"C{number - 1}") + ax.format(title=f"Panel {number}") +axs.format(xlabel="x", ylabel="y", suptitle="A shared-axis grid") + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Image with a colorbar +# --------------------- +# Plot the returned mappable and request an outer colorbar with +# :meth:`~ultraplot.axes.Axes.colorbar`. + +# %% +image = np.outer(np.sin(x[:60]), np.cos(x[:60])) +fig, ax = uplt.subplots(refwidth=3.0) +ax.imshow(image, cmap="viridis", colorbar="r") +ax.format(title="Image data", xformatter="none", yformatter="none") + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Figure-wide legend +# ------------------ +# A :meth:`~ultraplot.figure.Figure.legend` collects labelled artists across +# the selected axes. + +# %% +fig, axs = uplt.subplots(ncols=2, refwidth=2.2) +for ax, phase in zip(axs, (0, np.pi / 2)): + ax.plot(x, np.sin(x + phase), label="signal") + ax.plot(x, np.cos(x + phase), label="reference") + ax.format(title=f"phase = {phase:.2g}") +fig.legend(loc="b", ncols=2) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# Publication-sized export +# ------------------------ +# Set a physical figure width and write a vector file for a manuscript with +# :meth:`~ultraplot.figure.Figure.save`. + +# %% +fig, ax = uplt.subplots(figwidth="89mm", refaspect=1.6) +ax.plot(x, np.sin(x), color="C0") +ax.format(xlabel="angle", ylabel="value") +fig.save("figure.pdf") diff --git a/docs/usage.rst b/docs/usage.rst index 298c12132..96a1d96fb 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -10,13 +10,31 @@ .. _usage: -============= +=============== Using UltraPlot -============= +=============== -This page offers a condensed overview of UltraPlot's features. It is populated -with links to the :ref:`API reference` and :ref:`User Guide `. -For a more in-depth discussion, see :ref:`why`. +This page is an orientation to UltraPlot's main concepts and building blocks. +For a hands-on start, make a :doc:`first figure ` and then use +the :doc:`recipes` and :ref:`User Guide ` to explore the features +that fit your workflow. The :ref:`API reference` is the detailed reference; +for design context and motivation, see :doc:`Why UltraPlot `. + +.. _usage_first_figure: + +Your first finished figure +========================== + +The recommended learning path is deliberately short: + +#. :doc:`Install UltraPlot ` and import it as ``uplt``. +#. Follow the :doc:`first figure ` walkthrough to create, format, and save a + complete figure. +#. Try a related :doc:`recipe `, then open the guide for the plot + type or feature you want to add. + +You can continue using familiar Matplotlib plotting methods and objects while +adopting UltraPlot's figure layout and formatting helpers incrementally. .. _usage_background: diff --git a/docs/why_ultraplot.py b/docs/why_ultraplot.py new file mode 100644 index 000000000..1bc751f23 --- /dev/null +++ b/docs/why_ultraplot.py @@ -0,0 +1,129 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.11.4 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _why_ultraplot: +# +# Why UltraPlot? +# ============= +# +# Matplotlib is an incredibly powerful plotting engine, but creating multi-panel, +# publication-ready figures often requires repetitive boilerplate code. UltraPlot +# solves this by adding a concise, intuitive layer for layout and formatting, +# while letting you keep the familiar Matplotlib object-oriented methods you +# already know. +# +# .. note:: +# +# If you just need a quick, exploratory plot, vanilla Matplotlib is perfect. +# UltraPlot truly shines when you are managing multi-panel figures or need +# consistent, publication-quality styling across your work. +# +# The comparison below uses a straightforward two-panel figure to illustrate the +# difference. This isn't about code golf or minimizing lines; it's about the +# separation of concerns. Matplotlib handles the plotting primitives, while +# UltraPlot gives you a streamlined syntax to orchestrate the broader figure layout. + +# %% [raw] raw_mimetype="text/restructuredtext" +# Setting up the data +# ------------------- +# +# To keep things fully reproducible without needing external downloads, we will +# generate some basic synthetic data locally. The data itself is intentionally +# simple so we can focus entirely on the plotting mechanics. + +# %% +import numpy as np + +SEED = 51423 +rng = np.random.RandomState(SEED) +x = np.linspace(0, 10, 100) +line = np.sin(x) + 0.08 * rng.randn(x.size) +image = np.outer(np.sin(x / 2), np.cos(x / 3)) + + +# %% [raw] raw_mimetype="text/restructuredtext" +# The Matplotlib approach +# ----------------------- +# +# Matplotlib's explicit API is fantastic when you need surgical control over every +# individual artist on the canvas. Here is how you would conventionally build and +# format this two-panel figure using standard Matplotlib. +# +# For a deep dive into this approach, see the `Matplotlib subplots tutorial +# `__. + +# %% +import matplotlib.pyplot as plt + +mpl_fig, mpl_axs = plt.subplots(1, 2, figsize=(7, 3), constrained_layout=True) +mpl_axs[0].plot(x, line, label="signal", color="tab:blue") +mpl_axs[0].set(xlabel="x", ylabel="value", title="Line") +mpl_axs[0].legend(loc="upper right") +mpl_mesh = mpl_axs[1].imshow(image, origin="lower", aspect="auto", cmap="viridis") +mpl_axs[1].set(xlabel="column", ylabel="row", title="Image") +mpl_fig.colorbar(mpl_mesh, ax=mpl_axs[1], label="intensity") +mpl_fig.suptitle("The same two-panel figure") +mpl_fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# The UltraPlot approach +# ---------------------- +# +# Notice that the actual drawing commands, +# :meth:`~ultraplot.axes.PlotAxes.plot` and +# :meth:`~ultraplot.axes.PlotAxes.imshow`, are identical to the Matplotlib +# version above. UltraPlot adds :func:`~ultraplot.ui.subplots` for figure +# construction and :meth:`~ultraplot.axes.Axes.format` for panel formatting. +# +# Instead of scattering setter methods across your script, UltraPlot lets you +# define figure layouts and shared labels cohesively. As your figures grow in +# complexity, this centralized formatting keeps your code clean and readable. +# +# Discover more in the :ref:`format command ` guide and the +# :func:`~ultraplot.ui.subplots` API reference. + +# %% +import ultraplot as uplt + +uplt_fig, uplt_axs = uplt.subplots(ncols=2, share=False, refwidth=2.3) +uplt_axs[0].plot(x, line, label="signal", color="tab:blue") +uplt_mesh = uplt_axs[1].imshow(image, origin="lower", aspect="auto", cmap="viridis") +uplt_axs[0].format(title="Line", xlabel="x", ylabel="value") +uplt_axs[0].legend(loc="ur") +uplt_axs[1].format(title="Image", xlabel="column", ylabel="row") +uplt_fig.format(suptitle="The same two-panel figure") +uplt_fig.colorbar(uplt_mesh, loc="r", label="intensity") +uplt_fig.show() + + +# %% [raw] raw_mimetype="text/restructuredtext" +# The takeaway +# ------------ +# +# UltraPlot does not reinvent the wheel—it just makes it easier to steer. A good +# mental model for your workflow looks like this: +# +# * Use standard axes methods such as :meth:`~ultraplot.axes.PlotAxes.plot`, +# :meth:`~ultraplot.axes.PlotAxes.imshow`, and +# :meth:`~ultraplot.axes.PlotAxes.scatter` to draw data. +# * Use :meth:`~ultraplot.axes.Axes.format` through ``axs.format()`` to apply +# consistent labels, ticks, and styling at the panel level. +# * Use :meth:`~ultraplot.figure.Figure.format` through ``fig.format()`` for +# global aesthetics, and :meth:`~ultraplot.figure.Figure.colorbar` for a +# shared colorbar. +# +# For a single, fast plot, stick with Matplotlib. When layout scaling and repetitive +# formatting become a chore, let UltraPlot handle the heavy lifting.