Skip to content
Draft
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
3 changes: 0 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ Configuration tools

.. automodsumm:: ultraplot.config
:toctree: api
:skip: inline_backend_fmt, RcConfigurator


Constructor functions
Expand All @@ -79,7 +78,6 @@ Constructor functions

.. automodsumm:: ultraplot.constructor
:toctree: api
:skip: Colors


Locators and formatters
Expand Down Expand Up @@ -110,7 +108,6 @@ Colormaps and normalizers

.. automodsumm:: ultraplot.colors
:toctree: api
:skip: ListedColormap, LinearSegmentedColormap, PerceptuallyUniformColormap, LinearSegmentedNorm


Projection classes
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,5 @@ classes and :ref:`constructor functions <why_constructor>`.
:ref:`modifying individual settings, changing settings in bulk, and
temporarily changing settings in context blocks <ug_rc>`.
It also introduces several :ref:`new setings <ug_config>`
and sets up the inline plotting backend with :func:`~ultraplot.config.inline_backend_fmt`
and sets up the inline plotting backend with :func:`~ultraplot.config.config_inline_backend`
so that your inline figures look the same as your saved figures.
6 changes: 0 additions & 6 deletions ultraplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,7 @@
from .colors import DiscreteColormap as DiscreteColormap
from .colors import DiscreteNorm as DiscreteNorm
from .colors import DivergingNorm as DivergingNorm
from .colors import LinearSegmentedColormap as LinearSegmentedColormap
from .colors import LinearSegmentedNorm as LinearSegmentedNorm
from .colors import ListedColormap as ListedColormap
from .colors import PerceptualColormap as PerceptualColormap
from .colors import PerceptuallyUniformColormap as PerceptuallyUniformColormap
from .colors import SegmentedNorm as SegmentedNorm
from .colors import _cmap_database as colormaps
from .config import config_inline_backend as config_inline_backend
Expand Down Expand Up @@ -138,14 +134,12 @@
from .utils import edges as edges
from .utils import edges2d as edges2d
from .utils import get_colors as get_colors
from .utils import saturate as saturate
from .utils import scale_luminance as scale_luminance
from .utils import scale_saturation as scale_saturation
from .utils import set_alpha as set_alpha
from .utils import set_hue as set_hue
from .utils import set_luminance as set_luminance
from .utils import set_saturation as set_saturation
from .utils import shade as shade
from .utils import shift_hue as shift_hue
from .utils import to_hex as to_hex
from .utils import to_rgb as to_rgb
Expand Down
238 changes: 131 additions & 107 deletions ultraplot/_subplots.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from numbers import Integral
from typing import TYPE_CHECKING
from typing import Protocol

import matplotlib.axes as maxes
import matplotlib.gridspec as mgridspec
Expand All @@ -15,22 +15,138 @@
from . import gridspec as pgridspec
from .internals import _not_none, _pop_params, warnings

if TYPE_CHECKING:
from .figure import Figure

def parse_backend(backend=None, basemap=None):
"""
Handle deprecation of basemap and cartopy package.
"""
if backend == "basemap":
warnings._warn_ultraplot(
f"{backend=} will be deprecated in next major release (v2.0). "
"See https://github.com/Ultraplot/ultraplot/pull/243"
)
return backend


def parse_proj(
proj=None,
projection=None,
proj_kw=None,
projection_kw=None,
backend=None,
basemap=None,
**kwargs,
):
"""
Translate user-input projection into a registered matplotlib axes class.
"""
# Parse arguments
proj = _not_none(proj=proj, projection=projection, default="cartesian")
proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={})
backend = parse_backend(backend, basemap)
if isinstance(proj, str):
proj = proj.lower()

# Search axes projections
name = None

# Handle cartopy/basemap Projection objects directly
# These should be converted to Ultraplot GeoAxes
if not isinstance(proj, str):
if constructor.Projection is not object and isinstance(
proj, constructor.Projection
):
name = "ultraplot_cartopy"
kwargs["map_projection"] = proj
elif constructor.Basemap is not object and isinstance(
proj, constructor.Basemap
):
name = "ultraplot_basemap"
kwargs["map_projection"] = proj

if name is None and isinstance(proj, str):
try:
mproj.get_projection_class("ultraplot_" + proj)
except (KeyError, ValueError):
pass
else:
name = "ultraplot_" + proj
if name is None and isinstance(proj, str):
# Try geographic projections first if cartopy/basemap available
if constructor.Projection is not object or constructor.Basemap is not object:
try:
proj_obj = constructor.Proj(
proj, backend=backend, include_axes=True, **proj_kw
)
name = "ultraplot_" + proj_obj._proj_backend
kwargs["map_projection"] = proj_obj
except ValueError:
pass # not a geographic projection, try matplotlib registry below

