Skip to content

Commit 2e657c0

Browse files
committed
Wrap subplot - version 3
Wrapping the `subplot` function, in a `with` statement! Original GMT `subplot` function can be found at https://docs.generic-mapping-tools.org/6.1/subplot.html. This is my 3rd attempt at implementing `subplot` in PyGMT, with commits heavily re-adapted/cherry-picked from #412 and #427.
1 parent ef628bc commit 2e657c0

File tree

7 files changed

+499
-4
lines changed

7 files changed

+499
-4
lines changed

.pylintrc

+1
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ function-naming-style=snake_case
441441
good-names=i,
442442
j,
443443
k,
444+
ax,
444445
ex,
445446
Run,
446447
_,

doc/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
tutorials/coastlines.rst
3434
tutorials/regions.rst
3535
tutorials/plot.rst
36+
tutorials/subplots.rst
3637
tutorials/lines.rst
3738
tutorials/text.rst
3839
tutorials/contour-map.rst

examples/tutorials/subplots.py

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"""
2+
Subplots
3+
========
4+
5+
When you're preparing a figure for a paper, there will often be times when
6+
you'll need to put many individual plots into one large figure, and label them
7+
'abcd'. These individual plots are called subplots.
8+
9+
There are two main ways to create subplots in GMT:
10+
11+
- Use :meth:`pygmt.Figure.shift_origin` to manually move each individual plot
12+
to the right position.
13+
- Use :meth:`pygmt.Figure.subplot` to define the layout of the subplots.
14+
15+
The first method is easier to use and should handle simple cases involving a
16+
couple of subplots. For more advanced subplot layouts however, we recommend the
17+
use of :meth:`pygmt.Figure.subplot` which offers finer grained control, and
18+
this is what the tutorial below will cover.
19+
"""
20+
21+
###############################################################################
22+
# Let's start by importing the PyGMT library and initializing a Figure
23+
24+
import pygmt
25+
26+
fig = pygmt.Figure()
27+
28+
###############################################################################
29+
# Define subplot layout
30+
# ---------------------
31+
#
32+
# The :meth:`pygmt.Figure.subplot` command is used to setup the layout, size,
33+
# and other attributes of the figure. It divides the whole canvas into regular
34+
# grid areas with n rows and m columns. Each grid area can contain an
35+
# individual subplot. For example:
36+
37+
###############################################################################
38+
# .. code-block:: default
39+
#
40+
# with fig.subplot(
41+
# nrows=2, ncols=3, figsize=("15c", "6c"), frame="lrtb"
42+
# ) as axs:
43+
# ...
44+
45+
###############################################################################
46+
# will define our figure to have a 2 row and 3 column grid layout.
47+
# ``figsize=("15c", "6c")`` defines the overall size of the figure to be 15cm
48+
# wide by 6cm high. Using ``frame="lrtb"`` allows us to customize the map frame
49+
# for all subplots instead of setting them individually. The figure layout will
50+
# look like the following:
51+
52+
with fig.subplot(nrows=2, ncols=3, figsize=("15c", "6c"), frame="lrtb") as axs:
53+
for index in axs.flatten():
54+
i = index // axs.shape[1] # row
55+
j = index % axs.shape[1] # column
56+
fig.sca(ax=axs[i, j]) # sets the current Axes
57+
fig.text(
58+
position="MC",
59+
text=f"index: {index}, row: {i}, col: {j}",
60+
region=[0, 1, 0, 1],
61+
)
62+
fig.show()
63+
64+
###############################################################################
65+
# The ``fig.sca`` command activates a specified subplot, and all subsequent
66+
# plotting commands will take place in that subplot. This is similar to
67+
# matplotlib's ``plt.sca`` method. In order to specify a subplot, you will need
68+
# to provide the identifier for that subplot via the ``ax`` argument. This can
69+
# be found in the ``axs`` variable referenced by the ``row`` and ``col``
70+
# number.
71+
72+
###############################################################################
73+
# .. note::
74+
#
75+
# The row and column numbering starts from 0. So for a subplot layout with
76+
# N rows and M columns, row numbers will go from 0 to N-1, and column
77+
# numbers will go from 0 to M-1.
78+
79+
###############################################################################
80+
# For example, to activate the subplot on the top right corner (index: 2) at
81+
# ``row=0`` and ``col=2``, so that all subsequent plotting commands happen
82+
# there, you can use the following command:
83+
84+
###############################################################################
85+
# .. code-block:: default
86+
#
87+
# fig.sca(ax=axs[0, 2])
88+
89+
###############################################################################
90+
# Making your first subplot
91+
# -------------------------
92+
# Next, let's use what we learned above to make a 2 row by 2 column subplot
93+
# figure. We'll also pick up on some new parameters to configure our subplot.
94+
95+
fig = pygmt.Figure()
96+
with fig.subplot(
97+
nrows=2,
98+
ncols=2,
99+
figsize=("15c", "6c"),
100+
autolabel=True,
101+
margins=["0.1c", "0.2c"],
102+
title='"My Subplot Heading"',
103+
) as axs:
104+
fig.basemap(
105+
region=[0, 10, 0, 10], projection="X?", frame=["af", "WSne"], ax=axs[0, 0]
106+
)
107+
fig.basemap(
108+
region=[0, 20, 0, 10], projection="X?", frame=["af", "WSne"], ax=axs[0, 1]
109+
)
110+
fig.basemap(
111+
region=[0, 10, 0, 20], projection="X?", frame=["af", "WSne"], ax=axs[1, 0]
112+
)
113+
fig.basemap(
114+
region=[0, 20, 0, 20], projection="X?", frame=["af", "WSne"], ax=axs[1, 1]
115+
)
116+
fig.show()
117+
118+
###############################################################################
119+
# In this example, we define a 2-row, 2-column (2x2) subplot layout using
120+
# :meth:`pygmt.Figure.subplot`. The overall figure dimensions is set to be 15cm
121+
# wide and 6cm high (``figsize=["15c", "6c"]``). In addition, we used some
122+
# optional parameters to fine tune some details of the figure creation:
123+
#
124+
# - ``autolabel=True``: Each subplot is automatically labelled abcd
125+
# - ``margins=["0.1c", "0.2c"]``: adjusts the space between adjacent subplots.
126+
# In this case, it is set as 0.1 cm in the X direction and 0.2 cm in the Y
127+
# direction.
128+
# - ``title="My Subplot Heading"``: adds a title on top of the whole figure.
129+
#
130+
# Notice that each subplot was set to use a linear projection ``"X?"``.
131+
# Usually, we need to specify the width and height of the map frame, but it is
132+
# also possible to use a question mark ``"?"`` to let GMT decide automatically
133+
# on what is the most appropriate width/height for the each subplot's map
134+
# frame.
135+
136+
###############################################################################
137+
# .. tip::
138+
#
139+
# In the above example, we used the following commands to activate the
140+
# four subplots explicitly one after another::
141+
#
142+
# fig.basemap(..., ax=axs[0, 0])
143+
# fig.basemap(..., ax=axs[0, 1])
144+
# fig.basemap(..., ax=axs[1, 0])
145+
# fig.basemap(..., ax=axs[1, 1])
146+
#
147+
# In fact, we can just use ``fig.basemap(..., ax=True)`` without specifying
148+
# any subplot index number, and GMT will automatically activate the next
149+
# subplot.
150+
151+
###############################################################################
152+
# Shared X and Y axis labels
153+
# --------------------------
154+
# In the example above with the four subplots, the two subplots for each row
155+
# have the same Y-axis range, and the two subplots for each column have the
156+
# same X-axis range. You can use the **layout** option to set a common X and/or
157+
# Y axis between subplots.
158+
159+
fig = pygmt.Figure()
160+
with fig.subplot(
161+
nrows=2,
162+
ncols=2,
163+
figsize=("15c", "6c"),
164+
autolabel=True,
165+
margins=["0.3c", "0.2c"],
166+
title='"My Subplot Heading"',
167+
layout=["Rl", "Cb"],
168+
frame="WSrt",
169+
) as axs:
170+
fig.basemap(region=[0, 10, 0, 10], projection="X?", ax=True)
171+
fig.basemap(region=[0, 20, 0, 10], projection="X?", ax=True)
172+
fig.basemap(region=[0, 10, 0, 20], projection="X?", ax=True)
173+
fig.basemap(region=[0, 20, 0, 20], projection="X?", ax=True)
174+
fig.show()
175+
176+
###############################################################################
177+
# **Rl** indicates that subplots within a **R**\ ow will share the y-axis, and
178+
# only the **l**\ eft axis is displayed. **Cb** indicates that subplots in
179+
# a column will share the x-axis, and only the **b**\ ottom axis is displayed.
180+
#
181+
# Of course, instead of using the **layout** option, you can also set a
182+
# different **frame** for each subplot to control the axis properties
183+
# individually for each subplot.
184+
185+
###############################################################################
186+
# Advanced subplot layouts
187+
# ------------------------
188+
#
189+
# Nested subplot are currently not supported. If you want to create more
190+
# complex subplot layouts, some manual adjustments are needed.
191+
#
192+
# The following example draws three subplots in a 2-row, 2-column layout, with
193+
# the first subplot occupying the first row.
194+
195+
fig = pygmt.Figure()
196+
with fig.subplot(nrows=2, ncols=2, figsize=("15c", "6c"), autolabel=True):
197+
fig.basemap(
198+
region=[0, 10, 0, 10], projection="X15c/3c", frame=["af", "WSne"], ax=axs[0, 0]
199+
)
200+
fig.text(text="TEXT", x=5, y=5, projection="X15c/3c")
201+
fig.basemap(
202+
region=[0, 5, 0, 5], projection="X?", frame=["af", "WSne"], ax=axs[1, 0]
203+
)
204+
fig.basemap(
205+
region=[0, 5, 0, 5], projection="X?", frame=["af", "WSne"], ax=axs[1, 1]
206+
)
207+
fig.show()
208+
209+
###############################################################################
210+
#
211+
# When drawing the three basemaps, the last two basemaps use
212+
# ``projection="X?"``, so GMT will automatically determine the size of the
213+
# subplot according to the size of the subplot area. In order for the first
214+
# subplot to fill up the entire top row space, we use manually adjusted the
215+
# subplot width to 15cm using ``projection="X15c/3c"``.
216+
217+
###############################################################################
218+
# .. note::
219+
#
220+
# There are bugs that have not been fixed in the above example.
221+
#
222+
# In subplot mode, the size of each subgraph is controlled by the
223+
# ``figsize`` option of :meth:`pygmt.Figure.subplot`. Users can override
224+
# this and use``projection`` to specify the size of an individual subplot,
225+
# but this size will not be remembered. If the next command does not
226+
# specify``projection``, the default size of the subplot mode will be used,
227+
# and the resulting plot will be inccorect.
228+
#
229+
# The current workaround is to use the same ``projection`` option in all
230+
# commands for the subplot. For example, we forced subplot (a) to have a
231+
# different size using ``projection="15c/3c``. The next command within the
232+
# subplot (e.g. ``text``) must also use ``projection="x15c/3c"``, otherwise
233+
# the placement will be wrong.
234+
235+
###############################################################################
236+
# Since we skipped the second subplot, the auto label function will name the
237+
# three subplots as a, c and d, which is not what we want, so we have to use
238+
# ``fig.sca(A=""(a)"`` to manually set the subplot label.

