Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extent to Plot.layout configuration #3552

Merged
merged 4 commits into from
Dec 9, 2023
Merged
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
20 changes: 19 additions & 1 deletion doc/_docstrings/objects.Plot.layout.ipynb
Original file line number Diff line number Diff line change
@@ -69,10 +69,28 @@
"p.facet([\"A\", \"B\"], [\"X\", \"Y\"]).layout(engine=\"constrained\")"
]
},
{
"cell_type": "markdown",
"id": "d61054d1-dcef-4e11-9802-394bcc633f9f",
"metadata": {},
"source": [
"With `extent`, you can control the size of the plot relative to the underlying figure. Because the notebook display adapts the figure background to the plot, this appears only to change the plot size in a notebook context. But it can be useful when saving or displaying through a `pyplot` GUI window:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1b5d5969-2925-474f-8e3c-99e4f90a7a2b",
"metadata": {},
"outputs": [],
"source": [
"p.layout(extent=[0, 0, .8, 1]).show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "781ff58c-b805-4e93-8cae-be0442e273ea",
"id": "e5c41b7d-a064-4406-8571-a544b194f3dc",
"metadata": {},
"outputs": [],
"source": []
26 changes: 21 additions & 5 deletions seaborn/_compat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations
from typing import Literal

import numpy as np
import matplotlib as mpl
from matplotlib.figure import Figure
from seaborn.utils import _version_predates


@@ -84,19 +88,31 @@ def register_colormap(name, cmap):
mpl.cm.register_cmap(name, cmap)


def set_layout_engine(fig, engine):
def set_layout_engine(
fig: Figure,
engine: Literal["constrained", "compressed", "tight", "none"],
) -> None:
"""Handle changes to auto layout engine interface in 3.6"""
if hasattr(fig, "set_layout_engine"):
fig.set_layout_engine(engine)
else:
# _version_predates(mpl, 3.6)
if engine == "tight":
fig.set_tight_layout(True)
fig.set_tight_layout(True) # type: ignore # predates typing
elif engine == "constrained":
fig.set_constrained_layout(True)
fig.set_constrained_layout(True) # type: ignore
elif engine == "none":
fig.set_tight_layout(False)
fig.set_constrained_layout(False)
fig.set_tight_layout(False) # type: ignore
fig.set_constrained_layout(False) # type: ignore


def get_layout_engine(fig: Figure) -> mpl.layout_engine.LayoutEngine | None:
"""Handle changes to auto layout engine interface in 3.6"""
if hasattr(fig, "get_layout_engine"):
return fig.get_layout_engine()
else:
# _version_predates(mpl, 3.6)
return None


def share_axis(ax0, ax1, which):
36 changes: 32 additions & 4 deletions seaborn/_core/plot.py
Original file line number Diff line number Diff line change
@@ -40,7 +40,7 @@
)
from seaborn._core.exceptions import PlotSpecError
from seaborn._core.rules import categorical_order
from seaborn._compat import set_layout_engine
from seaborn._compat import get_layout_engine, set_layout_engine
from seaborn.rcmod import axes_style, plotting_context
from seaborn.palettes import color_palette

@@ -810,6 +810,7 @@
*,
size: tuple[float, float] | Default = default,
engine: str | None | Default = default,
extent: tuple[float, float, float, float] | Default = default,
) -> Plot:
"""
Control the figure size and layout.
@@ -825,9 +826,14 @@
size : (width, height)
Size of the resulting figure, in inches. Size is inclusive of legend when
using pyplot, but not otherwise.
engine : {{"tight", "constrained", None}}
engine : {{"tight", "constrained", "none"}}
Name of method for automatically adjusting the layout to remove overlap.
The default depends on whether :meth:`Plot.on` is used.
extent : (left, bottom, right, top)
Boundaries of the plot layout, in fractions of the figure size. Takes
effect through the layout engine; exact results will vary across engines.
Note: the extent includes axis decorations when using a layout engine,
but it is exclusive of them when `engine="none"`.

Examples
--------
@@ -845,6 +851,8 @@
new._figure_spec["figsize"] = size
if engine is not default:
new._layout_spec["engine"] = engine
if extent is not default:
new._layout_spec["extent"] = extent

return new

@@ -1793,12 +1801,32 @@
if axis_key in self._scales: # TODO when would it not be?
self._scales[axis_key]._finalize(p, axis_obj)

if (engine := p._layout_spec.get("engine", default)) is not default:
if (engine_name := p._layout_spec.get("engine", default)) is not default:
# None is a valid arg for Figure.set_layout_engine, hence `default`
set_layout_engine(self._figure, engine)
set_layout_engine(self._figure, engine_name)
elif p._target is None:
# Don't modify the layout engine if the user supplied their own
# matplotlib figure and didn't specify an engine through Plot
# TODO switch default to "constrained"?
# TODO either way, make configurable
set_layout_engine(self._figure, "tight")

if (extent := p._layout_spec.get("extent")) is not None:
engine = get_layout_engine(self._figure)
if engine is None:
self._figure.subplots_adjust(*extent)
else:
# Note the different parameterization for the layout engine rect...
left, bottom, right, top = extent
width, height = right - left, top - bottom
try:
# The base LayoutEngine.set method doesn't have rect= so we need
# to avoid typechecking this statement. We also catch a TypeError
# as a plugin LayoutEngine may not support it either.
# Alternatively we could guard this with a check on the engine type,
# but that would make later-developed engines would un-useable.
engine.set(rect=[left, bottom, width, height]) # type: ignore
except TypeError:

Check warning on line 1829 in seaborn/_core/plot.py

Codecov / codecov/patch

seaborn/_core/plot.py#L1829

Added line #L1829 was not covered by tests
# Should we warn / raise? Note that we don't expect to get here
# under any normal circumstances.
pass

Check warning on line 1832 in seaborn/_core/plot.py

Codecov / codecov/patch

seaborn/_core/plot.py#L1832

Added line #L1832 was not covered by tests
2 changes: 1 addition & 1 deletion seaborn/_core/subplots.py
Original file line number Diff line number Diff line change
@@ -144,7 +144,7 @@ def init_figure(
pair_spec: PairSpec,
pyplot: bool = False,
figure_kws: dict | None = None,
target: Axes | Figure | SubFigure = None,
target: Axes | Figure | SubFigure | None = None,
) -> Figure:
"""Initialize matplotlib objects and add seaborn-relevant metadata."""
# TODO reduce need to pass pair_spec here?
26 changes: 26 additions & 0 deletions tests/_core/test_plot.py
Original file line number Diff line number Diff line change
@@ -1091,6 +1091,32 @@ def test_layout_size(self):
p = Plot().layout(size=size).plot()
assert tuple(p._figure.get_size_inches()) == size

@pytest.mark.skipif(
_version_predates(mpl, "3.6"),
reason="mpl<3.6 does not have get_layout_engine",
)
def test_layout_extent(self):

p = Plot().layout(extent=(.1, .2, .6, 1)).plot()
assert p._figure.get_layout_engine().get()["rect"] == [.1, .2, .5, .8]

@pytest.mark.skipif(
_version_predates(mpl, "3.6"),
reason="mpl<3.6 does not have get_layout_engine",
)
def test_constrained_layout_extent(self):

p = Plot().layout(engine="constrained", extent=(.1, .2, .6, 1)).plot()
assert p._figure.get_layout_engine().get()["rect"] == [.1, .2, .5, .8]

def test_base_layout_extent(self):

p = Plot().layout(engine=None, extent=(.1, .2, .6, 1)).plot()
assert p._figure.subplotpars.left == 0.1
assert p._figure.subplotpars.right == 0.6
assert p._figure.subplotpars.bottom == 0.2
assert p._figure.subplotpars.top == 1

def test_on_axes(self):

ax = mpl.figure.Figure().subplots()