# If not geographic, check if registered globally in matplotlib
# (e.g., 'ternary', 'polar', '3d')
if name is None and proj in mproj.get_projection_names():
name = proj

if name is None and isinstance(proj, str):
raise ValueError(
f"Invalid projection name {proj!r}. If you are trying to generate a "
"GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap "
"then cartopy or basemap must be installed. Otherwise the known axes "
f"subclasses are:\n{paxes._cls_table}"
)

if name is not None:
kwargs["projection"] = name
return kwargs


class FigureHost(Protocol):
"""
The members of `~ultraplot.figure.Figure` that `SubplotManager` calls *directly*.

Naming them keeps the upward dependency deliberate: a change to `Figure` reaches
the manager through these four names and nowhere else, and adding a fifth is a
visible edit here rather than a new attribute poke buried in a method.

WARNING: This is not a sufficient interface -- satisfying it is not enough to
drive a `SubplotManager`. `gridspec` assigns ``gs.figure = self.figure``, and
`~ultraplot.gridspec.GridSpec.figure` rejects anything that is not a real
`Figure`, then reads ``_gridspec_params`` and calls ``set_size_inches``. So the
manager still requires a concrete `Figure`; that requirement simply arrives
through `GridSpec` rather than through this protocol.
"""

def _native_add_subplot(self, *args, **kwargs):
"""Create an axes with matplotlib's ``add_subplot``, skipping overrides."""

def _mark_layout_dirty(self) -> None:
"""Record that the layout must be recomputed before the next draw."""

def _pop_format_params(self, kwargs: dict) -> dict:
"""Remove and return the figure-level ``format`` keywords in *kwargs*."""

def format(self, **kwargs) -> None:
"""Apply figure-level formatting."""


class SubplotManager:
"""
Manages subplot creation, gridspec ownership, and projection parsing
for a Figure instance.
Manages subplot creation and gridspec ownership for a figure.

Parameters
----------
figure : `~ultraplot.figure.Figure`
The parent figure.
The parent figure. `FigureHost` documents which of its members are used.
"""

def __init__(self, figure: "Figure"):
# Projection parsing is a pure function of its arguments -- aliased here
# because `ultraplot.ui` introspects these signatures to split figure
# keywords from subplot keywords.
parse_backend = staticmethod(parse_backend)
parse_proj = staticmethod(parse_proj)

def __init__(self, figure: FigureHost):
self.figure = figure
self.subplot_dict: dict = {}
self.counter: int = 0
Expand Down Expand Up @@ -60,101 +176,13 @@ def gridspec(self, gs):
self._gridspec = gs
gs.figure = self.figure # gridspec.figure should reference the real Figure

@staticmethod
def parse_backend(backend=None, basemap=None):
"""
Handle deprecation of basemap and cartopy package.
"""
if backend == "basemap":
warnings._warn_ultraplot(
f"{backend=} will be deprecated in next major release (v2.0). "
"See https://github.com/Ultraplot/ultraplot/pull/243"
)
return backend

def parse_proj(
self,
proj=None,
projection=None,
proj_kw=None,
projection_kw=None,
backend=None,
basemap=None,
**kwargs,
):
"""
Translate user-input projection into a registered matplotlib axes class.
"""
# Parse arguments
proj = _not_none(proj=proj, projection=projection, default="cartesian")
proj_kw = _not_none(proj_kw=proj_kw, projection_kw=projection_kw, default={})
backend = self.parse_backend(backend, basemap)
if isinstance(proj, str):
proj = proj.lower()

# Search axes projections
name = None

# Handle cartopy/basemap Projection objects directly
# These should be converted to Ultraplot GeoAxes
if not isinstance(proj, str):
if constructor.Projection is not object and isinstance(
proj, constructor.Projection
):
name = "ultraplot_cartopy"
kwargs["map_projection"] = proj
elif constructor.Basemap is not object and isinstance(
proj, constructor.Basemap
):
name = "ultraplot_basemap"
kwargs["map_projection"] = proj

if name is None and isinstance(proj, str):
try:
mproj.get_projection_class("ultraplot_" + proj)
except (KeyError, ValueError):
pass
else:
name = "ultraplot_" + proj
if name is None and isinstance(proj, str):
# Try geographic projections first if cartopy/basemap available
if (
constructor.Projection is not object
or constructor.Basemap is not object
):
try:
proj_obj = constructor.Proj(
proj, backend=backend, include_axes=True, **proj_kw
)
name = "ultraplot_" + proj_obj._proj_backend
kwargs["map_projection"] = proj_obj
except ValueError:
pass # not a geographic projection, try matplotlib registry below

