Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 doc/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Plotting data and laying out the map:
Figure.logo
Figure.image
Figure.shift_origin
Figure.text

Saving and displaying the figure:

Expand Down
107 changes: 107 additions & 0 deletions pygmt/base_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@
Base class with plot generating commands.
Does not define any special non-GMT methods (savefig, show, etc).
"""
import csv
import numpy as np
import pandas as pd

from .clib import Session
from .exceptions import GMTInvalidInput
from .helpers import (
build_arg_string,
dummy_context,
data_kind,
fmt_docstring,
GMTTempFile,
use_alias,
kwargs_to_strings,
)
Expand Down Expand Up @@ -530,3 +535,105 @@ def image(self, imagefile, **kwargs):
with Session() as lib:
arg_str = " ".join([imagefile, build_arg_string(kwargs)])
lib.call_module("image", arg_str)

@fmt_docstring
@use_alias(R="region", J="projection")
@kwargs_to_strings(
R="sequence",
angle="sequence_comma",
font="sequence_comma",
justify="sequence_comma",
)
def text(
self,
textfile=None,
x=None,
y=None,
text=None,
angle=None,
font=None,
justify=None,
**kwargs,
):
"""
Places text on a map.

Used to be pstext.

Takes in a textfile or (x,y,text) triples as input.

Must provide at least *textfile* or *x*, *y*, and *text*.

Full option list at :gmt-docs:`text.html`

{aliases}

Parameters
----------
textfile : str
A text data file name
x, y : float or 1d arrays
The x and y coordinates, or an array of x and y coordinates to plot the text
text : str or 1d array
The text string, or an array of strings to plot on the figure
angle: int/float or bool
Set the angle measured in degrees counter-clockwise from horizontal. E.g. 30
sets the text at 30 degrees. If no angle is given then the input textfile
must have this as a column.
font : str or bool
Set the font specification with format "size,font,color" where size is text
size in points, font is the font to use, and color sets the font color. E.g.
"12p,Helvetica-Bold,red" selects a 12p red Helvetica-Bold font. If no font
info is given then the input textfile must have this information in one of
its columns.
justify: str or bool
Set the alignment which refers to the part of the text string that will be
mapped onto the (x,y) point. Choose a 2 character combination of L, C, R
(for left, center, or right) and T, M, B for top, middle, or bottom. E.g.,
BL for lower left. If no justification is given then the input textfile must
have this as a column.
{J}
{R}
"""
kwargs = self._preprocess(**kwargs)

kind = data_kind(textfile, x, y, text)
if kind == "vectors" and text is None:
raise GMTInvalidInput("Must provide text with x and y.")

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}"

with GMTTempFile(suffix=".txt") as tmpfile:
Copy link
Member

Choose a reason for hiding this comment

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

We should look into passing text though virtual files, actually. I was avoiding this a while ago because the API had changed but that has been standardized now in GMT.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I don't quite get your intention here? Do you mean passing text (as in the command) through virtual files or 'text' (as in plain old text)? Either way I'm confused...

with Session() as lib:
if kind == "file":
fname = textfile
elif kind == "vectors":
pd.DataFrame.from_dict(
{
"x": np.atleast_1d(x),
"y": np.atleast_1d(y),
"text": np.atleast_1d(text),
}
).to_csv(
tmpfile.name,
sep="\t",
header=False,
index=False,
quoting=csv.QUOTE_NONE,
)
fname = tmpfile.name
else:
raise GMTInvalidInput(
"Unrecognized data type: {}".format(type(textfile))
)

arg_str = " ".join([fname, build_arg_string(kwargs)])
lib.call_module("text", arg_str)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_single_line_of_text.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_input_file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_with_angle_30.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pygmt/tests/baseline/test_text_with_font_bold.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions pygmt/tests/data/cities.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
105.87 21.02 LM HANOI
282.95 -12.1 LM LIMA
178.42 -18.13 LM SUVA
237.67 47.58 RM SEATTLE
28.20 -25.75 LM PRETORIA
167 changes: 167 additions & 0 deletions pygmt/tests/test_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# pylint: disable=redefined-outer-name
"""
Tests text
"""
import os

import numpy as np
import pytest

from .. import Figure
from ..exceptions import GMTInvalidInput
from ..helpers import data_kind

TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
POINTS_DATA = os.path.join(TEST_DATA_DIR, "points.txt")
CITIES_DATA = os.path.join(TEST_DATA_DIR, "cities.txt")


@pytest.fixture(scope="module")
def projection():
"The projection system"
return "x4i"


@pytest.fixture(scope="module")
def region():
"The data region"
return [0, 5, 0, 2.5]


@pytest.mark.mpl_image_compare
def test_text_single_line_of_text(region, projection):
"""
Place a single line text of text at some x, y location
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=2.4,
text="This is a line of text",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_multiple_lines_of_text(region, projection):
"""
Place multiple lines of text at their respective x, y locations
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=[1.2, 1.6],
y=[0.6, 0.3],
text=["This is a line of text", "This is another line of text"],
)
return fig


def test_text_without_text_input(region, projection):
"""
Run text by passing in x and y, but no text
"""
fig = Figure()
with pytest.raises(GMTInvalidInput):
fig.text(region=region, projection=projection, x=1.2, y=2.4)


@pytest.mark.mpl_image_compare
def test_text_input_filename(projection):
"""
Run text by passing in a filename to textfile
"""
fig = Figure()
fig.text(region=[10, 70, -5, 10], projection=projection, textfile=POINTS_DATA)
return fig


def test_text_wrong_kind_of_input(projection):
"""
Run text by passing in a data input that is not a file/vectors
"""
fig = Figure()
data = np.loadtxt(POINTS_DATA) # Load points into numpy array
assert data_kind(data) == "matrix"
with pytest.raises(GMTInvalidInput):
fig.text(region=[10, 70, -5, 10], projection=projection, textfile=data)


@pytest.mark.mpl_image_compare
def test_text_angle_30(region, projection):
"""
Print text at 30 degrees counter-clockwise from horizontal
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=2.4,
text="text angle 30 degrees",
angle=30,
)
return fig


@pytest.mark.mpl_image_compare
def test_text_font_bold(region, projection):
"""
Print text with a bold font
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=2.4,
text="text in bold",
font="Helvetica-Bold",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_justify_bottom_right_and_top_left(region, projection):
"""
Print text justified at bottom right and top left
"""
fig = Figure()
fig.text(
region=region,
projection=projection,
x=1.2,
y=0.2,
text="text justified bottom right",
justify="BR",
)
fig.text(
region=region,
projection=projection,
x=1.2,
y=0.2,
text="text justified top left",
justify="TL",
)
return fig


@pytest.mark.mpl_image_compare
def test_text_justify_parsed_from_textfile():
"""
Print text justified based on a column from textfile, using justify=True boolean
operation. Loosely based on "All great-circle paths lead to Rome" gallery example at
https://gmt.soest.hawaii.edu/doc/latest/gallery/ex23.html
"""
fig = Figure()
fig.text(
region="g",
projection="H90/9i",
justify=True,
textfile=CITIES_DATA,
D="j0.45/0+vred", # draw red-line from xy point to text label (city name)
)
return fig