GenericMappingTools / pygmt

A Python interface for the Generic Mapping Tools.
https://www.pygmt.org
BSD 3-Clause "New" or "Revised" License
747 stars 216 forks source link

Pass string-type x and y to text or other plotting functions #633

Closed seisman closed 3 years ago

seisman commented 3 years ago

Description of the problem

PR #480 mention that, x and y of the text() function can accept int, float or str. It's true for PyGMT v0.1.2, but since PyGMT v0.2.0, it's no longer possible to pass string-type x or y.

I haven't looked into the codes to check why it works for v0.1.2. The question is, do we want to accept string-type x and y coordinates?

Full code that generated the error

import pygmt

fig = pygmt.Figure()

fig.basemap(region=[0, 10, 0, 10], frame=True, projection='X10c')
fig.text(x='5.0', y=5, text='TEXT')
fig.show()

Full error message

Traceback (most recent call last):
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/clib/session.py", line 727, in _check_dtype_and_dim
    array = np.asarray(array, dtype=np.datetime64)
  File "/Users/seisman/.anaconda/lib/python3.8/site-packages/numpy/core/_asarray.py", line 85, in asarray
    return array(a, dtype, copy=False, order=order)
ValueError: Error parsing datetime string "5.0" at position 1

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "test.py", line 6, in <module>
    fig.text(x='5.0', y=5, text='TEXT')
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/helpers/decorators.py", line 270, in new_module
    return module_func(*args, **kwargs)
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/helpers/decorators.py", line 407, in new_module
    return module_func(*args, **kwargs)
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/base_plotting.py", line 1213, in text
    with file_context as fname:
  File "/Users/seisman/.anaconda/lib/python3.8/contextlib.py", line 113, in __enter__
    return next(self.gen)
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/clib/session.py", line 1173, in virtualfile_from_vectors
    self.put_vector(dataset, column=col, vector=array)
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/clib/session.py", line 777, in put_vector
    gmt_type = self._check_dtype_and_dim(vector, ndim=1)
  File "/Users/seisman/Gits/gmt/pygmt/pygmt/clib/session.py", line 729, in _check_dtype_and_dim
    raise GMTInvalidInput(
pygmt.exceptions.GMTInvalidInput: Unsupported numpy data type '<class 'numpy.str_'>'.

System information

Please paste the output of python -c "import pygmt; pygmt.show_versions()":

PyGMT information:
  version: v0.2.0+16.g7bedf487
System information:
  python: 3.8.3 (default, Jul  2 2020, 11:26:31)  [Clang 10.0.0 ]
  executable: /Users/seisman/.anaconda/bin/python
  machine: macOS-10.15.6-x86_64-i386-64bit
Dependency information:
  numpy: 1.18.5
  pandas: 1.0.5
  xarray: 0.16.1
  netCDF4: 1.5.3
  packaging: 20.4
  ghostscript: 9.53.1
  gmt: 6.2.0_3f85983_2020.09.26
GMT library information:
  binary dir: /Users/seisman/.anaconda/bin
  cores: 8
  grid layout: rows
  library path: /Users/seisman/local/GMT/lib/libgmt.dylib
  padding: 2
  plugin dir: /Users/seisman/local/GMT/lib/gmt/plugins
  share dir: /Users/seisman/local/GMT/share
  version: 6.2.0
seisman commented 3 years ago

I have a similar issue when passing pandas columns to text().

Full code that generated the error

The script works with PyGMT v0.1.2, but fails with PyGMT v0.2.0 (and master). Here is the example input file used in the script: input.txt

import pandas as pd
import pygmt

data = pd.read_csv("input.txt", names=("id", "longitude", "latitude"), delim_whitespace=True)
fig = pygmt.Figure()
fig.basemap(region=[0, 100, 0, 100], projection='X10c', frame=True)
fig.plot(x=data.longitude, y=data.latitude, style='c0.2c', color='red')
fig.text(x=data.longitude, y=data.latitude, text=data.id, font='16p,red', offset='0.25c')
fig.show()

Full error message

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/Gits/gmt/pygmt/pygmt/clib/session.py in _check_dtype_and_dim(self, array, ndim)
    726                 # Try to convert any unknown numpy data types to np.datetime64
--> 727                 array = np.asarray(array, dtype=np.datetime64)
    728             except ValueError as e:

~/.anaconda/lib/python3.8/site-packages/numpy/core/_asarray.py in asarray(a, dtype, order)
     84     """
---> 85     return array(a, dtype, copy=False, order=order)
     86 

ValueError: Cannot create a NumPy datetime other than NaT with generic units

The above exception was the direct cause of the following exception:

GMTInvalidInput                           Traceback (most recent call last)
<ipython-input-7-b64e448be657> in <module>
      6 fig.basemap(region=[0, 100, 0, 100], projection='X10c', frame=True)
      7 fig.plot(x=data.longitude, y=data.latitude, style='c0.2c', color='red')
----> 8 fig.text(x=data.longitude, y=data.latitude, text=data.id, font='16p,red', offset='0.25c')
      9 fig.show()

~/Gits/gmt/pygmt/pygmt/helpers/decorators.py in new_module(*args, **kwargs)
    268                 if alias in kwargs:
    269                     kwargs[arg] = kwargs.pop(alias)
--> 270             return module_func(*args, **kwargs)
    271 
    272         new_module.aliases = aliases

~/Gits/gmt/pygmt/pygmt/helpers/decorators.py in new_module(*args, **kwargs)
    405                         kwargs[arg] = separators[fmt].join(f"{item}" for item in value)
    406             # Execute the original function and return its output
--> 407             return module_func(*args, **kwargs)
    408 
    409         return new_module

~/Gits/gmt/pygmt/pygmt/base_plotting.py in text(self, textfiles, x, y, position, text, angle, font, justify, **kwargs)
   1211                         np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(text)
   1212                     )
-> 1213             with file_context as fname:
   1214                 arg_str = " ".join([fname, build_arg_string(kwargs)])
   1215                 lib.call_module("text", arg_str)

~/.anaconda/lib/python3.8/contextlib.py in __enter__(self)
    111         del self.args, self.kwds, self.func
    112         try:
--> 113             return next(self.gen)
    114         except StopIteration:
    115             raise RuntimeError("generator didn't yield") from None

~/Gits/gmt/pygmt/pygmt/clib/session.py in virtualfile_from_vectors(self, *vectors)
   1171         # Use put_vector for columns with numerical type data
   1172         for col, array in enumerate(arrays[:columns]):
-> 1173             self.put_vector(dataset, column=col, vector=array)
   1174 
   1175         # Use put_strings for last column(s) with string type data

~/Gits/gmt/pygmt/pygmt/clib/session.py in put_vector(self, dataset, column, vector)
    775         )
    776 
--> 777         gmt_type = self._check_dtype_and_dim(vector, ndim=1)
    778         if gmt_type == self["GMT_DATETIME"]:
    779             vector_pointer = (ctp.c_char_p * len(vector))()

~/Gits/gmt/pygmt/pygmt/clib/session.py in _check_dtype_and_dim(self, array, ndim)
    727                 array = np.asarray(array, dtype=np.datetime64)
    728             except ValueError as e:
--> 729                 raise GMTInvalidInput(
    730                     f"Unsupported numpy data type '{array.dtype.type}'."
    731                 ) from e

GMTInvalidInput: Unsupported numpy data type '<class 'numpy.object_'>'.
weiji14 commented 3 years ago

PR #480 mention that, x and y of the text() function can accept int, float or str. It's true for PyGMT v0.1.2, but since PyGMT v0.2.0, it's no longer possible to pass string-type x or y.

This is because we refactored text in #559. String types work before because we would just read from a temporary intermediate csv file, but now we're passing the xy coordinates directly via the GMT C API.

The question is, do we want to accept string-type x and y coordinates?

Probably not, unless someone uses Degrees/Minutes/Seconds format (does GMT support this)? Our documentation for `text currently says:

https://github.com/GenericMappingTools/pygmt/blob/94b23a2cfe40c45e2ba097bdd2a5043f57b60a2d/pygmt/base_plotting.py#L1100-L1102

So we're somewhat hinting that only numerical types are used. We could try to support string types (that are actually numbers) but that will take a bit of work.

seisman commented 3 years ago

The question is, do we want to accept string-type x and y coordinates?

Probably not, unless someone uses Degrees/Minutes/Seconds format (does GMT support this)?

That's a good point. GMT CLI supports different input format for geographic coordinates. For example:

echo 20:30W 30:15S | gmt plot -JM10c -Baf -R-22/-18/-32/-28 -Sc2c -Gred -pdf map