Skip to content

Commit cd8ecc7

Browse files
committed
Experimental subplot implementation that mimics matplotlib's style
Experimenting with a more matplotlib-like syntax for managing subplots, namely the `fig, axs = plt.subplots()` style of defining subplots! A high-level `subplots` function is defined in pygmtplots.py, that wraps around the new class SubPlot (inherited from the main Figure class to include subplot functionality). Re-aliased F to figsize (previously called dimensions). Also updated tests and tutorials to reference this new syntax.
1 parent 072bc16 commit cd8ecc7

File tree

7 files changed

+158
-110
lines changed

7 files changed

+158
-110
lines changed

doc/api/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All plotting is handled through the :class:`pygmt.Figure` class and its methods.
1616
:toctree: generated
1717

1818
Figure
19+
subplots
1920

2021
Plotting data and laying out the map:
2122

@@ -34,7 +35,6 @@ Plotting data and laying out the map:
3435
Figure.logo
3536
Figure.image
3637
Figure.shift_origin
37-
Figure.subplot
3838
Figure.text
3939
Figure.meca
4040

examples/tutorials/subplots.py

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,58 @@
66
you'll need to put many individual plots into one large figure, and label them
77
'abcd'. These individual plots are called subplots.
88
9-
There are two main ways to handle subplots in GMT:
9+
There are two main ways to create subplots in GMT:
1010
1111
- Use :meth:`pygmt.Figure.shift_origin` to manually move each individual plot
1212
to the right position.
13-
- Use :meth:`pygmt.Figure.subplot` to define the layout of the subplots.
13+
- Use :meth:`pygmt.subplots` to define the layout of the subplots.
1414
1515
The first method is easier to use and should handle simple cases involving a
1616
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.
17+
use of :meth:`pygmt.subplots` which offers finer grained control, and this is
18+
what the tutorial below will cover.
1919
"""
2020

2121
###############################################################################
22-
# Let's start by importing the PyGMT library and initiating a figure.
22+
# Let's start by importing the PyGMT library
2323

2424
import pygmt
2525

26-
fig = pygmt.Figure()
27-
2826
###############################################################################
2927
# Define subplot layout
3028
# ---------------------
3129
#
32-
# The ``fig.subplot(directive="begin")`` command is used to setup the layout,
33-
# size, and other attributes of the figure. It divides the whole canvas into
34-
# regular grid areas with n rows and m columns. Each grid area can contain an
35-
# individual subplot. For example:
30+
# The ``pygmt.subplots`` command is used to setup the layout, size, and other
31+
# attributes of the figure. It divides the whole canvas into regular grid areas
32+
# with n rows and m columns. Each grid area can contain an individual subplot.
33+
# For example:
3634

37-
fig.subplot(directive="begin", row=2, col=3, dimensions="s5c/3c", frame="lrtb")
35+
fig, axs = pygmt.subplots(nrows=2, ncols=3, figsize=("15c", "6c"), frame="lrtb")
3836

3937
###############################################################################
4038
# will define our figure to have a 2 row and 3 column grid layout.
41-
# ``dimensions="s5c/3c"`` specifies that each 's'ubplot will have a width of
42-
# 5cm and height of 3cm. Alternatively, you can set ``dimensions="f15c/6c"`` to
43-
# define the overall size of the 'f'igure to be 15cm wide by 6cm high. Using
44-
# ``frame="lrtb"`` allows us to customize the map frame for all subplots. The
45-
# figure layout will look like the following:
46-
47-
for index in range(2 * 3):
48-
i = index // 3 # row
49-
j = index % 3 # column
50-
fig.subplot(directive="set", row=i, col=j)
39+
# ``figsize=("15c", "6c")`` defines the overall size of the figure to be 15cm
40+
# wide by 6cm high. Using ``frame="lrtb"`` allows us to customize the map frame
41+
# for all subplots instead of setting them individually. The figure layout will
42+
# look like the following:
43+
44+
for index in axs.flatten():
45+
i = index // axs.shape[1] # row
46+
j = index % axs.shape[1] # column
47+
fig.sca(ax=axs[i, j]) # sets the current Axes
5148
fig.text(
5249
x=0.5, y=0.5, text=f"index: {index}, row: {i}, col: {j}", region=[0, 1, 0, 1],
5350
)
54-
fig.subplot(directive="end")
51+
fig.end_subplot()
5552
fig.show()
5653

5754
###############################################################################
58-
# The ``fig.subplot(directive="set")`` command activates a specified subplot,
59-
# and all subsequent plotting commands will take place in that subplot. In
60-
# order to specify a subplot, you will need to know the identifier for each
61-
# subplot. This can be done by setting the ``row`` and ``col`` arguments.
55+
# The ``fig.sca`` command activates a specified subplot, and all subsequent
56+
# plotting commands will take place in that subplot. This is similar to
57+
# matplotlib's ``plt.sca`` method. In order to specify a subplot, you will need
58+
# to provide the identifier for that subplot via the ``ax`` argument. This can
59+
# be found in the ``axs`` variable referenced by the ``row`` and ``col``
60+
# number.
6261

6362
###############################################################################
6463
# .. note::
@@ -68,20 +67,19 @@
6867
# numbers will go from 0 to M-1.
6968

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

7574
###############################################################################
7675
# .. code-block:: default
7776
#
78-
# fig.subplot(directive="set", row=0, col=2)
77+
# fig.sca(ax=axs[0, 2])
7978

8079
###############################################################################
81-
# Finally, remember to use ``fig.subplot(directive="end")`` to exit the subplot
82-
# mode.
80+
# Finally, remember to use ``fig.end_subplot()`` to exit the subplot mode.
8381

8482
###############################################################################
8583
# .. code-block:: default
8684
#
87-
# fig.subplot(directive="end")
85+
# fig.end_subplot()

pygmt/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414
# Import modules to make the high-level GMT Python API
1515
from .session_management import begin as _begin, end as _end
16-
from .figure import Figure
16+
from .figure import Figure, SubPlot
1717
from .filtering import blockmedian
1818
from .gridding import surface
1919
from .sampling import grdtrack
2020
from .mathops import makecpt
2121
from .modules import GMTDataArrayAccessor, config, info, grdinfo, which
22+
from .pygmtplot import subplots
2223
from .gridops import grdcut
2324
from .x2sys import x2sys_init, x2sys_cross
2425
from . import datasets

pygmt/base_plotting.py

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -883,59 +883,6 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg
883883
arg_str = " ".join([specfile, build_arg_string(kwargs)])
884884
lib.call_module("legend", arg_str)
885885

886-
@fmt_docstring
887-
@use_alias(F="dimensions", B="frame")
888-
def subplot(self, directive: str, row: int = None, col: int = None, **kwargs):
889-
"""
890-
Manage modern mode figure subplot configuration and selection.
891-
892-
The subplot module is used to split the current figure into a
893-
rectangular layout of subplots that each may contain a single
894-
self-contained figure. A subplot setup is started with the begin
895-
directive that defines the layout of the subplots, while positioning to
896-
a particular subplot for plotting is done via the set directive. The
897-
subplot process is completed via the end directive.
898-
899-
Full option list at :gmt-docs:`subplot.html`
900-
901-
{aliases}
902-
903-
Parameters
904-
----------
905-
directive : str
906-
Either 'begin', 'set' or 'end'.
907-
row : int
908-
The number of rows if using the 'begin' directive, or the row
909-
number if using the 'set' directive. First row is 0, not 1.
910-
col : int
911-
The number of columns if using the 'begin' directive, or the column
912-
number if using the 'set' directive. First column is 0, not 1.
913-
dimensions : str
914-
``[f|s]width(s)/height(s)[+fwfracs/hfracs][+cdx/dy][+gfill][+ppen]
915-
[+wpen]``
916-
Specify the dimensions of the figure when using the 'begin'
917-
directive. There are two different ways to do this: (f) Specify
918-
overall figure dimensions or (s) specify the dimensions of a single
919-
subplot.
920-
"""
921-
if directive not in ("begin", "set", "end"):
922-
raise GMTInvalidInput(
923-
f"Unrecognized subplot directive '{directive}',\
924-
should be either 'begin', 'set', or 'end'"
925-
)
926-
927-
with Session() as lib:
928-
rowcol = "" # default is blank, e.g. when directive == "end"
929-
if row is not None and col is not None:
930-
if directive == "begin":
931-
rowcol = f"{row}x{col}"
932-
elif directive == "set":
933-
rowcol = f"{row},{col}"
934-
arg_str = " ".join(
935-
a for a in [directive, rowcol, build_arg_string(kwargs)] if a
936-
)
937-
lib.call_module(module="subplot", args=arg_str)
938-
939886
@fmt_docstring
940887
@use_alias(R="region", J="projection", B="frame")
941888
@use_alias(

pygmt/figure.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,78 @@ def _repr_html_(self):
374374
base64_png = base64.encodebytes(raw_png)
375375
html = '<img src="data:image/png;base64,{image}" width="{width}px">'
376376
return html.format(image=base64_png.decode("utf-8"), width=500)
377+
378+
379+
class SubPlot(Figure):
380+
"""
381+
Manage modern mode figure subplot configuration and selection.
382+
383+
The subplot module is used to split the current figure into a
384+
rectangular layout of subplots that each may contain a single
385+
self-contained figure. A subplot setup is started with the begin
386+
directive that defines the layout of the subplots, while positioning to
387+
a particular subplot for plotting is done via the set directive. The
388+
subplot process is completed via the end directive.
389+
390+
Full option list at :gmt-docs:`subplot.html`
391+
"""
392+
393+
def __init__(self, nrows, ncols, figsize, **kwargs):
394+
super().__init__()
395+
# Activate main Figure, and initiate subplot
396+
self._activate_figure()
397+
self.begin_subplot(row=nrows, col=ncols, figsize=figsize, **kwargs)
398+
399+
@fmt_docstring
400+
@use_alias(Ff="figsize", B="frame")
401+
@kwargs_to_strings(Ff="sequence")
402+
def begin_subplot(self, row=None, col=None, **kwargs):
403+
"""
404+
The begin directive of subplot defines the layout of the entire
405+
multi-panel illustration. Several options are available to specify
406+
the systematic layout, labeling, dimensions, and more for the
407+
subplots.
408+
409+
{aliases}
410+
"""
411+
arg_str = " ".join(["begin", f"{row}x{col}", build_arg_string(kwargs)])
412+
with Session() as lib:
413+
lib.call_module(module="subplot", args=arg_str)
414+
415+
@fmt_docstring
416+
@use_alias(F="dimensions")
417+
def sca(self, ax=None, **kwargs):
418+
"""
419+
Set the current Axes instance to *ax*.
420+
421+
Before you start plotting you must first select the active subplot.
422+
Note: If any projection (J) option is passed with ? as scale or
423+
width when plotting subplots, then the dimensions of the map are
424+
automatically determined by the subplot size and your region. For
425+
Cartesian plots: If you want the scale to apply equally to both
426+
dimensions then you must specify ``projection="x"`` [The default
427+
``projection="X"`` will fill the subplot by using unequal scales].
428+
429+
{aliases}
430+
"""
431+
arg_str = " ".join(["set", f"{ax}", build_arg_string(kwargs)])
432+
with Session() as lib:
433+
lib.call_module(module=f"subplot", args=arg_str)
434+
435+
@fmt_docstring
436+
@use_alias(V="verbose")
437+
def end_subplot(self, **kwargs):
438+
"""
439+
This command finalizes the current subplot, including any placement
440+
of tags, and updates the gmt.history to reflect the dimensions and
441+
linear projection required to draw the entire figure outline. This
442+
allows subsequent commands, such as colorbar, to use
443+
``position="J"`` to place bars with reference to the complete
444+
figure dimensions. We also reset the current plot location to where
445+
it was prior to the subplot.
446+
447+
{aliases}
448+
"""
449+
arg_str = " ".join(["end", build_arg_string(kwargs)])
450+
with Session() as lib:
451+
lib.call_module(module="subplot", args=arg_str)

pygmt/pygmtplot.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import numpy as np
2+
3+
from .figure import SubPlot
4+
5+
6+
def subplots(nrows=1, ncols=1, figsize=(6.4, 4.8), **kwargs):
7+
"""
8+
Create a figure with a set of subplots.
9+
10+
Parameters
11+
----------
12+
nrows : int
13+
Number of rows of the subplot grid.
14+
15+
ncols : int
16+
Number of columns of the subplot grid.
17+
18+
figsize : tuple
19+
Figure dimensions as ``(width, height)``.
20+
21+
Returns
22+
-------
23+
fig : :class:`pygmt.Figure`
24+
A PyGMT Figure instance.
25+
26+
axs : numpy.ndarray
27+
Array of Axes objects.
28+
"""
29+
# Get PyGMT Figure with SubPlot initiated
30+
fig = SubPlot(nrows=nrows, ncols=ncols, figsize=figsize, **kwargs)
31+
32+
# Setup matplotlib-like Axes
33+
axs = np.empty(shape=(nrows, ncols), dtype=object)
34+
for index in range(nrows * ncols):
35+
i = index // ncols # row
36+
j = index % ncols # column
37+
axs[i, j] = index
38+
39+
return fig, axs

pygmt/tests/test_subplot.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,20 @@
33
"""
44
import pytest
55

