|
| 1 | +""" |
| 2 | +================= |
| 3 | +Subfigure mosaic |
| 4 | +================= |
| 5 | +
|
| 6 | +This example is inspired by the `subplot_mosaic() |
| 7 | +<https://matplotlib.org/devdocs/users/explain/axes/mosaic.html>`__ |
| 8 | +and `subfigures() |
| 9 | +<https://matplotlib.org/devdocs/gallery/subplots_axes_and_figures/subfigures.html>`__ |
| 10 | +examples. It especially aims to mimic the former. Most of the API |
| 11 | +that is going to be described below is analogous to the `.Figure.subplot_mosaic` API. |
| 12 | +
|
| 13 | +`.Figure.subfigure_mosaic` provides a simple way of constructing |
| 14 | +complex layouts, such as SubFigures that span multiple columns / rows |
| 15 | +of the layout or leave some areas of the Figure blank. The layouts are |
| 16 | +constructed either through ASCII art or nested lists. |
| 17 | +
|
| 18 | +This interface naturally supports naming your SubFigures. `.Figure.subfigure_mosaic` |
| 19 | +returns a dictionary keyed on the labels used to lay out the Figure. |
| 20 | +""" |
| 21 | +import matplotlib.pyplot as plt |
| 22 | +import numpy as np |
| 23 | + |
| 24 | + |
| 25 | +# Let's define a function to help visualize |
| 26 | +def identify_subfigs(subfig_dict, fontsize=36): |
| 27 | + """ |
| 28 | + Helper to identify the SubFigures in the examples below. |
| 29 | +
|
| 30 | + Draws the label in a large font in the center of the SubFigure. |
| 31 | +
|
| 32 | + Parameters |
| 33 | + ---------- |
| 34 | + subfig_dict : dict[str, SubFigure] |
| 35 | + Mapping between the title / label and the SubFigures. |
| 36 | + fontsize : int, optional |
| 37 | + How big the label should be. |
| 38 | + """ |
| 39 | + kw = dict(ha="center", va="center", fontsize=fontsize, color="darkgrey") |
| 40 | + for k, subfig in subfig_dict.items(): |
| 41 | + subfig.text(0.5, 0.5, k, **kw) |
| 42 | + |
| 43 | + |
| 44 | +# %% |
| 45 | +# If we want a 2x2 grid we can use `.Figure.subfigures` which returns a 2D array |
| 46 | +# of `.figure.SubFigure` which we can index. |
| 47 | + |
| 48 | +fig = plt.figure() |
| 49 | +subfigs = fig.subfigures(2, 2) |
| 50 | + |
| 51 | +subfigs[0, 0].set_edgecolor('black') |
| 52 | +subfigs[0, 0].set_linewidth(2.1) |
| 53 | +subfigs[1, 1].set_edgecolor('yellow') |
| 54 | +subfigs[1, 1].set_linewidth(2.1) |
| 55 | +subfigs[0, 1].set_facecolor('green') |
| 56 | + |
| 57 | +identify_subfigs( |
| 58 | + {(j, k): a for j, r in enumerate(subfigs) for k, a in enumerate(r)}, |
| 59 | +) |
| 60 | + |
| 61 | +# %% |
| 62 | +# Using `.Figure.subfigure_mosaic` we can produce the same mosaic but give the |
| 63 | +# SubFigures names |
| 64 | + |
| 65 | +fig = plt.figure() |
| 66 | +subfigs = fig.subfigure_mosaic( |
| 67 | + [ |
| 68 | + ["First", "Second"], |
| 69 | + ["Third", "Fourth"], |
| 70 | + ], |
| 71 | +) |
| 72 | +subfigs["First"].set_edgecolor('black') |
| 73 | +subfigs["First"].set_linewidth(2.1) |
| 74 | +subfigs["Second"].set_facecolor('green') |
| 75 | +subfigs["Fourth"].set_edgecolor('yellow') |
| 76 | +subfigs["Fourth"].set_linewidth(2.1) |
| 77 | + |
| 78 | +identify_subfigs(subfigs) |
| 79 | + |
| 80 | +# %% |
| 81 | +# A key difference between `.Figure.subfigures` and |
| 82 | +# `.Figure.subfigure_mosaic` is the return value. While the former |
| 83 | +# returns an array for index access, the latter returns a dictionary |
| 84 | +# mapping the labels to the `.figure.SubFigure` instances created |
| 85 | + |
| 86 | +print(subfigs) |
| 87 | + |
| 88 | +# %% |
| 89 | +# String short-hand |
| 90 | +# ================= |
| 91 | +# |
| 92 | +# By restricting our labels to single characters we can |
| 93 | +# "draw" the SubFigures we want as "ASCII art". The following |
| 94 | + |
| 95 | + |
| 96 | +mosaic = """ |
| 97 | + AB |
| 98 | + CD |
| 99 | + """ |
| 100 | + |
| 101 | +# %% |
| 102 | +# will give us 4 SubFigures laid out in a 2x2 grid and generate the same |
| 103 | +# subfigure mosaic as above (but now labeled with ``{"A", "B", "C", |
| 104 | +# "D"}`` rather than ``{"First", "Second", "Third", "Fourth"}``). |
| 105 | +# Bear in mind that subfigures do not come 'visible' the way subplots do. |
| 106 | +# In case you want them to be clearly visible - you will need to set certain |
| 107 | +# keyword arguments (such as edge/face color). This is discussed at length in the |
| 108 | +# :ref:`controlling-creation` part of this example. |
| 109 | + |
| 110 | +fig = plt.figure() |
| 111 | +subfigs = fig.subfigure_mosaic(mosaic) |
| 112 | +identify_subfigs(subfigs) |
| 113 | + |
| 114 | +# %% |
| 115 | +# Alternatively, you can use the more compact string notation: |
| 116 | +mosaic = "AB;CD" |
| 117 | + |
| 118 | +# %% |
| 119 | +# will give you the same composition, where the ``";"`` is used |
| 120 | +# as the row separator instead of newline. |
| 121 | + |
| 122 | +fig = plt.figure() |
| 123 | +subfigs = fig.subfigure_mosaic(mosaic) |
| 124 | +identify_subfigs(subfigs) |
| 125 | + |
| 126 | +# %% |
| 127 | +# SubFigures spanning multiple rows/columns |
| 128 | +# ========================================= |
| 129 | +# |
| 130 | +# Something we can do with `.Figure.subfigure_mosaic`, that we cannot |
| 131 | +# do with `.Figure.subfigures`, is to specify that a SubFigure should span |
| 132 | +# several rows or columns. |
| 133 | + |
| 134 | + |
| 135 | +# %% |
| 136 | +# If we want to re-arrange our four SubFigures to have ``"C"`` be a horizontal |
| 137 | +# span on the bottom and ``"D"`` be a vertical span on the right we would do: |
| 138 | + |
| 139 | +subfigs = plt.figure().subfigure_mosaic( |
| 140 | + """ |
| 141 | + ABD |
| 142 | + CCD |
| 143 | + """ |
| 144 | +) |
| 145 | + |
| 146 | +# setting edges for clarity |
| 147 | +for sf in subfigs.values(): |
| 148 | + sf.set_edgecolor('black') |
| 149 | + sf.set_linewidth(2.1) |
| 150 | + |
| 151 | +identify_subfigs(subfigs) |
| 152 | + |
| 153 | +# %% |
| 154 | +# If we do not want to fill in all the spaces in the Figure with SubFigures, |
| 155 | +# we can specify some spaces in the grid to be blank, like so: |
| 156 | + |
| 157 | +subfigs = plt.figure().subfigure_mosaic( |
| 158 | + """ |
| 159 | + A.C |
| 160 | + BBB |
| 161 | + .D. |
| 162 | + """ |
| 163 | +) |
| 164 | + |
| 165 | +# setting edges for clarity |
| 166 | +for sf in subfigs.values(): |
| 167 | + sf.set_edgecolor('black') |
| 168 | + sf.set_linewidth(2.1) |
| 169 | + |
| 170 | +identify_subfigs(subfigs) |
| 171 | + |
| 172 | +# %% |
| 173 | +# If we prefer to use another character (rather than a period ``"."``) |
| 174 | +# to mark the empty space, we can use *empty_sentinel* to specify the |
| 175 | +# character to use. |
| 176 | + |
| 177 | +subfigs = plt.figure().subfigure_mosaic( |
| 178 | + """ |
| 179 | + aX |
| 180 | + Xb |
| 181 | + """, |
| 182 | + empty_sentinel="X", |
| 183 | +) |
| 184 | + |
| 185 | +# setting edges for clarity |
| 186 | +for sf in subfigs.values(): |
| 187 | + sf.set_edgecolor('black') |
| 188 | + sf.set_linewidth(2.1) |
| 189 | +identify_subfigs(subfigs) |
| 190 | + |
| 191 | +# %% |
| 192 | +# |
| 193 | +# Internally there is no meaning attached to the letters we use, any |
| 194 | +# Unicode code point is valid! |
| 195 | + |
| 196 | +subfigs = plt.figure().subfigure_mosaic( |
| 197 | + """αя |
| 198 | + ℝ☢""" |
| 199 | +) |
| 200 | + |
| 201 | +# setting edges for clarity |
| 202 | +for sf in subfigs.values(): |
| 203 | + sf.set_edgecolor('black') |
| 204 | + sf.set_linewidth(2.1) |
| 205 | +identify_subfigs(subfigs) |
| 206 | + |
| 207 | +# %% |
| 208 | +# It is not recommended to use white space as either a label or an |
| 209 | +# empty sentinel with the string shorthand because it may be stripped |
| 210 | +# while processing the input. |
| 211 | +# |
| 212 | +# Controlling mosaic creation |
| 213 | +# =========================== |
| 214 | +# |
| 215 | +# This feature is built on top of `.gridspec` and you can pass the |
| 216 | +# keyword arguments through to the underlying `.gridspec.GridSpec` |
| 217 | +# (the same as `.Figure.subfigures`). |
| 218 | +# |
| 219 | +# In this case we want to use the input to specify the arrangement, |
| 220 | +# but set the relative widths of the rows / columns. For convenience, |
| 221 | +# `.gridspec.GridSpec`'s *height_ratios* and *width_ratios* are exposed in the |
| 222 | +# `.Figure.subfigure_mosaic` calling sequence. |
| 223 | + |
| 224 | +subfigs = plt.figure().subfigure_mosaic( |
| 225 | + """ |
| 226 | + .a. |
| 227 | + bAc |
| 228 | + .d. |
| 229 | + """, |
| 230 | + # set the height ratios between the rows |
| 231 | + height_ratios=[1, 3.5, 1], |
| 232 | + # set the width ratios between the columns |
| 233 | + width_ratios=[1, 3.5, 1], |
| 234 | +) |
| 235 | + |
| 236 | +# setting edges for clarity |
| 237 | +for sf in subfigs.values(): |
| 238 | + sf.set_edgecolor('black') |
| 239 | + sf.set_linewidth(2.1) |
| 240 | +identify_subfigs(subfigs) |
| 241 | + |
| 242 | +# %% |
| 243 | +# You can also use the `.Figure.subfigures` functionality to |
| 244 | +# position the overall mosaic to put multiple versions of the same |
| 245 | +# mosaic in a figure. |
| 246 | + |
| 247 | +mosaic = """AA |
| 248 | + BC""" |
| 249 | +fig = plt.figure() |
| 250 | + |
| 251 | +left, right = fig.subfigures(nrows=1, ncols=2) |
| 252 | + |
| 253 | +subfigs = left.subfigure_mosaic(mosaic) |
| 254 | +for subfig in subfigs.values(): |
| 255 | + subfig.set_edgecolor('black') |
| 256 | + subfig.set_linewidth(2.1) |
| 257 | +identify_subfigs(subfigs) |
| 258 | + |
| 259 | +subfigs = right.subfigure_mosaic(mosaic) |
| 260 | +for subfig in subfigs.values(): |
| 261 | + subfig.set_edgecolor('black') |
| 262 | + subfig.set_linewidth(2.1) |
| 263 | +identify_subfigs(subfigs) |
| 264 | + |
| 265 | +# %% |
| 266 | +# .. _controlling-creation: |
| 267 | +# Controlling subfigure creation |
| 268 | +# ============================== |
| 269 | +# |
| 270 | +# We can also pass through arguments used to create the subfigures |
| 271 | +# which will apply to all of the SubFigures created. So instead of iterating like so: |
| 272 | + |
| 273 | +for sf in subfigs.values(): |
| 274 | + sf.set_edgecolor('black') |
| 275 | + sf.set_linewidth(2.1) |
| 276 | + |
| 277 | +# %% |
| 278 | +# we would write: |
| 279 | + |
| 280 | +subfigs = plt.figure().subfigure_mosaic( |
| 281 | + "A.B;A.C", subfigure_kw={"edgecolor": "black", "linewidth": 2.1} |
| 282 | +) |
| 283 | +identify_subfigs(subfigs) |
| 284 | + |
| 285 | +# %% |
| 286 | +# Per-SubFigure keyword arguments |
| 287 | +# ---------------------------------- |
| 288 | +# |
| 289 | +# If you need to control the parameters passed to each subfigure individually use |
| 290 | +# *per_subfigure_kw* to pass a mapping between the SubFigure identifiers (or |
| 291 | +# tuples of SubFigure identifiers) to dictionaries of keywords to be passed. |
| 292 | +# |
| 293 | + |
| 294 | +fig, subfigs = plt.subfigure_mosaic( |
| 295 | + "AB;CD", |
| 296 | + per_subfigure_kw={ |
| 297 | + "A": {"facecolor": "green"}, |
| 298 | + ("C", "D"): {"edgecolor": "black", "linewidth": 1.1, } |
| 299 | + }, |
| 300 | +) |
| 301 | +identify_subfigs(subfigs) |
| 302 | + |
| 303 | +# %% |
| 304 | +# If the layout is specified with the string short-hand, then we know the |
| 305 | +# SubFigure labels will be one character and can unambiguously interpret longer |
| 306 | +# strings in *per_subfigure_kw* to specify a set of SubFigures to apply the |
| 307 | +# keywords to: |
| 308 | + |
| 309 | +fig, subfigs = plt.subfigure_mosaic( |
| 310 | + "AB;CD", |
| 311 | + per_subfigure_kw={ |
| 312 | + "AD": {"facecolor": ".3"}, |
| 313 | + "BC": {"edgecolor": "black", "linewidth": 2.1, } |
| 314 | + }, |
| 315 | +) |
| 316 | +identify_subfigs(subfigs) |
| 317 | + |
| 318 | +# %% |
| 319 | +# If *subfigure_kw* and *per_subfigure_kw* are used together, then they are |
| 320 | +# merged with *per_subfigure_kw* taking priority: |
| 321 | + |
| 322 | +subfigs = plt.figure().subfigure_mosaic( |
| 323 | + "AB;CD", |
| 324 | + subfigure_kw={"facecolor": "xkcd:tangerine", "linewidth": 2}, |
| 325 | + per_subfigure_kw={ |
| 326 | + "B": {"facecolor": "xkcd:water blue"}, |
| 327 | + "D": {"edgecolor": "yellow", "linewidth": 2.2, "facecolor": "g"}, |
| 328 | + } |
| 329 | +) |
| 330 | +identify_subfigs(subfigs) |
| 331 | + |
| 332 | +# %% |
| 333 | +# Nested list input |
| 334 | +# ================= |
| 335 | +# |
| 336 | +# Everything we can do with the string shorthand we can also do when |
| 337 | +# passing in a list (internally we convert the string shorthand to a nested |
| 338 | +# list), for example using spans and blanks: |
| 339 | + |
| 340 | +subfigs = plt.figure().subfigure_mosaic( |
| 341 | + [ |
| 342 | + ["main", "zoom"], |
| 343 | + ["main", "BLANK"], |
| 344 | + ], |
| 345 | + empty_sentinel="BLANK", |
| 346 | + width_ratios=[2, 1], |
| 347 | + subfigure_kw={"facecolor": "xkcd:sea green", "linewidth": 2}, |
| 348 | +) |
| 349 | +identify_subfigs(subfigs) |
| 350 | + |
| 351 | +# %% |
| 352 | +# In addition, using the list input we can specify nested mosaics. Any element |
| 353 | +# of the inner list can be another set of nested lists: |
| 354 | + |
| 355 | +inner = [ |
| 356 | + ["inner A"], |
| 357 | + ["inner B"], |
| 358 | +] |
| 359 | +inner_three = [ |
| 360 | + ["inner Q"], |
| 361 | + ["inner Z"], |
| 362 | +] |
| 363 | +inner_two = [ |
| 364 | + ["inner C"], |
| 365 | + [inner_three], |
| 366 | +] |
| 367 | + |
| 368 | +layout = [["A", [[inner_two, "C"], |
| 369 | + ["D", "E"]] |
| 370 | + ], |
| 371 | + ["F", "G"], |
| 372 | + [".", [["H", [["I"], |
| 373 | + ["."] |
| 374 | + ] |
| 375 | + ] |
| 376 | + ] |
| 377 | + ] |
| 378 | + ] |
| 379 | +fig, subfigs = plt.subfigure_mosaic(layout, subfigure_kw={'edgecolor': 'black', |
| 380 | + 'linewidth': 1.5}, |
| 381 | + per_subfigure_kw={"E": {'edgecolor': 'xkcd:red'}, |
| 382 | + "G": {'facecolor': 'yellow'}, |
| 383 | + "H": {'edgecolor': 'blue', |
| 384 | + 'facecolor': 'xkcd:azure'}} |
| 385 | + ) |
| 386 | + |
| 387 | +identify_subfigs(subfigs, fontsize=12) |
| 388 | + |
| 389 | +# %% |
| 390 | +# We can also pass in a 2D NumPy array to do things like: |
| 391 | +mosaic = np.zeros((4, 4), dtype=int) |
| 392 | +for j in range(4): |
| 393 | + mosaic[j, j] = j + 1 |
| 394 | +subfigs = plt.figure().subfigure_mosaic( |
| 395 | + mosaic, |
| 396 | + subfigure_kw={'edgecolor': 'black', 'linewidth': 1.5}, |
| 397 | + empty_sentinel=0, |
| 398 | +) |
| 399 | +identify_subfigs(subfigs, fontsize=12) |
0 commit comments