Skip to content

Add tutorial for pygmt.Figure.text #480

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

Merged
merged 10 commits into from
Oct 15, 2020
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/text.rst

.. toctree::
:maxdepth: 2
Expand Down
1 change: 1 addition & 0 deletions examples/tutorials/examples.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
114,0.5,BORNEO
128 changes: 128 additions & 0 deletions examples/tutorials/text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""
Plotting text
=============

It is often useful to add annotations to a map plot. This is handled by
:meth:`pygmt.Figure.text`.
"""

import pygmt

########################################################################################
# Basic map annotation
# --------------------
#
# Text annotations can be added to a map using the :meth:`text` method of the
# :class:`pygmt.Figure`.
#
# Full details of the GMT6 command `text` can be found `here<https://docs.generic-mapping-tools.org/latest/text.html>`_.
# The Python binding to this command is documented `here<https://www.pygmt.org/latest/api/generated/pygmt.Figure.text.html>`_.
#
# Here we create a simple map and add an annotation using the ``text``, ``x``,
# and ``y`` arguments to specify the annotation text and position in the
# projection frame. ``text``, ``x``, and ``y`` accept `int`, `str`, or `float`.

fig = pygmt.Figure()
with pygmt.config(MAP_FRAME_TYPE="plain"):
fig.basemap(region=[108, 120, -5, 8], projection="M20c", frame="a")
Comment on lines +25 to +26
Copy link
Member

Choose a reason for hiding this comment

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

As an introductory tutorial, we probably should avoid using unrelated methods pygmt.config(), although plain frames sometimes look better than "fancy" frames. But I'm also OK to keep pygmt.config() here.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's ok to keep it here for now, this might reduce the number of 'fancy' frames out there in the world 😆

fig.coast(land="black", water="skyblue")

# Plotting text annotations using single elements
fig.text(text="SOUTH CHINA SEA", x=112, y=6)

# Plotting text annotations using lists of elements
fig.text(text=["CELEBES SEA", "JAVA SEA"], x=[119, 112], y=[3.25, -4.6])

fig.show()

########################################################################################
# Changing font style
# -------------------
# The size, family/weight, and colour of an annotation can be specified using the ``font`` argument.
#
# A list of all recognised fonts can be found `here<https://docs.generic-mapping-tools.org/latest/cookbook/postscript_fonts.html>`_),
# including details of how to use non-default fonts.

fig = pygmt.Figure()
with pygmt.config(MAP_FRAME_TYPE="plain"):
fig.basemap(region=[108, 120, -5, 8], projection="M20c", frame="a")
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

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

Same as above for pygmt.config().

fig.coast(land="black", water="skyblue")

# Customising the font style
fig.text(text="BORNEO", x="114.", y=0.5, font="22p,Helvetica-Bold,white")

fig.show()

########################################################################################
# Plotting from a text file
# -------------------------
#
# It is also possible to add annotations from a file containing `x`, `y`, and
# `text` fields. Here we give a complete example.

fig = pygmt.Figure()
with pygmt.config(MAP_FRAME_TYPE="plain"):
fig.basemap(region=[108, 120, -5, 8], projection="M20c", frame="a")
Comment on lines +65 to +66
Copy link
Member

Choose a reason for hiding this comment

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

Same as above for pygmt.config().

fig.coast(land="black", water="skyblue")

# Plot region names / sea names
fig.text(textfiles="examples.txt", font="22p,Helvetica-Bold,white")

# Plot names of seas
fig.text(text=["CELEBES SEA", "JAVA SEA"], x=[119, 112], y=[3.25, -4.6])
fig.text(text="SULU SEA", x=119.12, y=7.25, angle=-40)
fig.text(text="SOUTH CHINA SEA", x=112, y=6, angle=40)
fig.text(text="MAKASSAR STRAIT", x=118.4, y=-1, angle=65)
fig.show()

########################################################################################
# ``justify`` argument
# --------------------
#
# ``justify`` is used to define the anchor point for the bounding box for text
# being added to a plot. The following code segment demonstrates the
# positioning of the anchor point relative to the text.
#
# The anchor is specified with a two letter (order independent) code, chosen
# from:
# * Horizontal anchor: L(eft), C(entre), R(ight)
# * Vertical anchor: T(op), M(iddle), B(ottom)

fig = pygmt.Figure()
fig.basemap(region=[0, 4, 0, 4], projection="X10c", frame="WSen")
for i, y_justify in enumerate(["T", "M", "B"]):
y_pos = 3.5 - i*1.5
fig.plot(x=[0., 4.], y=[y_pos, y_pos], W='3p,red@85')
for j, x_justify in enumerate(["L", "C", "R"]):
justify_text = x_justify + y_justify
x_pos = 0.5 + j*1.5
fig.text(text=justify_text, x=x_pos, y=y_pos,
font="28p,Helvetica-Bold,black", justify=justify_text)
fig.plot(x=[x_pos, x_pos], y=[0., 4.], W="3p,red@85")
fig.show()

########################################################################################
# ``angle`` argument
# ------------------
# ``angle`` is an optional argument used to specify the clockwise rotation of
# the text from the horizontal.

fig = pygmt.Figure()
fig.basemap(region=[0, 4, 0, 4], projection="X10c", frame="WSen")
for i in range(0, 360, 30):
fig.text(text=f"` {i} Degrees", x=2, y=2, justify="LM", angle=i)
fig.show()

########################################################################################
# Additional arguments
# --------------------
#
# Text can be further configured by passing an argument corresponding to the
# flag names in GMT, following the same convention as described in the GMT
# documentation. It is hoped that over time more bindings to these arguments
# will be written into PyGMT.

fig = pygmt.Figure()
fig.basemap(region=[0, 1, 0, 1], projection="X5c", frame="WSen")
fig.text(text="Green", x=0.5, y=0.5, G="green")
Copy link
Member

Choose a reason for hiding this comment

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

We're actually trying to document only the long-form arguments (see #473), and have made a start in #474. Just that we haven't released it yet, see differences in documentation between https://www.pygmt.org/dev/api/generated/pygmt.Figure.text.html and https://www.pygmt.org/v0.1.1/api/generated/pygmt.Figure.text.html#pygmt.Figure.text. I've been thinking whether we should cut a v0.1.2 release to incorporate this PR and other documentation changes, before GMT 6.1.0 comes out around end of June.

Personally I have nothing against using short aliases like this to achieve additional functionality (since I do it too!). Better way would be to add an alias for G=color here, see #385 for an example on how to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I was planning to collect some thoughts on how best to bring the rest of the available flags, or short-form arguments, into the PyGMT fold for text and submit an issue.

So for now, should I remove this last section from the tutorial?

Copy link
Member

Choose a reason for hiding this comment

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

Or we could do a quick PR to get some extra aliases into text. They're basically one-line additions (per alias). Do you have a list of what you want besides color?

Copy link
Contributor Author

@hemmelig hemmelig Jun 18, 2020

Choose a reason for hiding this comment

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

It would make sense to also add an alias for W (pen?) if adding one for G.

I find having font as an alias for F when +f also relates to font a bit ambiguous. There was also the point I raised in Issue #479 - when adding text from a text file, it is unclear if it's actually possible to emulate the way you tell GMT to expect font, angle, or justify fields in the file (-F+f+a+j).

Copy link
Member

@weiji14 weiji14 Jun 18, 2020

Choose a reason for hiding this comment

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

It would make sense to also add an alias for W (pen?) if adding one for G.

Yes, W=pen seems like another obvious choice.

I find having font as an alias for F when +f also relates to font a bit ambiguous. There was also the point I raised in Issue #479 - when adding text from a text file, it is unclear if it's actually possible to emulate the way you tell GMT to expect font, angle, or justify fields in the file (-F+f+a+j).

Looking at the current v0.1.1 text implementation

if angle is not None or font is not None or justify is not None:
if "F" not in kwargs.keys():
kwargs.update({"F": ""})
if angle is not None and isinstance(angle, (int, float)):
kwargs["F"] += f"+a{str(angle)}"
if font is not None and isinstance(font, str):
kwargs["F"] += f"+f{font}"
if justify is not None and isinstance(justify, str):
kwargs["F"] += f"+j{justify}"

I think if you want to emulate -F+f+a+j, you could do fig.text(..., font=True, angle=True, justify=True), and that would then use treat the columns in the textfile as font/angle/justify respectively. Unfortunately it won't be possible to change the order of the columns (e.g. -F+a+j+f I think.

But yes, you're right that it's an ambiguous beast. We could either document this particular case for GMT 'power' users, or find a better way to implement the code block above, bearing in mind that PyGMT is meant to be accessible to new users (see Project Goals) who might not be as familiar with the GMT suite. It would be nice to somewhat emulate what matplotlib.pyplot.text does for example (i.e. have a fontdict like argument).

Copy link
Member

Choose a reason for hiding this comment

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

Yes - works for justify and font, but wouldn't work for angle. Ideally though you would want to be able to set them in the same way?

True, should allow str type for angle as well. I'll start a Pull Request to add some enhancements to text, give me a few minutes.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I've started PR #481 to handle this, so angle="" should work once it's merged in to master. Also checked that angle=True, font=True and justify=True works (See test case at https://github.com/GenericMappingTools/pygmt/pull/481/files#diff-c20c076528e6988bc916b4bfc47e7a8d.
). The documentation at https://www.pygmt.org/dev/api/generated/pygmt.Figure.text does mention that bool types are allowed. That would translate directly to -F+a+f+j to GMT.

Copy link
Contributor Author

@hemmelig hemmelig Jun 20, 2020

Choose a reason for hiding this comment

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

In which case, perhaps it would be best to wait until the above has been merged, so we can give the example as:

fig = pygmt.Figure()
with pygmt.config(MAP_FRAME_TYPE="plain"):
    fig.basemap(region=[108, 120, -5, 8], projection="M20c", frame="a")
fig.coast(land="black", water="skyblue")

# Create file
with open("examples.txt", "w") as f:
    f.write("114 0.5 0 22p,Helvetica-Bold,white CM BORNEO\n")
    f.write("119 3.25 0 12p,Helvetica-Bold,black CM CELEBES SEA\n")
    f.write("112 -4.6 0 12p,Helvetica-Bold,black CM JAVA SEA\n")
    f.write("112 6 40 12p,Helvetica-Bold,black CM SOUTH CHINA SEA\n")
    f.write("119.12 7.25 -40 12p,Helvetica-Bold,black CM SULU SEA\n")
    f.write("118.4 -1 65 12p,Helvetica-Bold,black CM MAKASSAR STRAIT\n")

# Plot region names / sea names
fig.text(textfiles="examples.txt", angle=True, font=True, justify=True)

# Cleanup
os.remove("examples.txt")

fig.show()

Copy link
Member

@weiji14 weiji14 Jun 21, 2020

Choose a reason for hiding this comment

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

Ok, we've merged in #481! If you can merge in the new changes from master into this branch (e.g. by clicking on the 'Update Branch' button), we can start to simplify some of the example code. The new text features/enhancements should be documented at https://www.pygmt.org/dev/api/generated/pygmt.Figure.text.html#pygmt.Figure.text (once the CI build finishes). Let us know if you need any help.

Copy link
Member

@weiji14 weiji14 Oct 14, 2020

Choose a reason for hiding this comment

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

The example here with using with open("examples.txt", "w") as f: ... might be another reason to support textfile=io.StringIO input as with #576, just so we don't need to explicitly create a temporary .txt file. But let's do this another time 😉 Edit: done at commit 20994aa.

So for now, should I remove this last section from the tutorial?

Done at commit 3dde4b0, and I've also kept the section but am using fill instead of G now.

fig.show()