6-
from .. import Figure
7-
from ..exceptions import GMTInvalidInput
6+
from ..pygmtplot import subplots
87

98

109
@pytest.mark.mpl_image_compare
1110
def test_subplot_basic():
1211
"""
1312
Create a subplot figure with 1 row and 2 columns.
1413
"""
15-
fig = Figure()
16-
fig.subplot(directive="begin", row=1, col=2, dimensions="f6c/3c")
17-
fig.subplot(directive="set", row=0, col=0)
14+
fig, axs = subplots(nrows=1, ncols=2, figsize=("6c", "3c"))
15+
fig.sca(ax=axs[0, 0])
1816
fig.basemap(region=[0, 3, 0, 3], frame=True)
19-
fig.subplot(directive="set", row=0, col=1)
17+
fig.sca(ax=axs[0, 1])
2018
fig.basemap(region=[0, 3, 0, 3], frame=True)
21-
fig.subplot(directive="end")
19+
fig.end_subplot()
2220
return fig
2321

2422

@@ -27,20 +25,10 @@ def test_subplot_frame():
2725
"""
2826
Check that map frame setting is applied to all subplot figures
2927
"""
30-
fig = Figure()
31-
fig.subplot(directive="begin", row=1, col=2, dimensions="f6c/3c", frame="WSne")
32-
fig.subplot(directive="set", row=0, col=0)
28+
fig, axs = subplots(nrows=1, ncols=2, figsize=("6c", "3c"), frame="WSne")
29+
fig.sca(ax=axs[0, 0])
3330
fig.basemap(region=[0, 3, 0, 3], frame="+tplot0")
34-
fig.subplot(directive="set", row=0, col=1)
31+
fig.sca(ax=axs[0, 1])
3532
fig.basemap(region=[0, 3, 0, 3], frame="+tplot1")
36-
fig.subplot(directive="end")
33+
fig.end_subplot()
3734
return fig
38-
39-
40-
def test_subplot_incorrect_directive():
41-
"""
42-
Check that subplot fails when an incorrect directive is used
43-
"""
44-
fig = Figure()
45-
with pytest.raises(GMTInvalidInput):
46-
fig.subplot(directive="start")

0 commit comments

Comments
 (0)