# If not geographic, check if registered globally in matplotlib
# (e.g., 'ternary', 'polar', '3d')
if name is None and proj in mproj.get_projection_names():
name = proj

if name is None and isinstance(proj, str):
raise ValueError(
f"Invalid projection name {proj!r}. If you are trying to generate a "
"GeoAxes with a cartopy.crs.Projection or mpl_toolkits.basemap.Basemap "
"then cartopy or basemap must be installed. Otherwise the known axes "
f"subclasses are:\n{paxes._cls_table}"
)

if name is not None:
kwargs["projection"] = name
return kwargs

def add_subplot(self, *args, **kwargs):
"""
The driver function for adding single subplots.
"""
fig = self.figure
fig._layout_dirty = True
kwargs = self.parse_proj(**kwargs)
fig._mark_layout_dirty()
kwargs = parse_proj(**kwargs)

args = args or (1, 1, 1)
gs = self.gridspec
Expand Down Expand Up @@ -261,13 +289,9 @@ def add_subplot(self, *args, **kwargs):

kwargs.pop("_subplot_spec", None)

# NOTE: Skip past Figure.add_subplot (which routes back here) to the
# matplotlib implementation. Using super() rather than naming the
# matplotlib class keeps any mixin between Figure and matplotlib's
# Figure in a subclass MRO from being bypassed.
from .figure import Figure

ax = super(Figure, fig).add_subplot(ss, **kwargs)
# NOTE: The host skips past its own add_subplot (which routes back here)
# to the matplotlib implementation.
ax = fig._native_add_subplot(ss, **kwargs)
if ax.number:
self.subplot_dict[ax.number] = ax
return ax
Expand Down Expand Up @@ -352,7 +376,7 @@ def _axes_dict(naxs, input, kw=False, default=None):
proj = _axes_dict(naxs, proj, kw=False, default="cartesian")
proj_kw = _not_none(projection_kw=projection_kw, proj_kw=proj_kw) or {}
proj_kw = _axes_dict(naxs, proj_kw, kw=True)
backend = self.parse_backend(backend, basemap)
backend = parse_backend(backend, basemap)
backend = _axes_dict(naxs, backend, kw=False)
axes_kw = {
num: {"proj": proj[num], "proj_kw": proj_kw[num], "backend": backend[num]}
Expand All @@ -367,7 +391,7 @@ def _axes_dict(naxs, input, kw=False, default=None):
"parameters as keyword arguments instead."
)
kwargs.update(kw or {})
figure_kw = _pop_params(kwargs, fig._format_signature)
figure_kw = fig._pop_format_params(kwargs)
gridspec_kw = _pop_params(kwargs, pgridspec.GridSpec._update_params)

# Create or update the gridspec and add subplots with subplotspecs
Expand All @@ -388,7 +412,7 @@ def _axes_dict(naxs, input, kw=False, default=None):
y0, y1 = axrows[idx, 0], axrows[idx, 1]
ss = gs[y0 : y1 + 1, x0 : x1 + 1]
kw = {**kwargs, **axes_kw[num], "number": num}
axs[idx] = fig.add_subplot(ss, **kw)
axs[idx] = self.add_subplot(ss, **kw)
fig.format(skip_axes=True, **figure_kw)
return pgridspec.SubplotGrid(axs)

Expand Down
2 changes: 1 addition & 1 deletion ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3336,7 +3336,7 @@ def format(
ultraplot.config.Configurator.context
"""
if self.figure is not None:
self.figure._layout_dirty = True
self.figure._mark_layout_dirty()
skip_figure = kwargs.pop("skip_figure", False) # internal keyword arg
params = _pop_params(kwargs, self.figure._format_signature)

Expand Down
5 changes: 0 additions & 5 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4063,7 +4063,6 @@ def _scatter_c_is_scalar_data(
return False
return values.shape[0] == point_count

@warnings._rename_kwargs("0.6.0", centers="values")
def _parse_cmap(
self,
*args,
Expand Down Expand Up @@ -7703,9 +7702,5 @@ def _iter_arg_cols(self, *args, label=None, labels=None, values=None, **kwargs):
# Related parsing functions for warnings
_level_parsers = (_parse_level_vals, _parse_level_num, _parse_level_lim)

# Rename the shorthands
boxes = warnings._rename_objs("0.8.0", boxes=box)
violins = warnings._rename_objs("0.8.0", violins=violin)


# mock commit
Loading
Loading