diff --git a/doc/api/index.rst b/doc/api/index.rst index c4f199b3dbc..d8c39a315c4 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -34,6 +34,7 @@ Plotting data and laying out the map: Figure.logo Figure.image Figure.shift_origin + Figure.subplot Figure.text Figure.meca diff --git a/doc/index.rst b/doc/index.rst index b57c05e992d..04292c9aa88 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -32,6 +32,7 @@ projections/index.rst tutorials/coastlines.rst tutorials/plot.rst + tutorials/subplots.rst .. toctree:: :maxdepth: 2 diff --git a/examples/tutorials/subplots.py b/examples/tutorials/subplots.py new file mode 100644 index 00000000000..4b849f08ee1 --- /dev/null +++ b/examples/tutorials/subplots.py @@ -0,0 +1,87 @@ +""" +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 handle subplots in GMT: + +- Use :meth:`pygmt.Figure.shift_origin` to manually move each individual plot + to the right position. +- Use :meth:`pygmt.Figure.subplot` 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.Figure.subplot` which offers finer grained control, and +this is what the tutorial below will cover. +""" + +############################################################################### +# Let's start by importing the PyGMT library and initiating a figure. + +import pygmt + +fig = pygmt.Figure() + +############################################################################### +# Define subplot layout +# --------------------- +# +# The ``fig.subplot(directive="begin")`` 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.subplot(directive="begin", row=2, col=3, dimensions="s5c/3c", frame="lrtb") + +############################################################################### +# will define our figure to have a 2 row and 3 column grid layout. +# ``dimensions="s5c/3c"`` specifies that each 's'ubplot will have a width of +# 5cm and height of 3cm. Alternatively, you can set ``dimensions="f15c/6c"`` to +# define the overall size of the 'f'igure to be 15cm wide by 6cm high. Using +# ``frame="lrtb"`` allows us to customize the map frame for all subplots. The +# figure layout will look like the following: + +for index in range(2 * 3): + i = index // 3 # row + j = index % 3 # column + fig.subplot(directive="set", row=i, col=j) + fig.text( + x=0.5, y=0.5, text=f"index: {index}, row: {i}, col: {j}", region=[0, 1, 0, 1] + ) +fig.subplot(directive="end") +fig.show() + +############################################################################### +# The ``fig.subplot(directive="set")`` command activates a specified subplot, +# and all subsequent plotting commands will take place in that subplot. In +# order to specify a subplot, you will need to know the identifier for each +# subplot. This can be done by setting the ``row`` and ``col`` arguments. + +############################################################################### +# .. 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) so +# that all subsequent plotting commands happen there, you can use the following +# command: + +############################################################################### +# .. code-block:: default +# +# fig.subplot(directive="set", row=0, col=2) + +############################################################################### +# Finally, remember to use ``fig.subplot(directive="end")`` to exit the subplot +# mode. + +############################################################################### +# .. code-block:: default +# +# fig.subplot(directive="end") diff --git a/pygmt/base_plotting.py b/pygmt/base_plotting.py index 950e1b089de..987813220af 100644 --- a/pygmt/base_plotting.py +++ b/pygmt/base_plotting.py @@ -884,6 +884,60 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg lib.call_module("legend", arg_str) @fmt_docstring + @use_alias(F="dimensions", B="frame") + def subplot(self, directive: str, row: int = None, col: int = None, **kwargs): + """ + Manage modern mode figure subplot configuration and selection. + + The subplot module is used to split the current figure into a + rectangular layout of subplots that each may contain a single + self-contained figure. A subplot setup is started with the begin + directive that defines the layout of the subplots, while positioning to + a particular subplot for plotting is done via the set directive. The + subplot process is completed via the end directive. + + Full option list at :gmt-docs:`subplot.html` + + {aliases} + + Parameters + ---------- + directive : str + Either 'begin', 'set' or 'end'. + row : int + The number of rows if using the 'begin' directive, or the row + number if using the 'set' directive. First row is 0, not 1. + col : int + The number of columns if using the 'begin' directive, or the column + number if using the 'set' directive. First column is 0, not 1. + dimensions : str + ``[f|s]width(s)/height(s)[+fwfracs/hfracs][+cdx/dy][+gfill][+ppen] + [+wpen]`` + Specify the dimensions of the figure when using the 'begin' + directive. There are two different ways to do this: (f) Specify + overall figure dimensions or (s) specify the dimensions of a single + subplot. + """ + if directive not in ("begin", "set", "end"): + raise GMTInvalidInput( + f"Unrecognized subplot directive '{directive}',\ + should be either 'begin', 'set', or 'end'" + ) + + with Session() as lib: + rowcol = "" # default is blank, e.g. when directive == "end" + if row is not None and col is not None: + if directive == "begin": + rowcol = f"{row}x{col}" + elif directive == "set": + rowcol = f"{row},{col}" + arg_str = " ".join( + a for a in [directive, rowcol, build_arg_string(kwargs)] if a + ) + lib.call_module(module="subplot", args=arg_str) + + @fmt_docstring + @use_alias(R="region", J="projection", B="frame") @use_alias( R="region", J="projection", diff --git a/pygmt/tests/baseline/test_subplot_basic.png b/pygmt/tests/baseline/test_subplot_basic.png new file mode 100644 index 00000000000..6a9a6a6b5e5 Binary files /dev/null and b/pygmt/tests/baseline/test_subplot_basic.png differ diff --git a/pygmt/tests/baseline/test_subplot_frame.png b/pygmt/tests/baseline/test_subplot_frame.png new file mode 100644 index 00000000000..071133e1869 Binary files /dev/null and b/pygmt/tests/baseline/test_subplot_frame.png differ diff --git a/pygmt/tests/test_subplot.py b/pygmt/tests/test_subplot.py new file mode 100644 index 00000000000..fa3630e70c8 --- /dev/null +++ b/pygmt/tests/test_subplot.py @@ -0,0 +1,46 @@ +""" +Tests subplot +""" +import pytest + +from .. import Figure +from ..exceptions import GMTInvalidInput + + +@pytest.mark.mpl_image_compare +def test_subplot_basic(): + """ + Create a subplot figure with 1 row and 2 columns. + """ + fig = Figure() + fig.subplot(directive="begin", row=1, col=2, dimensions="f6c/3c") + fig.subplot(directive="set", row=0, col=0) + fig.basemap(region=[0, 3, 0, 3], frame=True) + fig.subplot(directive="set", row=0, col=1) + fig.basemap(region=[0, 3, 0, 3], frame=True) + fig.subplot(directive="end") + return fig + + +@pytest.mark.mpl_image_compare +def test_subplot_frame(): + """ + Check that map frame setting is applied to all subplot figures + """ + fig = Figure() + fig.subplot(directive="begin", row=1, col=2, dimensions="f6c/3c", frame="WSne") + fig.subplot(directive="set", row=0, col=0) + fig.basemap(region=[0, 3, 0, 3], frame="+tplot0") + fig.subplot(directive="set", row=0, col=1) + fig.basemap(region=[0, 3, 0, 3], frame="+tplot1") + fig.subplot(directive="end") + return fig + + +def test_subplot_incorrect_directive(): + """ + Check that subplot fails when an incorrect directive is used + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.subplot(directive="start")