Skip to content

Commit e5845ba

Browse files
mjziebarthleouieda
authored andcommitted
Add basic contour functionality (#212)
The `Figure.contour` method plots contours from tabular data. Add tests and support for data in vectors, matrix, and files.
1 parent 9bc7838 commit e5845ba

File tree

7 files changed

+183
-3
lines changed

7 files changed

+183
-3
lines changed

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Plotting data and laying out the map:
2323
Figure.basemap
2424
Figure.coast
2525
Figure.plot
26+
Figure.contour
2627
Figure.grdimage
2728
Figure.logo
2829

gmt/base_plotting.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,83 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs):
270270
arg_str = " ".join([fname, build_arg_string(kwargs)])
271271
lib.call_module("plot", arg_str)
272272

273+
@fmt_docstring
274+
@use_alias(
275+
R="region",
276+
J="projection",
277+
B="frame",
278+
S="skip",
279+
G="label_placement",
280+
W="pen",
281+
L="triangular_mesh_pen",
282+
i="columns",
283+
C="levels",
284+
)
285+
@kwargs_to_strings(R="sequence", i="sequence_comma")
286+
def contour(self, x=None, y=None, z=None, data=None, **kwargs):
287+
"""
288+
Contour table data by direct triangulation.
289+
290+
Takes a matrix, (x,y,z) pairs, or a file name as input and plots lines,
291+
polygons, or symbols at those locations on a map.
292+
293+
Must provide either *data* or *x*, *y*, and *z*.
294+
295+
[TODO: Insert more documentation]
296+
297+
{gmt_module_docs}
298+
299+
{aliases}
300+
301+
Parameters
302+
----------
303+
x, y, z : 1d arrays
304+
Arrays of x and y coordinates and values z of the data points.
305+
data : str or 2d array
306+
Either a data file name or a 2d numpy array with the tabular data.
307+
{J}
308+
{R}
309+
A : bool or str
310+
``'[m|p|x|y]'``
311+
By default, geographic line segments are drawn as great circle
312+
arcs. To draw them as straight lines, use *A*.
313+
{B}
314+
C : Contour file or level(s)
315+
D : Dump contour coordinates
316+
E : Network information
317+
G : Placement of labels
318+
I : Color the triangles using CPT
319+
L : Pen to draw the underlying triangulation (default none)
320+
N : Do not clip contours
321+
Q : Minimum contour length
322+
``'[p|t]'``
323+
S : Skip input points outside region
324+
``'[p|t]'``
325+
{W}
326+
X : Origin shift x
327+
Y : Origin shift y
328+
329+
330+
"""
331+
kwargs = self._preprocess(**kwargs)
332+
333+
kind = data_kind(data, x, y, z)
334+
if kind == "vectors" and z is None:
335+
raise GMTInvalidInput("Must provided both x, y, and z.")
336+
337+
with Session() as lib:
338+
# Choose how data will be passed in to the module
339+
if kind == "file":
340+
file_context = dummy_context(data)
341+
elif kind == "matrix":
342+
file_context = lib.virtualfile_from_matrix(data)
343+
elif kind == "vectors":
344+
file_context = lib.virtualfile_from_vectors(x, y, z)
345+
346+
with file_context as fname:
347+
arg_str = " ".join([fname, build_arg_string(kwargs)])
348+
lib.call_module("contour", arg_str)
349+
273350
@fmt_docstring
274351
@use_alias(R="region", J="projection", B="frame")
275352
@kwargs_to_strings(R="sequence")

gmt/helpers/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
from ..exceptions import GMTInvalidInput
1313

1414

