Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions docs/colorbars_legends.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,85 @@
ax = axs[1]
ax.legend(hs2, loc="b", ncols=3, center=True, title="centered rows")
axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Legend formatting demo")

# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_semantic_legends:
# Semantic legends
# ----------------
#
# Legends usually annotate artists already drawn on an axes, but sometimes you need
# standalone semantic keys (categories, size scales, color levels, or geometry types).
# UltraPlot provides helper methods that build these entries directly:
#
# * :meth:`~ultraplot.axes.Axes.catlegend`
# * :meth:`~ultraplot.axes.Axes.sizelegend`
# * :meth:`~ultraplot.axes.Axes.numlegend`
# * :meth:`~ultraplot.axes.Axes.geolegend`

# %%
import cartopy.crs as ccrs
import shapely.geometry as sg

fig, ax = uplt.subplots(refwidth=4.2)
ax.format(title="Semantic legend helpers", grid=False)

ax.catlegend(
["A", "B", "C"],
colors={"A": "red7", "B": "green7", "C": "blue7"},
markers={"A": "o", "B": "s", "C": "^"},
loc="top",
frameon=False,
)
ax.sizelegend(
[10, 50, 200],
loc="upper right",
title="Population",
ncols=1,
frameon=False,
)
ax.numlegend(
vmin=0,
vmax=1,
n=5,
cmap="viko",
fmt="{:.2f}",
loc="ll",
ncols=1,
frameon=False,
)

poly1 = sg.Polygon([(0, 0), (2, 0), (1.2, 1.4)])
ax.geolegend(
[
("Triangle", "triangle"),
("Triangle-ish", poly1),
("Australia", "country:AU"),
("Netherlands (Mercator)", "country:NLD", "mercator"),
(
"Netherlands (Lambert)",
"country:NLD",
{
"country_proj": ccrs.LambertConformal(
central_longitude=5,
central_latitude=52,
),
"country_reso": "10m",
"country_territories": False,
"facecolor": "steelblue",
"fill": True,
},
),
],
loc="r",
ncols=1,
handlesize=2.4,
handletextpad=0.35,
frameon=False,
country_reso="10m",
)
ax.axis("off")


# %% [raw] raw_mimetype="text/restructuredtext"
# .. _ug_guides_decouple:
#
Expand Down
91 changes: 91 additions & 0 deletions docs/examples/legends_colorbars/03_semantic_legends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""
Semantic legends
================

Build legends from semantic mappings rather than existing artists.

Why UltraPlot here?
-------------------
UltraPlot adds semantic legend helpers directly on axes:
``catlegend``, ``sizelegend``, ``numlegend``, and ``geolegend``.
These are useful when you want legend meaning decoupled from plotted handles.

Key functions: :py:meth:`ultraplot.axes.Axes.catlegend`, :py:meth:`ultraplot.axes.Axes.sizelegend`, :py:meth:`ultraplot.axes.Axes.numlegend`, :py:meth:`ultraplot.axes.Axes.geolegend`.

