30
30
:ref:`figure_explanation`.
31
31
"""
32
32
33
- from contextlib import ExitStack
34
33
import inspect
35
34
import itertools
36
35
import logging
37
- from numbers import Integral
38
36
import threading
37
+ from contextlib import ExitStack
38
+ from numbers import Integral
39
39
40
40
import numpy as np
41
41
42
42
import matplotlib as mpl
43
- from matplotlib import _blocking_input , backend_bases , _docstring , projections
44
- from matplotlib .artist import (
45
- Artist , allow_rasterization , _finalize_rasterization )
46
- from matplotlib .backend_bases import (
47
- DrawEvent , FigureCanvasBase , NonGuiException , MouseButton , _get_renderer )
48
43
import matplotlib ._api as _api
49
44
import matplotlib .cbook as cbook
50
45
import matplotlib .colorbar as cbar
51
46
import matplotlib .image as mimage
52
-
47
+ import matplotlib .legend as mlegend
48
+ from matplotlib import _blocking_input , _docstring , backend_bases , projections
49
+ from matplotlib .artist import Artist , _finalize_rasterization , allow_rasterization
53
50
from matplotlib .axes import Axes
51
+ from matplotlib .backend_bases import (
52
+ DrawEvent ,
53
+ FigureCanvasBase ,
54
+ MouseButton ,
55
+ NonGuiException ,
56
+ _get_renderer ,
57
+ )
54
58
from matplotlib .gridspec import GridSpec
55
59
from matplotlib .layout_engine import (
56
- ConstrainedLayoutEngine , TightLayoutEngine , LayoutEngine ,
57
- PlaceHolderLayoutEngine
60
+ ConstrainedLayoutEngine ,
61
+ LayoutEngine ,
62
+ PlaceHolderLayoutEngine ,
63
+ TightLayoutEngine ,
58
64
)
59
- import matplotlib .legend as mlegend
60
65
from matplotlib .patches import Rectangle
61
66
from matplotlib .text import Text
62
- from matplotlib .transforms import (Affine2D , Bbox , BboxTransformTo ,
63
- TransformedBbox )
67
+ from matplotlib .transforms import Affine2D , Bbox , BboxTransformTo , TransformedBbox
64
68
65
69
_log = logging .getLogger (__name__ )
66
70
@@ -1871,11 +1875,18 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
1871
1875
The Axes identifiers may be `str` or a non-iterable hashable
1872
1876
object (e.g. `tuple` s may not be used).
1873
1877
1874
- sharex, sharey : bool, default: False
1875
- If True, the x-axis (*sharex*) or y-axis (*sharey*) will be shared
1876
- among all subplots. In that case, tick label visibility and axis
1877
- units behave as for `subplots`. If False, each subplot's x- or
1878
- y-axis will be independent.
1878
+ sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default: False
1879
+ Controls sharing of x-axis (*sharex*) or y-axis (*sharey*):
1880
+
1881
+ - True or 'all': x- or y-axis will be shared among all subplots.
1882
+ - False or 'none': each subplot x- or y-axis will be independent.
1883
+ - 'row': each subplot with the same rows span will share an x- or
1884
+ y-axis.
1885
+ - 'col': each subplot with the same column span will share an x- or
1886
+ y-axis.
1887
+
1888
+ Tick label visibility and axis units behave as for `subplots`.
1889
+
1879
1890
1880
1891
width_ratios : array-like of length *ncols*, optional
1881
1892
Defines the relative widths of the columns. Each column gets a
@@ -1934,6 +1945,13 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
1934
1945
gridspec_kw = dict (gridspec_kw or {})
1935
1946
per_subplot_kw = per_subplot_kw or {}
1936
1947
1948
+ # Only accept strict bool and str to allow a possible future API expansion.
1949
+ _api .check_isinstance ((bool , str ), sharex = sharex , sharey = sharey )
1950
+ if not isinstance (sharex , str ):
1951
+ sharex = "all" if sharex else "none"
1952
+ if not isinstance (sharey , str ):
1953
+ sharey = "all" if sharey else "none"
1954
+
1937
1955
if height_ratios is not None :
1938
1956
if 'height_ratios' in gridspec_kw :
1939
1957
raise ValueError ("'height_ratios' must not be defined both as "
@@ -1954,9 +1972,6 @@ def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False,
1954
1972
1955
1973
per_subplot_kw = self ._norm_per_subplot_kw (per_subplot_kw )
1956
1974
1957
- # Only accept strict bools to allow a possible future API expansion.
1958
- _api .check_isinstance (bool , sharex = sharex , sharey = sharey )
1959
-
1960
1975
def _make_array (inp ):
1961
1976
"""
1962
1977
Convert input into 2D array
@@ -2015,6 +2030,33 @@ def _identify_keys_and_nested(mosaic):
2015
2030
2016
2031
return tuple (unique_ids ), nested
2017
2032
2033
+ def _parse_mosaic_to_span (mosaic , unique_ids ):
2034
+ """
2035
+ Maps the mosaic label/ids to the row and column span.
2036
+
2037
+ Returns
2038
+ -------
2039
+ dict[str, (row_slice, col_slice)]
2040
+ """
2041
+ ids_to_span = {}
2042
+ for id_ in unique_ids :
2043
+ # sort out where each axes starts/ends
2044
+ indx = np .argwhere (mosaic == id_ )
2045
+ start_row , start_col = np .min (indx , axis = 0 )
2046
+ end_row , end_col = np .max (indx , axis = 0 ) + 1
2047
+ # and construct the slice object
2048
+ slc = (slice (start_row , end_row ), slice (start_col , end_col ))
2049
+
2050
+ if (mosaic [slc ] != id_ ).any ():
2051
+ raise ValueError (
2052
+ f"While trying to layout\n { mosaic !r} \n "
2053
+ f"we found that the label { id_ !r} specifies a "
2054
+ "non-rectangular or non-contiguous area." )
2055
+
2056
+ ids_to_span [id_ ] = slc
2057
+
2058
+ return ids_to_span
2059
+
2018
2060
def _do_layout (gs , mosaic , unique_ids , nested ):
2019
2061
"""
2020
2062
Recursively do the mosaic.
@@ -2045,22 +2087,14 @@ def _do_layout(gs, mosaic, unique_ids, nested):
2045
2087
# nested mosaic) at this level
2046
2088
this_level = dict ()
2047
2089
2090
+ label_to_span = _parse_mosaic_to_span (mosaic , unique_ids )
2091
+
2048
2092
# go through the unique keys,
2049
- for name in unique_ids :
2050
- # sort out where each axes starts/ends
2051
- indx = np .argwhere (mosaic == name )
2052
- start_row , start_col = np .min (indx , axis = 0 )
2053
- end_row , end_col = np .max (indx , axis = 0 ) + 1
2054
- # and construct the slice object
2055
- slc = (slice (start_row , end_row ), slice (start_col , end_col ))
2056
- # some light error checking
2057
- if (mosaic [slc ] != name ).any ():
2058
- raise ValueError (
2059
- f"While trying to layout\n { mosaic !r} \n "
2060
- f"we found that the label { name !r} specifies a "
2061
- "non-rectangular or non-contiguous area." )
2093
+ for label , slc in label_to_span .items ():
2062
2094
# and stash this slice for later
2063
- this_level [(start_row , start_col )] = (name , slc , 'axes' )
2095
+ start_row = slc [0 ].start
2096
+ start_col = slc [1 ].start
2097
+ this_level [(start_row , start_col )] = (label , slc , 'axes' )
2064
2098
2065
2099
# do the same thing for the nested mosaics (simpler because these
2066
2100
# cannot be spans yet!)
@@ -2070,24 +2104,25 @@ def _do_layout(gs, mosaic, unique_ids, nested):
2070
2104
# now go through the things in this level and add them
2071
2105
# in order left-to-right top-to-bottom
2072
2106
for key in sorted (this_level ):
2073
- name , arg , method = this_level [key ]
2107
+ label , arg , method = this_level [key ]
2074
2108
# we are doing some hokey function dispatch here based
2075
2109
# on the 'method' string stashed above to sort out if this
2076
2110
# element is an Axes or a nested mosaic.
2077
2111
if method == 'axes' :
2078
2112
slc = arg
2079
2113
# add a single axes
2080
- if name in output :
2081
- raise ValueError (f"There are duplicate keys { name } "
2114
+ if label in output :
2115
+ raise ValueError (f"There are duplicate keys { label } "
2082
2116
f"in the layout\n { mosaic !r} " )
2117
+
2083
2118
ax = self .add_subplot (
2084
2119
gs [slc ], ** {
2085
- 'label' : str (name ),
2120
+ 'label' : str (label ),
2086
2121
** subplot_kw ,
2087
- ** per_subplot_kw .get (name , {})
2122
+ ** per_subplot_kw .get (label , {})
2088
2123
}
2089
2124
)
2090
- output [name ] = ax
2125
+ output [label ] = ax
2091
2126
elif method == 'nested' :
2092
2127
nested_mosaic = arg
2093
2128
j , k = key
@@ -2113,15 +2148,75 @@ def _do_layout(gs, mosaic, unique_ids, nested):
2113
2148
mosaic = _make_array (mosaic )
2114
2149
rows , cols = mosaic .shape
2115
2150
gs = self .add_gridspec (rows , cols , ** gridspec_kw )
2116
- ret = _do_layout (gs , mosaic , * _identify_keys_and_nested (mosaic ))
2117
- ax0 = next (iter (ret .values ()))
2118
- for ax in ret .values ():
2119
- if sharex :
2151
+ unique_labels , nested_coord_to_labels = _identify_keys_and_nested (mosaic )
2152
+ ret = _do_layout (gs , mosaic , unique_labels , nested_coord_to_labels )
2153
+
2154
+ # Handle axes sharing
2155
+
2156
+ def _find_row_col_groups (mosaic , unique_labels ):
2157
+ label_to_span = _parse_mosaic_to_span (mosaic , unique_labels )
2158
+
2159
+ row_group = {}
2160
+ col_group = {}
2161
+ for label , (row_slice , col_slice ) in label_to_span .items ():
2162
+ start_row , end_row = row_slice .start , row_slice .stop
2163
+ start_col , end_col = col_slice .start , col_slice .stop
2164
+
2165
+ if (start_col , end_col ) not in col_group :
2166
+ col_group [(start_col , end_col )] = [label ]
2167
+ else :
2168
+ col_group [(start_col , end_col )].append (label )
2169
+
2170
+ if (start_row , end_row ) not in row_group :
2171
+ row_group [(start_row , end_row )] = [label ]
2172
+ else :
2173
+ row_group [(start_row , end_row )].append (label )
2174
+
2175
+ return row_group , col_group
2176
+
2177
+ # Pairs of axes where the first axes is meant to call sharex/sharey on
2178
+ # the second axes. The second axes depends on if the sharex/sharey is
2179
+ # set to "row", "col", or "all".
2180
+ share_axes_pairs = {
2181
+ "all" : tuple (
2182
+ (ax , next (iter (ret .values ()))) for ax in ret .values ()
2183
+ )
2184
+ }
2185
+ if sharex in ("row" , "col" ) or sharey in ("row" , "col" ):
2186
+ if nested_coord_to_labels :
2187
+ raise ValueError (
2188
+ "Cannot share axes by row or column when using nested mosaic"
2189
+ )
2190
+ else :
2191
+ row_groups , col_groups = _find_row_col_groups (mosaic , unique_labels )
2192
+
2193
+ share_axes_pairs .update ({
2194
+ "row" : tuple (
2195
+ (ret [label ], ret [row_group [0 ]])
2196
+ for row_group in row_groups .values ()
2197
+ for label in row_group
2198
+ ),
2199
+ "col" : tuple (
2200
+ (ret [label ], ret [col_group [0 ]])
2201
+ for col_group in col_groups .values ()
2202
+ for label in col_group
2203
+ ),
2204
+ })
2205
+
2206
+ if sharex in share_axes_pairs :
2207
+ for ax , ax0 in share_axes_pairs [sharex ]:
2120
2208
ax .sharex (ax0 )
2121
- ax ._label_outer_xaxis (skip_non_rectangular_axes = True )
2122
- if sharey :
2209
+ if sharex in ["col" , "all" ] and ax0 is not ax :
2210
+ ax0 ._label_outer_xaxis (skip_non_rectangular_axes = True )
2211
+ ax ._label_outer_xaxis (skip_non_rectangular_axes = True )
2212
+
2213
+ if sharey in share_axes_pairs :
2214
+ for ax , ax0 in share_axes_pairs [sharey ]:
2123
2215
ax .sharey (ax0 )
2124
- ax ._label_outer_yaxis (skip_non_rectangular_axes = True )
2216
+ if sharey in ["row" , "all" ] and ax0 is not ax :
2217
+ ax0 ._label_outer_yaxis (skip_non_rectangular_axes = True )
2218
+ ax ._label_outer_yaxis (skip_non_rectangular_axes = True )
2219
+
2125
2220
if extra := set (per_subplot_kw ) - set (ret ):
2126
2221
raise ValueError (
2127
2222
f"The keys { extra } are in *per_subplot_kw* "
@@ -2155,7 +2250,7 @@ class SubFigure(FigureBase):
2155
2250
See :doc:`/gallery/subplots_axes_and_figures/subfigures`
2156
2251
2157
2252
.. note::
2158
- The *subfigure* concept is new in v3.4, and the API is still provisional.
2253
+ The *subfigure* concept is new in v3.4, and API is still provisional.
2159
2254
"""
2160
2255
2161
2256
def __init__ (self , parent , subplotspec , * ,
@@ -3213,8 +3308,8 @@ def __setstate__(self, state):
3213
3308
3214
3309
if restore_to_pylab :
3215
3310
# lazy import to avoid circularity
3216
- import matplotlib .pyplot as plt
3217
3311
import matplotlib ._pylab_helpers as pylab_helpers
3312
+ import matplotlib .pyplot as plt
3218
3313
allnums = plt .get_fignums ()
3219
3314
num = max (allnums ) + 1 if allnums else 1
3220
3315
backend = plt ._get_backend_mod ()
0 commit comments