15-
def data_kind(data, x=None, y=None):
15+
def data_kind(data, x=None, y=None, z=None):
1616
"""
1717
Check what kind of data is provided to a module.
1818
1919
Possible types:
2020
2121
* a file name provided as 'data'
2222
* a matrix provided as 'data'
23-
* 1D arrays x and y
23+
* 1D arrays x and y (and z, optionally)
2424
2525
Arguments should be ``None`` if not used. If doesn't fit any of these
2626
categories (or fits more than one), will raise an exception.
@@ -31,6 +31,9 @@ def data_kind(data, x=None, y=None):
3131
Data file name or numpy array.
3232
x, y : 1d arrays or None
3333
x and y columns as numpy arrays.
34+
z : 1d array or None
35+
z column as numpy array. To be used optionally when x and y
36+
are given.
3437
3538
Returns
3639
-------
@@ -67,7 +70,7 @@ def data_kind(data, x=None, y=None):
6770
"""
6871
if data is None and x is None and y is None:
6972
raise GMTInvalidInput("No input data provided.")
70-
if data is not None and (x is not None or y is not None):
73+
if data is not None and (x is not None or y is not None or z is not None):
7174
raise GMTInvalidInput("Too much data. Use either data or x and y.")
7275
if data is None and (x is None or y is None):
7376
raise GMTInvalidInput("Must provided both x and y.")
66.5 KB
Loading
54.5 KB
Loading
91.4 KB
Loading

gmt/tests/test_contour.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# pylint: disable=redefined-outer-name
2+
"""
3+
Tests contour.
4+
"""
5+
import os
6+
from itertools import product
7+
8+
import pytest
9+
import numpy as np
10+
11+
from .. import Figure
12+
from ..exceptions import GMTInvalidInput
13+
14+
15+
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
16+
POINTS_DATA = os.path.join(TEST_DATA_DIR, "points.txt")
17+
18+
19+
@pytest.fixture(scope="module")
20+
def data():
21+
"Load the point data from the test file"
22+
return np.loadtxt(POINTS_DATA)
23+
24+
25+
@pytest.fixture(scope="module")
26+
def region():
27+
"The data region"
28+
return [10, 70, -5, 10]
29+
30+
31+
def test_contour_fail_no_data(data):
32+
"Should raise an exception if no data is given"
33+
# Contour should raise an exception if no or not sufficient data
34+
# is given
35+
fig = Figure()
36+
# Test all combinations where at least one data variable
37+
# is not given:
38+
for variable in product([None, data[:, 0]], repeat=3):
39+
# Filter one valid configuration:
40+
if not any(item is None for item in variable):
41+
continue
42+
with pytest.raises(GMTInvalidInput):
43+
fig.contour(
44+
x=variable[0],
45+
y=variable[1],
46+
z=variable[2],
47+
region=region,
48+
projection="X4i",
49+
color="red",
50+
frame="afg",
51+
pen="",
52+
)
53+
# Should also fail if given too much data
54+
with pytest.raises(GMTInvalidInput):
55+
fig.contour(
56+
x=data[:, 0],
57+
y=data[:, 1],
58+
z=data[:, 2],
59+
data=data,
60+
region=region,
61+
projection="X4i",
62+
style="c0.2c",
63+
color="red",
64+
frame="afg",
65+
pen="",
66+
)
67+
68+
69+
@pytest.mark.mpl_image_compare
70+
def test_contour_vec(region):
71+
"Plot an x-centered gaussian kernel with different y scale"
72+
fig = Figure()
73+
x, y = np.meshgrid(
74+
np.linspace(region[0], region[1]), np.linspace(region[2], region[3])
75+
)
76+
x = x.flatten()
77+
y = y.flatten()
78+
z = (x - 0.5 * (region[0] + region[1])) ** 2 + 4 * y ** 2
79+
z = np.exp(-z / 10 ** 2 * np.log(2))
80+
fig.contour(x=x, y=y, z=z, projection="X4i", region=region, frame="a", pen="")
81+
return fig
82+
83+
84+
@pytest.mark.mpl_image_compare
85+
def test_contour_matrix(data, region):
86+
"Plot data"
87+
fig = Figure()
88+
fig.contour(data=data, projection="X3i", region=region, frame="ag", pen="")
89+
return fig
90+
91+
92+
@pytest.mark.mpl_image_compare
93+
def test_contour_from_file(region):
94+
"Plot using the data file name instead of loaded data"
95+
fig = Figure()
96+
fig.contour(
97+
data=POINTS_DATA, projection="X4i", region=region, frame="af", pen="#ffcb87"
98+
)
99+
return fig

0 commit comments

Comments
 (0)