pygmt/base_plotting.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,7 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs):
12221222
V="verbose",
12231223
X="xshift",
12241224
Y="yshift",
1225+
c="ax",
12251226
p="perspective",
12261227
t="transparency",
12271228
)
@@ -1265,9 +1266,9 @@ def basemap(self, **kwargs):
12651266
{t}
12661267
"""
12671268
kwargs = self._preprocess(**kwargs)
1268-
if not args_in_kwargs(args=["B", "L", "Td", "Tm"], kwargs=kwargs):
1269+
if not args_in_kwargs(args=["B", "L", "Td", "Tm", "c"], kwargs=kwargs):
12691270
raise GMTInvalidInput(
1270-
"At least one of frame, map_scale, compass, or rose must be specified."
1271+
"At least one of frame, map_scale, compass, rose, or ax must be specified."
12711272
)
12721273
with Session() as lib:
12731274
lib.call_module("basemap", build_arg_string(kwargs))
@@ -1642,5 +1643,5 @@ def text(
16421643
arg_str = " ".join([fname, build_arg_string(kwargs)])
16431644
lib.call_module("text", arg_str)
16441645

1645-
# GMT Supplementary modules
1646-
from pygmt.src import meca # pylint: disable=import-outside-toplevel
1646+
# GMT plotting modules
1647+
from pygmt.src import meca, subplot, sca # pylint: disable=import-outside-toplevel

pygmt/src/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
"""
44
# pylint: disable=import-outside-toplevel
55
from pygmt.src.meca import meca
6+
from pygmt.src.subplot import sca, subplot

0 commit comments

Comments
 (0)