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 @@
+
+
+
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.