See also
--------
* :doc:`Colorbars and legends </colorbars_legends>`
"""

# %%
import cartopy.crs as ccrs
import numpy as np
import shapely.geometry as sg
from matplotlib.path import Path

import ultraplot as uplt

np.random.seed(0)
data = np.random.randn(2, 100)
sizes = np.random.randint(10, 512, data.shape[1])
colors = np.random.rand(data.shape[1])

fig, ax = uplt.subplots()
ax.scatter(*data, color=colors, s=sizes, cmap="viko")
ax.format(title="Semantic legend helpers")

ax.catlegend(
["A", "B", "C"],
colors={"A": "red7", "B": "green7", "C": "blue7"},
markers={"A": "o", "B": "s", "C": "^"},
loc="top",
frameon=False,
)
ax.sizelegend(
[10, 50, 200],
loc="upper right",
title="Population",
ncols=1,
frameon=False,
)
ax.numlegend(
vmin=0,
vmax=1,
n=5,
cmap="viko",
fmt="{:.2f}",
loc="ll",
ncols=1,
frameon=False,
)

poly1 = sg.Polygon([(0, 0), (2, 0), (1.2, 1.4)])
ax.geolegend(
[
("Triangle", "triangle"),
("Triangle-ish", poly1),
("Australia", "country:AU"),
("Netherlands (Mercator)", "country:NLD", "mercator"),
(
"Netherlands (Lambert)",
"country:NLD",
{
"country_proj": ccrs.LambertConformal(
central_longitude=5,
central_latitude=52,
),
"country_reso": "10m",
"country_territories": False,
"facecolor": "steelblue",
"fill": True,
},
),
],
loc="r",
ncols=1,
handlesize=2.4,
handletextpad=0.35,
frameon=False,
country_reso="10m",
)
fig.show()
79 changes: 76 additions & 3 deletions ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2100,10 +2100,12 @@ def _parse_legend_centered(
return objs

@staticmethod
def _parse_legend_group(handles, labels=None):
def _parse_legend_group(handles, labels=None, handler_map=None):
"""
Parse possibly tuple-grouped input handles.
"""
handler_map_full = plegend.Legend.get_default_handler_map().copy()
handler_map_full.update(handler_map or {})

# Helper function. Retrieve labels from a tuple group or from objects
# in a container. Multiple labels lead to multiple legend entries.
Expand Down Expand Up @@ -2154,7 +2156,18 @@ def _legend_tuple(*objs): # noqa: E306
continue
handles.append(obj)
else:
warnings._warn_ultraplot(f"Ignoring invalid legend handle {obj!r}.")
try:
handler = plegend.Legend.get_legend_handler(
handler_map_full, obj
)
except Exception:
handler = None
if handler is not None:
handles.append(obj)
else:
warnings._warn_ultraplot(
f"Ignoring invalid legend handle {obj!r}."
)
return tuple(handles)

# Sanitize labels. Ignore e.g. extra hist() or hist2d() return values,
Expand Down Expand Up @@ -2247,7 +2260,9 @@ def _parse_legend_handles(
ihandles, ilabels = to_list(ihandles), to_list(ilabels)
if ihandles is None:
ihandles = self._get_legend_handles(handler_map)
ihandles, ilabels = self._parse_legend_group(ihandles, ilabels)
ihandles, ilabels = self._parse_legend_group(
ihandles, ilabels, handler_map=handler_map
)
ipairs = list(zip(ihandles, ilabels))
if alphabetize:
ipairs = sorted(ipairs, key=lambda pair: pair[1])
Expand Down Expand Up @@ -3487,6 +3502,64 @@ def legend(
**kwargs,
)

def catlegend(self, categories, **kwargs):
"""
Build categorical legend entries and optionally add a legend.

Parameters
----------
categories
Category labels used to generate legend handles.
**kwargs
Forwarded to `ultraplot.legend.UltraLegend.catlegend`.
Pass ``add=False`` to return ``(handles, labels)`` without drawing.
"""
return plegend.UltraLegend(self).catlegend(categories, **kwargs)

def sizelegend(self, levels, **kwargs):
"""
Build size legend entries and optionally add a legend.

Parameters
----------
levels
Numeric levels used to generate marker-size entries.
**kwargs
Forwarded to `ultraplot.legend.UltraLegend.sizelegend`.
Pass ``add=False`` to return ``(handles, labels)`` without drawing.
"""
return plegend.UltraLegend(self).sizelegend(levels, **kwargs)

def numlegend(self, levels=None, **kwargs):
"""
Build numeric-color legend entries and optionally add a legend.

Parameters
----------
levels
Numeric levels or number of levels.
**kwargs
Forwarded to `ultraplot.legend.UltraLegend.numlegend`.
Pass ``add=False`` to return ``(handles, labels)`` without drawing.
"""
return plegend.UltraLegend(self).numlegend(levels=levels, **kwargs)

def geolegend(self, entries, labels=None, **kwargs):
"""
Build geometry legend entries and optionally add a legend.

Parameters
----------
entries
Geometry entries (mapping, ``(label, geometry)`` pairs, or geometries).
labels
Optional labels for geometry sequences.
**kwargs
Forwarded to `ultraplot.legend.UltraLegend.geolegend`.
Pass ``add=False`` to return ``(handles, labels)`` without drawing.
"""
return plegend.UltraLegend(self).geolegend(entries, labels=labels, **kwargs)

@classmethod
def _coerce_curve_xy(cls, x, y):
"""
Expand Down
Loading