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

Experimental subplot implementation that mimics matplotlib's style #427

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ function-naming-style=snake_case
good-names=i,
j,
k,
ax,
ex,
Run,
_,
Expand Down
1 change: 1 addition & 0 deletions doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All plotting is handled through the :class:`pygmt.Figure` class and its methods.
:toctree: generated

Figure
subplots

Plotting data and laying out the map:

Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
projections/index.rst
tutorials/coastlines.rst
tutorials/plot.rst
tutorials/subplots.rst
tutorials/text.rst

.. toctree::
Expand Down
224 changes: 224 additions & 0 deletions examples/tutorials/subplots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""
Subplots
========

When you're preparing a figure for a paper, there will often be times when
you'll need to put many individual plots into one large figure, and label them
'abcd'. These individual plots are called subplots.

There are two main ways to create subplots in GMT:

- Use :meth:`pygmt.Figure.shift_origin` to manually move each individual plot
to the right position.
- Use :meth:`pygmt.subplots` to define the layout of the subplots.

The first method is easier to use and should handle simple cases involving a
couple of subplots. For more advanced subplot layouts however, we recommend the
use of :meth:`pygmt.subplots` which offers finer grained control, and this is
what the tutorial below will cover.
"""

###############################################################################
# Let's start by importing the PyGMT library

import pygmt

###############################################################################
# Define subplot layout
# ---------------------
#
# The :meth:`pygmt.subplots` command is used to setup the layout, size, and
# other attributes of the figure. It divides the whole canvas into regular grid
# areas with n rows and m columns. Each grid area can contain an individual
# subplot. For example:

fig, axs = pygmt.subplots(nrows=2, ncols=3, figsize=("15c", "6c"), frame="lrtb")

###############################################################################
# will define our figure to have a 2 row and 3 column grid layout.
# ``figsize=("15c", "6c")`` defines the overall size of the figure to be 15cm
# wide by 6cm high. Using ``frame="lrtb"`` allows us to customize the map frame
# for all subplots instead of setting them individually. The figure layout will
# look like the following:

for index in axs.flatten():
i = index // axs.shape[1] # row
j = index % axs.shape[1] # column
fig.sca(ax=axs[i, j]) # sets the current Axes
fig.text(
position="MC", text=f"index: {index}, row: {i}, col: {j}", region=[0, 1, 0, 1]
)
fig.end_subplot()
fig.show()

###############################################################################
# The ``fig.sca`` command activates a specified subplot, and all subsequent
# plotting commands will take place in that subplot. This is similar to
# matplotlib's ``plt.sca`` method. In order to specify a subplot, you will need
# to provide the identifier for that subplot via the ``ax`` argument. This can
# be found in the ``axs`` variable referenced by the ``row`` and ``col``
# number.

###############################################################################
# .. note::
#
# The row and column numbering starts from 0. So for a subplot layout with
# N rows and M columns, row numbers will go from 0 to N-1, and column
# numbers will go from 0 to M-1.

###############################################################################
# For example, to activate the subplot on the top right corner (index: 2) at
# ``row=0`` and ``col=2``, so that all subsequent plotting commands happen
# there, you can use the following command:

###############################################################################
# .. code-block:: default
#
# fig.sca(ax=axs[0, 2])
Comment on lines +54 to +77
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self to remove mention of fig.sca, if we're going to hide this away as per #427 (comment).


###############################################################################
# Finally, remember to use ``fig.end_subplot()`` to exit the subplot mode.

###############################################################################
# .. code-block:: default
#
# fig.end_subplot()

###############################################################################
# Making your first subplot
# -------------------------
# Next, let's use what we learned above to make a 2 row by 2 column subplot
# figure. We'll also pick up on some new parameters to configure our subplot.

fig, axs = pygmt.subplots(
nrows=2,
ncols=2,
figsize=("15c", "6c"),
autolabel=True,
margins=["0.1c", "0.2c"],
title="My Subplot Heading",
)
fig.basemap(region=[0, 10, 0, 10], projection="X?", frame=["af", "WSne"], ax=axs[0, 0])
fig.basemap(region=[0, 20, 0, 10], projection="X?", frame=["af", "WSne"], ax=axs[0, 1])
fig.basemap(region=[0, 10, 0, 20], projection="X?", frame=["af", "WSne"], ax=axs[1, 0])
fig.basemap(region=[0, 20, 0, 20], projection="X?", frame=["af", "WSne"], ax=axs[1, 1])
fig.end_subplot()
fig.show()

###############################################################################
# In this example, we define a 2-row, 2-column (2x2) subplot layout using
# :meth:`pygmt.subplots`. The overall figure dimensions is set to be 15cm wide
# and 6cm high (``figsize=["15c", "6c"]``). In addition, we used some optional
# parameters to fine tune some details of the figure creation:
#
# - ``autolabel=True``: Each subplot is automatically labelled abcd
# - ``margins=["0.1c", "0.2c"]``: adjusts the space between adjacent subplots.
# In this case, it is set as 0.1 cm in the X direction and 0.2 cm in the Y
# direction.
# - ``title="My Subplot Heading"``: adds a title on top of the whole figure.
#
# Notice that each subplot was set to use a linear projection ``"X?"``.
# Usually, we need to specify the width and height of the map frame, but it is
# also possible to use a question mark ``"?"`` to let GMT decide automatically
# on what is the most appropriate width/height for the each subplot's map
# frame.

###############################################################################
# .. tip::
#
# In the above example, we used the following commands to activate the
# four subplots explicitly one after another::
#
# fig.basemap(..., ax=axs[0, 0])
# fig.basemap(..., ax=axs[0, 1])
# fig.basemap(..., ax=axs[1, 0])
# fig.basemap(..., ax=axs[1, 1])
#
# In fact, we can just use ``fig.basemap(..., ax=True)`` without specifying
# any subplot index number, and GMT will automatically activate the next
# subplot.

###############################################################################
# Shared X and Y axis labels
# --------------------------
# In the example above with the four subplots, the two subplots for each row
# have the same Y-axis range, and the two subplots for each column have the
# same X-axis range. You can use the **layout** option to set a common X and/or
# Y axis between subplots.

fig, axs = pygmt.subplots(
nrows=2,
ncols=2,
figsize=("15c", "6c"),
autolabel=True,
margins=["0.3c", "0.2c"],
title="My Subplot Heading",
layout=["Rl", "Cb"],
frame="WSrt",
)
fig.basemap(region=[0, 10, 0, 10], projection="X?", ax=True)
fig.basemap(region=[0, 20, 0, 10], projection="X?", ax=True)
fig.basemap(region=[0, 10, 0, 20], projection="X?", ax=True)
fig.basemap(region=[0, 20, 0, 20], projection="X?", ax=True)
fig.end_subplot()
fig.show()

###############################################################################
# **Rl** indicates that subplots within a **R**\ ow will share the y-axis, and
# only the **l**\ eft axis is displayed. **Cb** indicates that subplots in
# a column will share the x-axis, and only the **b**\ ottom axis is displayed.
#
# Of course, instead of using the **layout** option, you can also set a
# different **frame** for each subplot to control the axis properties
# individually for each subplot.

###############################################################################
# Advanced subplot layouts
# ------------------------
#
# Nested subplot are currently not supported. If you want to create more
# complex subplot layouts, some manual adjustments are needed.
#
# The following example draws three subplots in a 2-row, 2-column layout, with
# the first subplot occupying the first row.

fig, axs = pygmt.subplots(nrows=2, ncols=2, figsize=("15c", "6c"), autolabel=True)
fig.basemap(
region=[0, 10, 0, 10], projection="X15c/3c", frame=["af", "WSne"], ax=axs[0, 0]
)
fig.text(text="TEXT", x=5, y=5, projection="X15c/3c")
fig.basemap(region=[0, 5, 0, 5], projection="X?", frame=["af", "WSne"], ax=axs[1, 0])
fig.basemap(region=[0, 5, 0, 5], projection="X?", frame=["af", "WSne"], ax=axs[1, 1])
fig.end_subplot()
fig.show()
Comment on lines +185 to +193
Copy link
Member Author

@weiji14 weiji14 Oct 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to fix this example, since it's currently labelling the subplots as 'a)', 'c)', 'd)' (and the 'a)' position is wrong). The obvious way would be to use fig.sca (i.e. subplot set), but if we want to avoid it as per #427 (comment), then it's going to require more thought.

image


###############################################################################
#
# When drawing the three basemaps, the last two basemaps use
# ``projection="X?"``, so GMT will automatically determine the size of the
# subplot according to the size of the subplot area. In order for the first
# subplot to fill up the entire top row space, we use manually adjusted the
# subplot width to 15cm using ``projection="X15c/3c"``.

###############################################################################
# .. note::
#
# There are bugs that have not been fixed in the above example.
#
# In subplot mode, the size of each subgraph is controlled by the
# ``figsize`` option of :meth:`pygmt.subplots`. Users can override this and
# use``projection`` to specify the size of an individual subplot, but this
# size will not be remembered. If the next command does not specify
# ``projection``, the default size of the subplot mode will be used, and
# the resulting plot will be inccorect.
#
# The current workaround is to use the same ``projection`` option in all
# commands for the subplot. For example, we forced subplot (a) to have a
# different size using ``projection="15c/3c``. The next command within the
# subplot (e.g. ``text``) must also use ``projection="x15c/3c"``, otherwise
# the placement will be wrong.

###############################################################################
Comment on lines +203 to +221
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can take this out first, if we want to wait for upstream GMT to support nested subplots at GenericMappingTools/gmt#1280.

# Since we skipped the second subplot, the auto label function will name the
# three subplots as a, c and d, which is not what we want, so we have to use
# ``fig.sca(A=""(a)"`` to manually set the subplot label.
1 change: 1 addition & 0 deletions pygmt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .sampling import grdtrack
from .mathops import makecpt
from .modules import GMTDataArrayAccessor, config, info, grdinfo, which
from .subplot import SubPlot, subplots
from .gridops import grdcut
from .x2sys import x2sys_init, x2sys_cross
from . import datasets
Expand Down
17 changes: 15 additions & 2 deletions pygmt/base_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def _preprocess(self, **kwargs): # pylint: disable=no-self-use
U="timestamp",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -153,6 +154,7 @@ def coast(self, **kwargs):
W="scale",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -238,9 +240,10 @@ def colorbar(self, **kwargs):
S="resample",
U="timestamp",
W="pen",
l="label",
X="xshift",
Y="yshift",
c="ax",
l="label",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -334,6 +337,7 @@ def grdcontour(self, grid, **kwargs):
V="verbose",
X="xshift",
Y="yshift",
c="ax",
n="interpolation",
p="perspective",
t="transparency",
Expand Down Expand Up @@ -490,6 +494,7 @@ def grdimage(self, grid, **kwargs):
I="shading",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -609,6 +614,7 @@ def grdview(self, grid, **kwargs):
U="timestamp",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -735,6 +741,7 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs):
C="levels",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -829,6 +836,7 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs):
U="timestamp",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -870,7 +878,7 @@ def basemap(self, **kwargs):

"""
kwargs = self._preprocess(**kwargs)
if not ("B" in kwargs or "L" in kwargs or "T" in kwargs):
if not ("B" in kwargs or "L" in kwargs or "T" in kwargs or "c" in kwargs):
raise GMTInvalidInput("At least one of B, L, or T must be specified.")
with Session() as lib:
lib.call_module("basemap", build_arg_string(kwargs))
Expand All @@ -884,6 +892,7 @@ def basemap(self, **kwargs):
F="box",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -932,6 +941,7 @@ def logo(self, **kwargs):
M="monochrome",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -985,6 +995,7 @@ def image(self, imagefile, **kwargs):
F="box",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -1055,6 +1066,7 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
W="pen",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down Expand Up @@ -1222,6 +1234,7 @@ def text(
C="offset",
X="xshift",
Y="yshift",
c="ax",
p="perspective",
t="transparency",
)
Expand Down
Loading