Skip to content

Commit c278400

Browse files
ENH: added sharex/sharey string support to subplot_mosaic
1 parent 717459b commit c278400

3 files changed

Lines changed: 131 additions & 21 deletions

File tree

‎lib/matplotlib/figure.py‎

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,8 +2117,16 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
21172117

21182118
per_subplot_kw = self._norm_per_subplot_kw(per_subplot_kw)
21192119

2120-
# Only accept strict bools to allow a possible future API expansion.
2121-
_api.check_isinstance(bool, sharex=sharex, sharey=sharey)
2120+
# Custom Validation Function to check for booleans and strings.
2121+
def _validate_share_param(param_name, value):
2122+
if type(value) is bool:
2123+
return value
2124+
if value in ["all", "row", "col"]:
2125+
return value
2126+
raise ValueError(f"{param_name} must be True, False, 'all', 'row', or 'col'")
2127+
2128+
sharex = _validate_share_param("sharex", sharex)
2129+
sharey = _validate_share_param("sharey", sharey)
21222130

21232131
def _make_array(inp):
21242132
"""
@@ -2277,14 +2285,61 @@ def _do_layout(gs, mosaic, unique_ids, nested):
22772285
rows, cols = mosaic.shape
22782286
gs = self.add_gridspec(rows, cols, **gridspec_kw)
22792287
ret = _do_layout(gs, mosaic, *_identify_keys_and_nested(mosaic))
2280-
ax0 = next(iter(ret.values()))
2281-
for ax in ret.values():
2282-
if sharex:
2283-
ax.sharex(ax0)
2284-
ax._label_outer_xaxis(skip_non_rectangular_axes=True)
2285-
if sharey:
2286-
ax.sharey(ax0)
2287-
ax._label_outer_yaxis(skip_non_rectangular_axes=True)
2288+
# Asymmetrical/Directional Implementation that needs to be changed
2289+
# ax0 = next(iter(ret.values()))
2290+
# for ax in ret.values():
2291+
# if sharex:
2292+
# ax.sharex(ax0)
2293+
# ax._label_outer_xaxis(skip_non_rectangular_axes=True)
2294+
# if sharey:
2295+
# ax.sharey(ax0)
2296+
# ax._label_outer_yaxis(skip_non_rectangular_axes=True)
2297+
2298+
# Symmetric Sharing
2299+
def _apply_sharing(ret_dict, share_val, axis_name):
2300+
if share_val is False:
2301+
return
2302+
2303+
# Group the axes based on their spans
2304+
groups = {}
2305+
for ax in ret_dict.values():
2306+
span = ax.get_subplotspec()
2307+
grid = span.get_gridspec()
2308+
2309+
if share_val is True or share_val == "all":
2310+
groups.setdefault("all", []).append(ax)
2311+
elif share_val == "row":
2312+
groups.setdefault((grid, span.rowspan.start, span.rowspan.stop), []).append(ax)
2313+
elif share_val == "col":
2314+
groups.setdefault((grid, span.colspan.start, span.colspan.stop), []).append(ax)
2315+
2316+
# Bind the groups together
2317+
for group in groups.values():
2318+
if len(group) > 1:
2319+
parent = group[0]
2320+
for child_ax in group[1:]:
2321+
if axis_name == 'x':
2322+
child_ax.sharex(parent)
2323+
else:
2324+
child_ax.sharey(parent)
2325+
2326+
# Handle tick label visibility per isolated group
2327+
if axis_name == 'x':
2328+
# Find the bottom-most edge within a specific group
2329+
bottom_edge = max(ax.get_subplotspec().rowspan.stop for ax in group)
2330+
for ax in group:
2331+
if ax.get_subplotspec().rowspan.stop < bottom_edge:
2332+
ax.tick_params(labelbottom=False)
2333+
else:
2334+
# Find the left-most edge within a specific group
2335+
left_edge = min(ax.get_subplotspec().colspan.start for ax in group)
2336+
for ax in group:
2337+
if ax.get_subplotspec().colspan.start > left_edge:
2338+
ax.tick_params(labelleft=False)
2339+
2340+
_apply_sharing(ret, sharex, 'x')
2341+
_apply_sharing(ret, sharey, 'y')
2342+
22882343
if extra := set(per_subplot_kw) - set(ret):
22892344
raise ValueError(
22902345
f"The keys {extra} are in *per_subplot_kw* "

‎lib/matplotlib/pyplot.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
import sys
5555
import threading
5656
import time
57-
from typing import IO, TYPE_CHECKING, cast, overload
57+
from typing import IO, TYPE_CHECKING, cast, overload, Literal
5858

5959
from cycler import cycler # noqa: F401
6060
import matplotlib
@@ -1896,8 +1896,8 @@ def subplots(
18961896
def subplot_mosaic(
18971897
mosaic: str,
18981898
*,
1899-
sharex: bool = ...,
1900-
sharey: bool = ...,
1899+
sharex: bool | Literal["all", "row", "col"] = ...,
1900+
sharey: bool | Literal["all", "row", "col"] = ...,
19011901
width_ratios: ArrayLike | None = ...,
19021902
height_ratios: ArrayLike | None = ...,
19031903
empty_sentinel: str = ...,
@@ -1912,8 +1912,8 @@ def subplot_mosaic(
19121912
def subplot_mosaic[T](
19131913
mosaic: list[HashableList[T]],
19141914
*,
1915-
sharex: bool = ...,
1916-
sharey: bool = ...,
1915+
sharex: bool | Literal["all", "row", "col"] = ...,
1916+
sharey: bool | Literal["all", "row", "col"] = ...,
19171917
width_ratios: ArrayLike | None = ...,
19181918
height_ratios: ArrayLike | None = ...,
19191919
empty_sentinel: T = ...,
@@ -1928,8 +1928,8 @@ def subplot_mosaic[T](
19281928
def subplot_mosaic(
19291929
mosaic: list[HashableList[Hashable]],
19301930
*,
1931-
sharex: bool = ...,
1932-
sharey: bool = ...,
1931+
sharex: bool | Literal["all", "row", "col"] = ...,
1932+
sharey: bool | Literal["all", "row", "col"] = ...,
19331933
width_ratios: ArrayLike | None = ...,
19341934
height_ratios: ArrayLike | None = ...,
19351935
empty_sentinel: Any = ...,
@@ -1943,8 +1943,8 @@ def subplot_mosaic(
19431943
def subplot_mosaic[T](
19441944
mosaic: str | list[HashableList[T]] | list[HashableList[Hashable]],
19451945
*,
1946-
sharex: bool = False,
1947-
sharey: bool = False,
1946+
sharex: bool | Literal["all", "row", "col"] = False,
1947+
sharey: bool | Literal["all", "row", "col"] = False,
19481948
width_ratios: ArrayLike | None = None,
19491949
height_ratios: ArrayLike | None = None,
19501950
empty_sentinel: Any = '.',

‎lib/matplotlib/tests/test_figure.py‎

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,8 @@ def test_nested_user_order(self):
13201320
assert list(ax_dict) == list("ABCDEFGHI")
13211321
assert list(fig.axes) == list(ax_dict.values())
13221322

1323-
def test_share_all(self):
1323+
@pytest.mark.parametrize("share_val", [True, "all"])
1324+
def test_share_all(self, share_val):
13241325
layout = [
13251326
["A", [["B", "C"],
13261327
["D", "E"]]],
@@ -1329,11 +1330,65 @@ def test_share_all(self):
13291330
["."]]]]]
13301331
]
13311332
fig = plt.figure()
1332-
ax_dict = fig.subplot_mosaic(layout, sharex=True, sharey=True)
1333+
ax_dict = fig.subplot_mosaic(layout, sharex=share_val, sharey=share_val)
13331334
ax_dict["A"].set(xscale="log", yscale="logit")
13341335
assert all(ax.get_xscale() == "log" and ax.get_yscale() == "logit"
13351336
for ax in ax_dict.values())
13361337

1338+
def test_share_row_col(self):
1339+
layout = [
1340+
["A", "B", "C", "D"],
1341+
["E", "F", "C", "D"]
1342+
]
1343+
fig = plt.figure()
1344+
axd = fig.subplot_mosaic(layout, sharey="row", sharex="col")
1345+
1346+
# Testing row sharing for y-axis
1347+
axd["A"].set_ylim(0, 50)
1348+
axd["C"].set_ylim(-10, 10)
1349+
1350+
assert axd["B"].get_ylim() == (0, 50)
1351+
assert axd["D"].get_ylim() == (-10, 10)
1352+
assert axd["E"].get_ylim() != (0, 50)
1353+
1354+
axd["A"].set_xlim(-5, 5)
1355+
axd["C"].set_xlim(100, 200)
1356+
1357+
assert axd["E"].get_xlim() == (-5, 5)
1358+
assert axd["D"].get_xlim() != (100, 200)
1359+
1360+
def test_share_invalid(self):
1361+
layout = [
1362+
["A", "B"],
1363+
["C", "D"]
1364+
]
1365+
fig = plt.figure()
1366+
1367+
with pytest.raises(ValueError, match="must be True, False, 'all', 'row', or 'col'"):
1368+
fig.subplot_mosaic(layout, sharex="invalid_string")
1369+
1370+
with pytest.raises(ValueError, match="must be True, False, 'all', 'row', or 'col'"):
1371+
fig.subplot_mosaic(layout, sharey={"A": "B"})
1372+
1373+
def test_share_row_col_nested(self):
1374+
layout = [
1375+
["A", [["B", "C"],
1376+
["D", "E"]]]
1377+
]
1378+
fig = plt.figure()
1379+
axd = fig.subplot_mosaic(layout, sharey="row", sharex="col")
1380+
1381+
axd["B"].set_ylim(0, 50)
1382+
assert axd["C"].get_ylim() == (0, 50)
1383+
assert axd["D"].get_ylim() != (0, 50)
1384+
1385+
axd["B"].set_xlim(0, 50)
1386+
assert axd["D"].get_xlim() == (0, 50)
1387+
assert axd["C"].get_xlim() != (0, 50)
1388+
1389+
axd["A"].set_ylim(-10, 10)
1390+
assert axd["B"].get_ylim() != (-10, 10)
1391+
13371392

13381393
def test_reused_gridspec():
13391394
"""Test that these all use the same gridspec"""

0 commit comments

Comments
 (0)