From 66c4b97320ce92fab9b70fde2791ad5054f0c71e Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Fri, 13 Oct 2023 22:08:43 +0800
Subject: [PATCH 01/19] Refactor the data_kind and the virtualfile_to_data
 functions

---
 pygmt/clib/session.py     |  35 ++++----
 pygmt/helpers/__init__.py |   1 +
 pygmt/helpers/utils.py    | 166 +++++++++++++++++++-------------------
 pygmt/src/contour.py      |   2 +-
 4 files changed, 99 insertions(+), 105 deletions(-)

diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py
index 8db686812c1..0988d87728d 100644
--- a/pygmt/clib/session.py
+++ b/pygmt/clib/session.py
@@ -32,6 +32,7 @@
     fmt_docstring,
     tempfile_from_geojson,
     tempfile_from_image,
+    validate_data_input,
 )
 
 FAMILIES = [
@@ -1474,11 +1475,8 @@ def virtualfile_from_data(
         self,
         check_kind=None,
         data=None,
-        x=None,
-        y=None,
-        z=None,
-        extra_arrays=None,
-        required_z=False,
+        vectors=None,
+        ncols=2,
         required_data=True,
     ):
         """
@@ -1497,13 +1495,11 @@ def virtualfile_from_data(
             Any raster or vector data format. This could be a file name or
             path, a raster grid, a vector matrix/arrays, or other supported
             data input.
-        x/y/z : 1-D arrays or None
-            x, y, and z columns as numpy arrays.
-        extra_arrays : list of 1-D arrays
-            Optional. A list of numpy arrays in addition to x, y, and z.
-            All of these arrays must be of the same size as the x/y/z arrays.
-        required_z : bool
-            State whether the 'z' column is required.
+        vectors : list of 1-D arrays or None
+            A list of 1-D arrays. Each array will be a column in the table.
+            All of these arrays must be of the same size.
+        ncols : int
+            The minimum number of columns required for the data.
         required_data : bool
             Set to True when 'data' is required, or False when dealing with
             optional virtual files. [Default is True].
@@ -1537,8 +1533,13 @@ def virtualfile_from_data(
         ...
         <vector memory>: N = 3 <7/9> <4/6> <1/3>
         """
-        kind = data_kind(
-            data, x, y, z, required_z=required_z, required_data=required_data
+        kind = data_kind(data, required=required_data)
+        validate_data_input(
+            data=data,
+            vectors=vectors,
+            ncols=ncols,
+            required_data=required_data,
+            kind=kind,
         )
 
         if check_kind:
@@ -1579,11 +1580,7 @@ def virtualfile_from_data(
                 warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2)
             _data = (data,) if not isinstance(data, pathlib.PurePath) else (str(data),)
         elif kind == "vectors":
-            _data = [np.atleast_1d(x), np.atleast_1d(y)]
-            if z is not None:
-                _data.append(np.atleast_1d(z))
-            if extra_arrays:
-                _data.extend(extra_arrays)
+            _data = [np.atleast_1d(v) for v in vectors]
         elif kind == "matrix":  # turn 2-D arrays into list of vectors
             try:
                 # pandas.Series will be handled below like a 1-D numpy.ndarray
diff --git a/pygmt/helpers/__init__.py b/pygmt/helpers/__init__.py
index 5eb265e8002..71bc0582252 100644
--- a/pygmt/helpers/__init__.py
+++ b/pygmt/helpers/__init__.py
@@ -20,4 +20,5 @@
     is_nonstr_iter,
     launch_external_viewer,
     non_ascii_to_octal,
+    validate_data_input,
 )
diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index 31629a6ea52..d6b7ab8ce5a 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -15,113 +15,125 @@
 from pygmt.exceptions import GMTInvalidInput
 
 
-def _validate_data_input(
-    data=None, x=None, y=None, z=None, required_z=False, required_data=True, kind=None
+def validate_data_input(
+    data=None, vectors=None, ncols=2, required_data=True, kind=None
 ):
     """
-    Check if the combination of data/x/y/z is valid.
+    Check if the data input is valid.
 
     Examples
     --------
-    >>> _validate_data_input(data="infile")
-    >>> _validate_data_input(x=[1, 2, 3], y=[4, 5, 6])
-    >>> _validate_data_input(x=[1, 2, 3], y=[4, 5, 6], z=[7, 8, 9])
-    >>> _validate_data_input(data=None, required_data=False)
-    >>> _validate_data_input()
+    >>> validate_data_input(data="infile")
+    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6]], ncols=2)
+    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], ncols=3)
+    >>> validate_data_input(data=None, required_data=False)
+    >>> validate_data_input()
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: No input data provided.
-    >>> _validate_data_input(x=[1, 2, 3])
+    >>> validate_data_input(vectors=[[1, 2, 3], None], ncols=2)
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Must provide both x and y.
-    >>> _validate_data_input(y=[4, 5, 6])
+    pygmt.exceptions.GMTInvalidInput: The 'y' column can't be None.
+    >>> validate_data_input(vectors=[None, [4, 5, 6]], ncols=2)
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Must provide both x and y.
-    >>> _validate_data_input(x=[1, 2, 3], y=[4, 5, 6], required_z=True)
+    pygmt.exceptions.GMTInvalidInput: The 'x' column can't be None.
+    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], None], ncols=3)
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Must provide x, y, and z.
+    pygmt.exceptions.GMTInvalidInput: The 'z' column can't be None.
     >>> import numpy as np
     >>> import pandas as pd
     >>> import xarray as xr
     >>> data = np.arange(8).reshape((4, 2))
-    >>> _validate_data_input(data=data, required_z=True, kind="matrix")
+    >>> validate_data_input(data=data, ncols=3, kind="matrix")
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: data must provide x, y, and z columns.
-    >>> _validate_data_input(
+    pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    >>> validate_data_input(
     ...     data=pd.DataFrame(data, columns=["x", "y"]),
-    ...     required_z=True,
+    ...     ncols=3,
     ...     kind="matrix",
     ... )
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: data must provide x, y, and z columns.
-    >>> _validate_data_input(
+    pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    >>> validate_data_input(
     ...     data=xr.Dataset(pd.DataFrame(data, columns=["x", "y"])),
-    ...     required_z=True,
+    ...     ncols=3,
     ...     kind="matrix",
     ... )
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: data must provide x, y, and z columns.
-    >>> _validate_data_input(data="infile", x=[1, 2, 3])
+    pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    >>> validate_data_input(data="infile", vectors=[[1, 2, 3], None])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Use either data or x/y/z.
-    >>> _validate_data_input(data="infile", y=[4, 5, 6])
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.  # noqa: W505
+    >>> validate_data_input(data="infile", vectors=[None, [4, 5, 6]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Use either data or x/y/z.
-    >>> _validate_data_input(data="infile", z=[7, 8, 9])
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays. # noqa: W505
+    >>> validate_data_input(data="infile", vectors=[None, None, [7, 8, 9]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Use either data or x/y/z.
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.  # noqa: W505
 
     Raises
     ------
     GMTInvalidInput
         If the data input is not valid.
     """
-    if data is None:  # data is None
-        if x is None and y is None:  # both x and y are None
-            if required_data:  # data is not optional
-                raise GMTInvalidInput("No input data provided.")
-        elif x is None or y is None:  # either x or y is None
-            raise GMTInvalidInput("Must provide both x and y.")
-        if required_z and z is None:  # both x and y are not None, now check z
-            raise GMTInvalidInput("Must provide x, y, and z.")
-    else:  # data is not None
-        if x is not None or y is not None or z is not None:
-            raise GMTInvalidInput("Too much data. Use either data or x/y/z.")
-        # For 'matrix' kind, check if data has the required z column
-        if kind == "matrix" and required_z:
+    if kind is None:
+        kind = data_kind(data=data, required=required_data)
+
+    if kind == "vectors":  # From data_kind, we know that data is None
+        if vectors is None:
+            raise GMTInvalidInput("No input data provided.")
+        if len(vectors) < ncols:
+            raise GMTInvalidInput(
+                f"Requires {ncols} 1-D arrays but got {len(vectors)}."
+            )
+        for i, v in enumerate(vectors[:ncols]):
+            if v is None:
+                if i < 3:
+                    msg = f"The '{'xyz'[i]}' column can't be None."
+                else:
+                    msg = "Column {i} can't be None."
+                raise GMTInvalidInput(msg)
+    else:
+        if vectors is not None and any(v is not None for v in vectors):
+            raise GMTInvalidInput("Too much data. Pass in either 'data' or 1-D arrays.")
+        if kind == "matrix":  # check number of columns for matrix-like data
             if hasattr(data, "shape"):  # np.ndarray or pd.DataFrame
-                if len(data.shape) == 1 and data.shape[0] < 3:
-                    raise GMTInvalidInput("data must provide x, y, and z columns.")
-                if len(data.shape) > 1 and data.shape[1] < 3:
-                    raise GMTInvalidInput("data must provide x, y, and z columns.")
-            if hasattr(data, "data_vars") and len(data.data_vars) < 3:  # xr.Dataset
-                raise GMTInvalidInput("data must provide x, y, and z columns.")
+                if len(data.shape) == 1 and data.shape[0] < ncols:
+                    raise GMTInvalidInput(f"data must have at least {ncols} columns.")
+                if len(data.shape) > 1 and data.shape[1] < ncols:
+                    raise GMTInvalidInput(f"data must have at least {ncols} columns.")
+            if hasattr(data, "data_vars") and len(data.data_vars) < ncols:  # xr.Dataset
+                raise GMTInvalidInput(f"data must have at least {ncols} columns.")
 
 
-def data_kind(data=None, x=None, y=None, z=None, required_z=False, required_data=True):
+def data_kind(data=None, required=True):
     """
-    Check what kind of data is provided to a module.
+    Determine the kind of data that will be passed to a module.
 
-    Possible types:
+    It checks the type of the ``data`` argument and determines the kind of
+    data. Falls back to ``"vectors"`` if ``data`` is None but required.
 
-    * a file name provided as 'data'
-    * a pathlib.PurePath object provided as 'data'
-    * an xarray.DataArray object provided as 'data'
-    * a 2-D matrix provided as 'data'
-    * 1-D arrays x and y (and z, optionally)
-    * an optional argument (None, bool, int or float) provided as 'data'
+    Possible data kinds:
 
-    Arguments should be ``None`` if not used. If doesn't fit any of these
-    categories (or fits more than one), will raise an exception.
+    - ``'file'``: a file name or a pathlib.PurePath object providfed as 'data'
+    - ``'arg'``: an optional argument (None, bool, int or float) provided
+      as 'data'
+    - ``'grid'``: an xarray.DataArray with 2 dimensions provided as 'data'
+    - ``'image'``: an xarray.DataArray with 3 dimensions provided as 'data'
+    - ``'geojson'``: a geo-like Python object that implements
+      ``__geo_interface__`` (geopandas.GeoDataFrame or shapely.geometry)
+      provided as 'data'
+    - ``'matrix'``: a 2-D array provided as 'data'
+    - ``'vectors'``: a list of 1-D arrays provided as 'vectors'
 
     Parameters
     ----------
@@ -129,13 +141,7 @@ def data_kind(data=None, x=None, y=None, z=None, required_z=False, required_data
         Pass in either a file name or :class:`pathlib.Path` to an ASCII data
         table, an :class:`xarray.DataArray`, a 1-D/2-D
         {table-classes} or an option argument.
-    x/y : 1-D arrays or None
-        x and y columns as numpy arrays.
-    z : 1-D array or None
-        z column as numpy array. To be used optionally when x and y are given.
-    required_z : bool
-        State whether the 'z' column is required.
-    required_data : bool
+    required : bool
         Set to True when 'data' is required, or False when dealing with
         optional virtual files. [Default is True].
 
@@ -151,29 +157,28 @@ def data_kind(data=None, x=None, y=None, z=None, required_z=False, required_data
     >>> import numpy as np
     >>> import xarray as xr
     >>> import pathlib
-    >>> data_kind(data=None, x=np.array([1, 2, 3]), y=np.array([4, 5, 6]))
+    >>> data_kind(data=None)
     'vectors'
-    >>> data_kind(data=np.arange(10).reshape((5, 2)), x=None, y=None)
+    >>> data_kind(data=np.arange(10).reshape((5, 2)))
     'matrix'
-    >>> data_kind(data="my-data-file.txt", x=None, y=None)
+    >>> data_kind(data="my-data-file.txt")
     'file'
-    >>> data_kind(data=pathlib.Path("my-data-file.txt"), x=None, y=None)
+    >>> data_kind(data=pathlib.Path("my-data-file.txt"))
     'file'
-    >>> data_kind(data=None, x=None, y=None, required_data=False)
+    >>> data_kind(data=None, required=False)
     'arg'
-    >>> data_kind(data=2.0, x=None, y=None, required_data=False)
+    >>> data_kind(data=2.0, required=False)
     'arg'
-    >>> data_kind(data=True, x=None, y=None, required_data=False)
+    >>> data_kind(data=True, required=False)
     'arg'
     >>> data_kind(data=xr.DataArray(np.random.rand(4, 3)))
     'grid'
     >>> data_kind(data=xr.DataArray(np.random.rand(3, 4, 5)))
     'image'
     """
-    # determine the data kind
     if isinstance(data, (str, pathlib.PurePath)):
         kind = "file"
-    elif isinstance(data, (bool, int, float)) or (data is None and not required_data):
+    elif isinstance(data, (bool, int, float)) or (data is None and not required):
         kind = "arg"
     elif isinstance(data, xr.DataArray):
         kind = "image" if len(data.dims) == 3 else "grid"
@@ -181,19 +186,10 @@ def data_kind(data=None, x=None, y=None, z=None, required_z=False, required_data
         # geo-like Python object that implements ``__geo_interface__``
         # (geopandas.GeoDataFrame or shapely.geometry)
         kind = "geojson"
-    elif data is not None:
+    elif data is not None:  # anything but None is taken as a matrix
         kind = "matrix"
-    else:
+    else:  # fallback to vectors if data is None but required
         kind = "vectors"
-    _validate_data_input(
-        data=data,
-        x=x,
-        y=y,
-        z=z,
-        required_z=required_z,
-        required_data=required_data,
-        kind=kind,
-    )
     return kind
 
 
diff --git a/pygmt/src/contour.py b/pygmt/src/contour.py
index 6aaf22b7cd6..ac34dcb5d95 100644
--- a/pygmt/src/contour.py
+++ b/pygmt/src/contour.py
@@ -116,7 +116,7 @@ def contour(self, data=None, x=None, y=None, z=None, **kwargs):
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+            check_kind="vector", data=data, vectors=[x, y, z], ncols=3
         )
         with file_context as fname:
             lib.call_module(

From 78c28cdd52679b92dfb78e36e27a029bc30c56e7 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Sat, 14 Oct 2023 21:59:34 +0800
Subject: [PATCH 02/19] Update more functions

---
 pygmt/src/blockm.py       |  2 +-
 pygmt/src/nearneighbor.py |  2 +-
 pygmt/src/plot.py         | 19 ++++++++++++-------
 pygmt/src/plot3d.py       | 25 ++++++++++++-------------
 pygmt/src/project.py      |  2 +-
 pygmt/src/rose.py         |  2 +-
 pygmt/src/sphdistance.py  |  2 +-
 pygmt/src/surface.py      |  2 +-
 pygmt/src/text.py         | 13 ++++++++-----
 pygmt/src/triangulate.py  |  2 +-
 pygmt/src/wiggle.py       |  2 +-
 pygmt/src/xyz2grd.py      |  2 +-
 12 files changed, 41 insertions(+), 34 deletions(-)

diff --git a/pygmt/src/blockm.py b/pygmt/src/blockm.py
index bc896c853dc..d8462445ed6 100644
--- a/pygmt/src/blockm.py
+++ b/pygmt/src/blockm.py
@@ -44,7 +44,7 @@ def _blockm(block_method, data, x, y, z, outfile, **kwargs):
     with GMTTempFile(suffix=".csv") as tmpfile:
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
             )
             # Run blockm* on data table
             with table_context as infile:
diff --git a/pygmt/src/nearneighbor.py b/pygmt/src/nearneighbor.py
index 53aa9057dde..b53b8ec4a9f 100644
--- a/pygmt/src/nearneighbor.py
+++ b/pygmt/src/nearneighbor.py
@@ -150,7 +150,7 @@ def nearneighbor(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
             )
             with table_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py
index 069ef5c7077..3e998122486 100644
--- a/pygmt/src/plot.py
+++ b/pygmt/src/plot.py
@@ -213,11 +213,13 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
     # pylint: disable=too-many-locals
     kwargs = self._preprocess(**kwargs)  # pylint: disable=protected-access
 
-    kind = data_kind(data, x, y)
+    kind = data_kind(data)
+    vectors = [x, y]
+    ncols = 2
 
-    extra_arrays = []
     if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
-        extra_arrays.extend(direction)
+        vectors.extend(direction)
+        ncols += 2
     elif (
         kwargs.get("S") is None
         and kind == "geojson"
@@ -239,14 +241,16 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
             raise GMTInvalidInput(
                 "Can't use arrays for fill if data is matrix or file."
             )
-        extra_arrays.append(kwargs["G"])
+        vectors.append(kwargs["G"])
+        ncols += 1
         del kwargs["G"]
     if size is not None:
         if kind != "vectors":
             raise GMTInvalidInput(
                 "Can't use arrays for 'size' if data is a matrix or file."
             )
-        extra_arrays.append(size)
+        vectors.append(size)
+        ncols += 1
 
     for flag in ["I", "t"]:
         if kwargs.get(flag) is not None and is_nonstr_iter(kwargs[flag]):
@@ -254,12 +258,13 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
                 raise GMTInvalidInput(
                     f"Can't use arrays for {plot.aliases[flag]} if data is matrix or file."
                 )
-            extra_arrays.append(kwargs[flag])
+            vectors.append(kwargs[flag])
+            ncols += 1
             kwargs[flag] = ""
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, x=x, y=y, extra_arrays=extra_arrays
+            check_kind="vector", data=data, vectors=vectors, ncols=ncols
         )
 
         with file_context as fname:
diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py
index 8999933ff6f..215b659ac8f 100644
--- a/pygmt/src/plot3d.py
+++ b/pygmt/src/plot3d.py
@@ -183,11 +183,13 @@ def plot3d(
     # pylint: disable=too-many-locals
     kwargs = self._preprocess(**kwargs)  # pylint: disable=protected-access
 
-    kind = data_kind(data, x, y, z)
+    kind = data_kind(data)
+    vectors = [x, y, z]
+    ncols = 3
 
-    extra_arrays = []
     if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
-        extra_arrays.extend(direction)
+        vectors.extend(direction)
+        ncols += 2
     elif (
         kwargs.get("S") is None
         and kind == "geojson"
@@ -209,14 +211,16 @@ def plot3d(
             raise GMTInvalidInput(
                 "Can't use arrays for fill if data is matrix or file."
             )
-        extra_arrays.append(kwargs["G"])
+        vectors.append(kwargs["G"])
+        ncols += 1
         del kwargs["G"]
     if size is not None:
         if kind != "vectors":
             raise GMTInvalidInput(
                 "Can't use arrays for 'size' if data is a matrix or a file."
             )
-        extra_arrays.append(size)
+        ncols += 1
+        vectors.append(size)
 
     for flag in ["I", "t"]:
         if kwargs.get(flag) is not None and is_nonstr_iter(kwargs[flag]):
@@ -224,18 +228,13 @@ def plot3d(
                 raise GMTInvalidInput(
                     f"Can't use arrays for {plot3d.aliases[flag]} if data is matrix or file."
                 )
-            extra_arrays.append(kwargs[flag])
+            vectors.append(kwargs[flag])
+            ncols += 1
             kwargs[flag] = ""
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector",
-            data=data,
-            x=x,
-            y=y,
-            z=z,
-            extra_arrays=extra_arrays,
-            required_z=True,
+            check_kind="vector", data=data, vectors=vectors, ncols=ncols
         )
 
         with file_context as fname:
diff --git a/pygmt/src/project.py b/pygmt/src/project.py
index bdb2490c071..dd7f665fd4b 100644
--- a/pygmt/src/project.py
+++ b/pygmt/src/project.py
@@ -228,7 +228,7 @@ def project(data=None, x=None, y=None, z=None, outfile=None, **kwargs):
         with Session() as lib:
             if kwargs.get("G") is None:
                 table_context = lib.virtualfile_from_data(
-                    check_kind="vector", data=data, x=x, y=y, z=z, required_z=False
+                    check_kind="vector", data=data, vectors=[x, y, z], ncols=3
                 )
 
                 # Run project on the temporary (csv) data table
diff --git a/pygmt/src/rose.py b/pygmt/src/rose.py
index c60ede7d61d..4a9b39d2cfd 100644
--- a/pygmt/src/rose.py
+++ b/pygmt/src/rose.py
@@ -203,7 +203,7 @@ def rose(self, data=None, length=None, azimuth=None, **kwargs):
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, x=length, y=azimuth
+            check_kind="vector", data=data, vectors=[length, azimuth], ncols=2
         )
 
         with file_context as fname:
diff --git a/pygmt/src/sphdistance.py b/pygmt/src/sphdistance.py
index f6242b1f43d..46f2ee2abc2 100644
--- a/pygmt/src/sphdistance.py
+++ b/pygmt/src/sphdistance.py
@@ -120,7 +120,7 @@ def sphdistance(data=None, x=None, y=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y
+                check_kind="vector", data=data, vectors=[x, y], ncols=2
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/surface.py b/pygmt/src/surface.py
index 80987f80de4..32b099c2af0 100644
--- a/pygmt/src/surface.py
+++ b/pygmt/src/surface.py
@@ -165,7 +165,7 @@ def surface(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/text.py b/pygmt/src/text.py
index cd7dabcc3d5..f4a368aa16f 100644
--- a/pygmt/src/text.py
+++ b/pygmt/src/text.py
@@ -179,7 +179,7 @@ def text_(
             raise GMTInvalidInput(
                 "Provide either position only, or x/y pairs, or textfiles."
             )
-        kind = data_kind(textfiles, x, y, text)
+        kind = data_kind(textfiles)
         if kind == "vectors" and text is None:
             raise GMTInvalidInput("Must provide text with x/y pairs")
     else:
@@ -221,22 +221,25 @@ def text_(
     if isinstance(position, str):
         kwargs["F"] += f"+c{position}+t{text}"
 
-    extra_arrays = []
+    vectors = [x, y]
+    ncols = 2
     # If an array of transparency is given, GMT will read it from
     # the last numerical column per data record.
     if kwargs.get("t") is not None and is_nonstr_iter(kwargs["t"]):
-        extra_arrays.append(kwargs["t"])
+        vectors.append(kwargs["t"])
         kwargs["t"] = ""
+        ncols += 1
 
     # Append text at last column. Text must be passed in as str type.
     if kind == "vectors":
-        extra_arrays.append(
+        vectors.append(
             np.vectorize(non_ascii_to_octal)(np.atleast_1d(text).astype(str))
         )
+        ncols += 1
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=textfiles, x=x, y=y, extra_arrays=extra_arrays
+            check_kind="vector", data=textfiles, vectors=vectors, ncols=ncols
         )
         with file_context as fname:
             lib.call_module(module="text", args=build_arg_string(kwargs, infile=fname))
diff --git a/pygmt/src/triangulate.py b/pygmt/src/triangulate.py
index de77394cc9b..3f47926e90b 100644
--- a/pygmt/src/triangulate.py
+++ b/pygmt/src/triangulate.py
@@ -127,7 +127,7 @@ def _triangulate(
         """
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y, z=z, required_z=False
+                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
             )
             with table_context as infile:
                 # table output if outgrid is unset, else output to outgrid
diff --git a/pygmt/src/wiggle.py b/pygmt/src/wiggle.py
index ff5dec107a1..2b2577a0b2c 100644
--- a/pygmt/src/wiggle.py
+++ b/pygmt/src/wiggle.py
@@ -122,7 +122,7 @@ def wiggle(
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+            check_kind="vector", data=data, vectors=[x, y, z], ncols=3
         )
 
         with file_context as fname:
diff --git a/pygmt/src/xyz2grd.py b/pygmt/src/xyz2grd.py
index fe510cc3316..b0cf1f51ff2 100644
--- a/pygmt/src/xyz2grd.py
+++ b/pygmt/src/xyz2grd.py
@@ -154,7 +154,7 @@ def xyz2grd(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, x=x, y=y, z=z, required_z=True
+                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:

From f37413b2aa03c24de6c9ea1e94053d7607b4d9b2 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Sun, 15 Oct 2023 19:14:33 +0800
Subject: [PATCH 03/19] Change ncols to names

---
 pygmt/clib/session.py     |  9 +++---
 pygmt/helpers/utils.py    | 64 +++++++++++++++++++++++++--------------
 pygmt/src/blockm.py       |  2 +-
 pygmt/src/contour.py      |  2 +-
 pygmt/src/nearneighbor.py |  2 +-
 pygmt/src/plot.py         | 12 ++++----
 pygmt/src/plot3d.py       | 12 ++++----
 pygmt/src/project.py      |  2 +-
 pygmt/src/rose.py         |  5 ++-
 pygmt/src/sphdistance.py  |  2 +-
 pygmt/src/surface.py      |  2 +-
 pygmt/src/text.py         |  8 ++---
 pygmt/src/triangulate.py  |  2 +-
 pygmt/src/wiggle.py       |  2 +-
 pygmt/src/xyz2grd.py      |  2 +-
 15 files changed, 75 insertions(+), 53 deletions(-)

diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py
index 0988d87728d..c2a0ba12427 100644
--- a/pygmt/clib/session.py
+++ b/pygmt/clib/session.py
@@ -1476,7 +1476,7 @@ def virtualfile_from_data(
         check_kind=None,
         data=None,
         vectors=None,
-        ncols=2,
+        names=["x", "y"],
         required_data=True,
     ):
         """
@@ -1498,8 +1498,9 @@ def virtualfile_from_data(
         vectors : list of 1-D arrays or None
             A list of 1-D arrays. Each array will be a column in the table.
             All of these arrays must be of the same size.
-        ncols : int
-            The minimum number of columns required for the data.
+        names : list of str
+            A list of names for each of the columns. Must be of the same size
+            as the number of vectors. Default is ``["x", "y"]``.
         required_data : bool
             Set to True when 'data' is required, or False when dealing with
             optional virtual files. [Default is True].
@@ -1537,7 +1538,7 @@ def virtualfile_from_data(
         validate_data_input(
             data=data,
             vectors=vectors,
-            ncols=ncols,
+            names=names,
             required_data=required_data,
             kind=kind,
         )
diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index d6b7ab8ce5a..6926f1a1911 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -16,30 +16,49 @@
 
 
 def validate_data_input(
-    data=None, vectors=None, ncols=2, required_data=True, kind=None
+    data=None, vectors=None, names=["x", "y"], required_data=True, kind=None
 ):
     """
     Check if the data input is valid.
 
+    Parameters
+    ----------
+    data : str, pathlib.PurePath, None, bool, xarray.DataArray or {table-like}
+        Pass in either a file name or :class:`pathlib.Path` to an ASCII data
+        table, an :class:`xarray.DataArray`, a 1-D/2-D
+        {table-classes} or an option argument.
+    vectors : list of 1-D arrays
+        A list of 1-D arrays with the data columns.
+    names : list of str
+        List of column names.
+    required_data : bool
+        Set to True when 'data' is required, or False when dealing with
+        optional virtual files. [Default is True].
+    kind : str or None
+        The kind of data that will be passed to a module. If not given, it
+        will be determined by calling :func:`data_kind`.
+
     Examples
     --------
     >>> validate_data_input(data="infile")
-    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6]], ncols=2)
-    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], ncols=3)
+    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6]], names="xy")
+    >>> validate_data_input(
+    ...     vectors=[[1, 2, 3], [4, 5, 6], [7, 8, 9]], names="xyz"
+    ... )
     >>> validate_data_input(data=None, required_data=False)
     >>> validate_data_input()
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: No input data provided.
-    >>> validate_data_input(vectors=[[1, 2, 3], None], ncols=2)
+    >>> validate_data_input(vectors=[[1, 2, 3], None], names="xy")
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: The 'y' column can't be None.
-    >>> validate_data_input(vectors=[None, [4, 5, 6]], ncols=2)
+    >>> validate_data_input(vectors=[None, [4, 5, 6]], names="xy")
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: The 'x' column can't be None.
-    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], None], ncols=3)
+    >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], None], names="xyz")
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: The 'z' column can't be None.
@@ -47,13 +66,13 @@ def validate_data_input(
     >>> import pandas as pd
     >>> import xarray as xr
     >>> data = np.arange(8).reshape((4, 2))
-    >>> validate_data_input(data=data, ncols=3, kind="matrix")
+    >>> validate_data_input(data=data, names="xyz", kind="matrix")
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
     >>> validate_data_input(
     ...     data=pd.DataFrame(data, columns=["x", "y"]),
-    ...     ncols=3,
+    ...     names="xyz",
     ...     kind="matrix",
     ... )
     Traceback (most recent call last):
@@ -61,7 +80,7 @@ def validate_data_input(
     pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
     >>> validate_data_input(
     ...     data=xr.Dataset(pd.DataFrame(data, columns=["x", "y"])),
-    ...     ncols=3,
+    ...     names="xyz",
     ...     kind="matrix",
     ... )
     Traceback (most recent call last):
@@ -91,28 +110,27 @@ def validate_data_input(
     if kind == "vectors":  # From data_kind, we know that data is None
         if vectors is None:
             raise GMTInvalidInput("No input data provided.")
-        if len(vectors) < ncols:
+        if len(vectors) < len(names):
             raise GMTInvalidInput(
-                f"Requires {ncols} 1-D arrays but got {len(vectors)}."
+                f"Requires {len(names)} 1-D arrays but got {len(vectors)}."
             )
-        for i, v in enumerate(vectors[:ncols]):
+        for i, v in enumerate(vectors[: len(names)]):
             if v is None:
-                if i < 3:
-                    msg = f"The '{'xyz'[i]}' column can't be None."
-                else:
-                    msg = "Column {i} can't be None."
-                raise GMTInvalidInput(msg)
+                raise GMTInvalidInput(f"Column {i} ('{names[i]}') can't be None.")
     else:
         if vectors is not None and any(v is not None for v in vectors):
             raise GMTInvalidInput("Too much data. Pass in either 'data' or 1-D arrays.")
         if kind == "matrix":  # check number of columns for matrix-like data
+            msg = f"data must have at least {len(names)} columns.\n" + " ".join(names)
             if hasattr(data, "shape"):  # np.ndarray or pd.DataFrame
-                if len(data.shape) == 1 and data.shape[0] < ncols:
-                    raise GMTInvalidInput(f"data must have at least {ncols} columns.")
-                if len(data.shape) > 1 and data.shape[1] < ncols:
-                    raise GMTInvalidInput(f"data must have at least {ncols} columns.")
-            if hasattr(data, "data_vars") and len(data.data_vars) < ncols:  # xr.Dataset
-                raise GMTInvalidInput(f"data must have at least {ncols} columns.")
+                if len(data.shape) == 1 and data.shape[0] < len(names):
+                    raise GMTInvalidInput(msg)
+                if len(data.shape) > 1 and data.shape[1] < len(names):
+                    raise GMTInvalidInput(msg)
+            if hasattr(data, "data_vars") and len(data.data_vars) < len(
+                names
+            ):  # xr.Dataset
+                raise GMTInvalidInput(msg)
 
 
 def data_kind(data=None, required=True):
diff --git a/pygmt/src/blockm.py b/pygmt/src/blockm.py
index d8462445ed6..bfe1be138dd 100644
--- a/pygmt/src/blockm.py
+++ b/pygmt/src/blockm.py
@@ -44,7 +44,7 @@ def _blockm(block_method, data, x, y, z, outfile, **kwargs):
     with GMTTempFile(suffix=".csv") as tmpfile:
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
             )
             # Run blockm* on data table
             with table_context as infile:
diff --git a/pygmt/src/contour.py b/pygmt/src/contour.py
index ac34dcb5d95..3e88c45aec5 100644
--- a/pygmt/src/contour.py
+++ b/pygmt/src/contour.py
@@ -116,7 +116,7 @@ def contour(self, data=None, x=None, y=None, z=None, **kwargs):
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+            check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
         )
         with file_context as fname:
             lib.call_module(
diff --git a/pygmt/src/nearneighbor.py b/pygmt/src/nearneighbor.py
index b53b8ec4a9f..d266cce075c 100644
--- a/pygmt/src/nearneighbor.py
+++ b/pygmt/src/nearneighbor.py
@@ -150,7 +150,7 @@ def nearneighbor(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
             )
             with table_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py
index 3e998122486..5c7bfb20351 100644
--- a/pygmt/src/plot.py
+++ b/pygmt/src/plot.py
@@ -215,11 +215,11 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
 
     kind = data_kind(data)
     vectors = [x, y]
-    ncols = 2
+    names = ["x", "y"]
 
     if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
         vectors.extend(direction)
-        ncols += 2
+        names.extend(["x2", "y2"])
     elif (
         kwargs.get("S") is None
         and kind == "geojson"
@@ -242,7 +242,7 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
                 "Can't use arrays for fill if data is matrix or file."
             )
         vectors.append(kwargs["G"])
-        ncols += 1
+        names.append("fill")
         del kwargs["G"]
     if size is not None:
         if kind != "vectors":
@@ -250,7 +250,7 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
                 "Can't use arrays for 'size' if data is a matrix or file."
             )
         vectors.append(size)
-        ncols += 1
+        names.append("size")
 
     for flag in ["I", "t"]:
         if kwargs.get(flag) is not None and is_nonstr_iter(kwargs[flag]):
@@ -259,12 +259,12 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
                     f"Can't use arrays for {plot.aliases[flag]} if data is matrix or file."
                 )
             vectors.append(kwargs[flag])
-            ncols += 1
+            names.append(plot.aliases[flag])
             kwargs[flag] = ""
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, vectors=vectors, ncols=ncols
+            check_kind="vector", data=data, vectors=vectors, names=names
         )
 
         with file_context as fname:
diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py
index 215b659ac8f..1cbce57bc6e 100644
--- a/pygmt/src/plot3d.py
+++ b/pygmt/src/plot3d.py
@@ -185,11 +185,11 @@ def plot3d(
 
     kind = data_kind(data)
     vectors = [x, y, z]
-    ncols = 3
+    names = ["x", "y", "z"]
 
     if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
         vectors.extend(direction)
-        ncols += 2
+        names.extend(["x2", "y2"])
     elif (
         kwargs.get("S") is None
         and kind == "geojson"
@@ -212,15 +212,15 @@ def plot3d(
                 "Can't use arrays for fill if data is matrix or file."
             )
         vectors.append(kwargs["G"])
-        ncols += 1
+        names.append("fill")
         del kwargs["G"]
     if size is not None:
         if kind != "vectors":
             raise GMTInvalidInput(
                 "Can't use arrays for 'size' if data is a matrix or a file."
             )
-        ncols += 1
         vectors.append(size)
+        names.append("size")
 
     for flag in ["I", "t"]:
         if kwargs.get(flag) is not None and is_nonstr_iter(kwargs[flag]):
@@ -229,12 +229,12 @@ def plot3d(
                     f"Can't use arrays for {plot3d.aliases[flag]} if data is matrix or file."
                 )
             vectors.append(kwargs[flag])
-            ncols += 1
+            names.append(plot3d.aliases[flag])
             kwargs[flag] = ""
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, vectors=vectors, ncols=ncols
+            check_kind="vector", data=data, vectors=vectors, names=names
         )
 
         with file_context as fname:
diff --git a/pygmt/src/project.py b/pygmt/src/project.py
index dd7f665fd4b..c5ecd8cb83a 100644
--- a/pygmt/src/project.py
+++ b/pygmt/src/project.py
@@ -228,7 +228,7 @@ def project(data=None, x=None, y=None, z=None, outfile=None, **kwargs):
         with Session() as lib:
             if kwargs.get("G") is None:
                 table_context = lib.virtualfile_from_data(
-                    check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                    check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
                 )
 
                 # Run project on the temporary (csv) data table
diff --git a/pygmt/src/rose.py b/pygmt/src/rose.py
index 4a9b39d2cfd..01862152543 100644
--- a/pygmt/src/rose.py
+++ b/pygmt/src/rose.py
@@ -203,7 +203,10 @@ def rose(self, data=None, length=None, azimuth=None, **kwargs):
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, vectors=[length, azimuth], ncols=2
+            check_kind="vector",
+            data=data,
+            vectors=[length, azimuth],
+            names=["length", "azimuth"],
         )
 
         with file_context as fname:
diff --git a/pygmt/src/sphdistance.py b/pygmt/src/sphdistance.py
index 46f2ee2abc2..a7179496531 100644
--- a/pygmt/src/sphdistance.py
+++ b/pygmt/src/sphdistance.py
@@ -120,7 +120,7 @@ def sphdistance(data=None, x=None, y=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y], ncols=2
+                check_kind="vector", data=data, vectors=[x, y], names="xy"
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/surface.py b/pygmt/src/surface.py
index 32b099c2af0..62341a52b95 100644
--- a/pygmt/src/surface.py
+++ b/pygmt/src/surface.py
@@ -165,7 +165,7 @@ def surface(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:
diff --git a/pygmt/src/text.py b/pygmt/src/text.py
index f4a368aa16f..21e652bb8e4 100644
--- a/pygmt/src/text.py
+++ b/pygmt/src/text.py
@@ -222,24 +222,24 @@ def text_(
         kwargs["F"] += f"+c{position}+t{text}"
 
     vectors = [x, y]
-    ncols = 2
+    names = ["x", "y"]
     # If an array of transparency is given, GMT will read it from
     # the last numerical column per data record.
     if kwargs.get("t") is not None and is_nonstr_iter(kwargs["t"]):
         vectors.append(kwargs["t"])
         kwargs["t"] = ""
-        ncols += 1
+        names.append("transparency")
 
     # Append text at last column. Text must be passed in as str type.
     if kind == "vectors":
         vectors.append(
             np.vectorize(non_ascii_to_octal)(np.atleast_1d(text).astype(str))
         )
-        ncols += 1
+        names.append("text")
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=textfiles, vectors=vectors, ncols=ncols
+            check_kind="vector", data=textfiles, vectors=vectors, names=names
         )
         with file_context as fname:
             lib.call_module(module="text", args=build_arg_string(kwargs, infile=fname))
diff --git a/pygmt/src/triangulate.py b/pygmt/src/triangulate.py
index 3f47926e90b..da980730638 100644
--- a/pygmt/src/triangulate.py
+++ b/pygmt/src/triangulate.py
@@ -127,7 +127,7 @@ def _triangulate(
         """
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
             )
             with table_context as infile:
                 # table output if outgrid is unset, else output to outgrid
diff --git a/pygmt/src/wiggle.py b/pygmt/src/wiggle.py
index 2b2577a0b2c..74cd5c34223 100644
--- a/pygmt/src/wiggle.py
+++ b/pygmt/src/wiggle.py
@@ -122,7 +122,7 @@ def wiggle(
 
     with Session() as lib:
         file_context = lib.virtualfile_from_data(
-            check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+            check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
         )
 
         with file_context as fname:
diff --git a/pygmt/src/xyz2grd.py b/pygmt/src/xyz2grd.py
index b0cf1f51ff2..e0b0d041353 100644
--- a/pygmt/src/xyz2grd.py
+++ b/pygmt/src/xyz2grd.py
@@ -154,7 +154,7 @@ def xyz2grd(data=None, x=None, y=None, z=None, **kwargs):
     with GMTTempFile(suffix=".nc") as tmpfile:
         with Session() as lib:
             file_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], ncols=3
+                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
             )
             with file_context as infile:
                 if (outgrid := kwargs.get("G")) is None:

From 3de76667a175493dda96be524cff3bc063515b89 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Sun, 15 Oct 2023 20:28:07 +0800
Subject: [PATCH 04/19] Fix more tests

---
 pygmt/tests/test_clib.py | 22 +++++-----------------
 1 file changed, 5 insertions(+), 17 deletions(-)

diff --git a/pygmt/tests/test_clib.py b/pygmt/tests/test_clib.py
index 9f2c73857cd..a537cd7306c 100644
--- a/pygmt/tests/test_clib.py
+++ b/pygmt/tests/test_clib.py
@@ -440,7 +440,7 @@ def test_virtualfile_from_data_required_z_matrix(array_func, kind):
     data = array_func(dataframe)
     with clib.Session() as lib:
         with lib.virtualfile_from_data(
-            data=data, required_z=True, check_kind="vector"
+            data=data, names="xyz", check_kind="vector"
         ) as vfile:
             with GMTTempFile() as outfile:
                 lib.call_module("info", f"{vfile} ->{outfile.name}")
@@ -463,9 +463,7 @@ def test_virtualfile_from_data_required_z_matrix_missing():
     data = np.ones((5, 2))
     with clib.Session() as lib:
         with pytest.raises(GMTInvalidInput):
-            with lib.virtualfile_from_data(
-                data=data, required_z=True, check_kind="vector"
-            ):
+            with lib.virtualfile_from_data(data=data, names="xyz", check_kind="vector"):
                 pass
 
 
@@ -481,10 +479,7 @@ def test_virtualfile_from_data_fail_non_valid_data(data):
             continue
         with clib.Session() as lib:
             with pytest.raises(GMTInvalidInput):
-                lib.virtualfile_from_data(
-                    x=variable[0],
-                    y=variable[1],
-                )
+                lib.virtualfile_from_data(vectors=variable[:2])
 
     # Test all combinations where at least one data variable
     # is not given in the x, y, z case:
@@ -494,19 +489,12 @@ def test_virtualfile_from_data_fail_non_valid_data(data):
             continue
         with clib.Session() as lib:
             with pytest.raises(GMTInvalidInput):
-                lib.virtualfile_from_data(
-                    x=variable[0], y=variable[1], z=variable[2], required_z=True
-                )
+                lib.virtualfile_from_data(vectors=variable[:3], names="xyz")
 
     # Should also fail if given too much data
     with clib.Session() as lib:
         with pytest.raises(GMTInvalidInput):
-            lib.virtualfile_from_data(
-                x=data[:, 0],
-                y=data[:, 1],
-                z=data[:, 2],
-                data=data,
-            )
+            lib.virtualfile_from_data(vectors=data[:, :3], data=data, names="xyz")
 
 
 def test_virtualfile_from_vectors(dtypes):

From 93b91d041a9a925e1dccc233e0f4e727dcd88045 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Sun, 15 Oct 2023 23:43:30 +0800
Subject: [PATCH 05/19] Fix project

---
 pygmt/src/project.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/pygmt/src/project.py b/pygmt/src/project.py
index c5ecd8cb83a..7eaf846accc 100644
--- a/pygmt/src/project.py
+++ b/pygmt/src/project.py
@@ -227,8 +227,9 @@ def project(data=None, x=None, y=None, z=None, outfile=None, **kwargs):
             outfile = tmpfile.name
         with Session() as lib:
             if kwargs.get("G") is None:
+                # passed three vectors but only x/y are required
                 table_context = lib.virtualfile_from_data(
-                    check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
+                    check_kind="vector", data=data, vectors=[x, y, z], names="xy"
                 )
 
                 # Run project on the temporary (csv) data table

From 1d6e568227531fd1224196ae0e2d70cf94c33a8f Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Mon, 16 Oct 2023 13:26:05 +0800
Subject: [PATCH 06/19] Fix more tests

---
 pygmt/helpers/utils.py      |  6 +++---
 pygmt/tests/test_helpers.py | 21 ---------------------
 2 files changed, 3 insertions(+), 24 deletions(-)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index 6926f1a1911..f45d1d51375 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -53,15 +53,15 @@ def validate_data_input(
     >>> validate_data_input(vectors=[[1, 2, 3], None], names="xy")
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: The 'y' column can't be None.
+    pygmt.exceptions.GMTInvalidInput: Column 1 ('y') can't be None.
     >>> validate_data_input(vectors=[None, [4, 5, 6]], names="xy")
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: The 'x' column can't be None.
+    pygmt.exceptions.GMTInvalidInput: Column 0 ('x') can't be None.
     >>> validate_data_input(vectors=[[1, 2, 3], [4, 5, 6], None], names="xyz")
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: The 'z' column can't be None.
+    pygmt.exceptions.GMTInvalidInput: Column 2 ('z') can't be None.
     >>> import numpy as np
     >>> import pandas as pd
     >>> import xarray as xr
diff --git a/pygmt/tests/test_helpers.py b/pygmt/tests/test_helpers.py
index b726897e12e..02871d62c84 100644
--- a/pygmt/tests/test_helpers.py
+++ b/pygmt/tests/test_helpers.py
@@ -3,7 +3,6 @@
 """
 import os
 
-import numpy as np
 import pytest
 import xarray as xr
 from pygmt import Figure
@@ -11,7 +10,6 @@
 from pygmt.helpers import (
     GMTTempFile,
     args_in_kwargs,
-    data_kind,
     kwargs_to_strings,
     unique_name,
 )
@@ -31,25 +29,6 @@ def test_load_static_earth_relief():
     assert isinstance(data, xr.DataArray)
 
 
-@pytest.mark.parametrize(
-    "data,x,y",
-    [
-        (None, None, None),
-        ("data.txt", np.array([1, 2]), np.array([4, 5])),
-        ("data.txt", np.array([1, 2]), None),
-        ("data.txt", None, np.array([4, 5])),
-        (None, np.array([1, 2]), None),
-        (None, None, np.array([4, 5])),
-    ],
-)
-def test_data_kind_fails(data, x, y):
-    """
-    Make sure data_kind raises exceptions when it should.
-    """
-    with pytest.raises(GMTInvalidInput):
-        data_kind(data=data, x=x, y=y)
-
-
 def test_unique_name():
     """
     Make sure the names are really unique.

From 6f9fc195107d8981ae20c368ae97c55f254557e2 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Mon, 16 Oct 2023 15:02:44 +0800
Subject: [PATCH 07/19] Fixes

---
 pygmt/src/project.py     | 8 ++++++--
 pygmt/src/triangulate.py | 6 +++++-
 2 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/pygmt/src/project.py b/pygmt/src/project.py
index 7eaf846accc..b85c783004e 100644
--- a/pygmt/src/project.py
+++ b/pygmt/src/project.py
@@ -222,14 +222,18 @@ def project(data=None, x=None, y=None, z=None, outfile=None, **kwargs):
             "The `convention` parameter is not allowed with `generate`."
         )
 
+    # z is optional
+    vectors, names = [x, y], "xy"
+    if z is not None:
+        vectors.append(z)
+
     with GMTTempFile(suffix=".csv") as tmpfile:
         if outfile is None:  # Output to tmpfile if outfile is not set
             outfile = tmpfile.name
         with Session() as lib:
             if kwargs.get("G") is None:
-                # passed three vectors but only x/y are required
                 table_context = lib.virtualfile_from_data(
-                    check_kind="vector", data=data, vectors=[x, y, z], names="xy"
+                    check_kind="vector", data=data, vectors=vectors, names=names
                 )
 
                 # Run project on the temporary (csv) data table
diff --git a/pygmt/src/triangulate.py b/pygmt/src/triangulate.py
index da980730638..759b567a4c4 100644
--- a/pygmt/src/triangulate.py
+++ b/pygmt/src/triangulate.py
@@ -125,9 +125,13 @@ def _triangulate(
             - None if ``output_type`` is "file" (output is stored in
               ``outgrid`` or ``outfile``)
         """
+        vectors, names = [x, y], "xy"
+        if z is not None:
+            vectors.append(z)
+
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], names="xyz"
+                check_kind="vector", data=data, vectors=[x, y, z], names="xy"
             )
             with table_context as infile:
                 # table output if outgrid is unset, else output to outgrid

From 0db21bca0293140db92428788442830e7647d6c2 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 17:05:08 +0800
Subject: [PATCH 08/19] Fix triangulate

---
 pygmt/src/triangulate.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pygmt/src/triangulate.py b/pygmt/src/triangulate.py
index edf8b010ac0..f89904e1b3f 100644
--- a/pygmt/src/triangulate.py
+++ b/pygmt/src/triangulate.py
@@ -131,7 +131,7 @@ def _triangulate(
 
         with Session() as lib:
             table_context = lib.virtualfile_from_data(
-                check_kind="vector", data=data, vectors=[x, y, z], names="xy"
+                check_kind="vector", data=data, vectors=vectors, names=names
             )
             with table_context as infile:
                 # table output if outgrid is unset, else output to outgrid

From 7cf52903a362fa0ac9c87e3115df1c47ba92acd0 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 17:06:58 +0800
Subject: [PATCH 09/19] Fix text

---
 pygmt/src/text.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/pygmt/src/text.py b/pygmt/src/text.py
index f23b166ddd9..91b60b7187a 100644
--- a/pygmt/src/text.py
+++ b/pygmt/src/text.py
@@ -198,24 +198,29 @@ def text_(
     ):
         kwargs.update({"F": ""})
 
-    extra_arrays = []
-    for arg, flag in [(angle, "+a"), (font, "+f"), (justify, "+j")]:
+    vectors = [x, y]
+    names = ["x", "y"]
+    for arg, flag, name in [
+        (angle, "+a", "angle"),
+        (font, "+f", "font"),
+        (justify, "+j", "justify"),
+    ]:
         if arg is True:
             kwargs["F"] += flag
         elif is_nonstr_iter(arg):
             kwargs["F"] += flag
             if flag == "+a":  # angle is numeric type
-                extra_arrays.append(np.atleast_1d(arg))
+                vectors.append(np.atleast_1d(arg))
+                names.append(name)
             else:  # font or justify is str type
-                extra_arrays.append(np.atleast_1d(arg).astype(str))
+                vectors.append(np.atleast_1d(arg).astype(str))
+                names.append(name)
         elif isinstance(arg, (int, float, str)):
             kwargs["F"] += f"{flag}{arg}"
 
     if isinstance(position, str):
         kwargs["F"] += f"+c{position}+t{text}"
 
-    vectors = [x, y]
-    names = ["x", "y"]
     # If an array of transparency is given, GMT will read it from
     # the last numerical column per data record.
     if kwargs.get("t") is not None and is_nonstr_iter(kwargs["t"]):

From b0b6d2a3efd5c80f9bae0e6d59dc21b72ce4fc4a Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 17:18:23 +0800
Subject: [PATCH 10/19] Fix more failing tests

---
 pygmt/helpers/utils.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index f45d1d51375..aa3796800f8 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -70,6 +70,7 @@ def validate_data_input(
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    x y z
     >>> validate_data_input(
     ...     data=pd.DataFrame(data, columns=["x", "y"]),
     ...     names="xyz",
@@ -78,6 +79,7 @@ def validate_data_input(
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    x y z
     >>> validate_data_input(
     ...     data=xr.Dataset(pd.DataFrame(data, columns=["x", "y"])),
     ...     names="xyz",
@@ -86,6 +88,7 @@ def validate_data_input(
     Traceback (most recent call last):
         ...
     pygmt.exceptions.GMTInvalidInput: data must have at least 3 columns.
+    x y z
     >>> validate_data_input(data="infile", vectors=[[1, 2, 3], None])
     Traceback (most recent call last):
         ...

From fa875efff861d512501b81f4f9c3e9c7a9e712f4 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 17:33:47 +0800
Subject: [PATCH 11/19] More fixes

---
 pygmt/helpers/utils.py | 6 +++---
 pygmt/src/text.py      | 2 ++
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index aa3796800f8..ddc4c66ca40 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -92,15 +92,15 @@ def validate_data_input(
     >>> validate_data_input(data="infile", vectors=[[1, 2, 3], None])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.  # noqa: W505
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, [4, 5, 6]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays. # noqa: W505
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, None, [7, 8, 9]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.  # noqa: W505
+    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
 
     Raises
     ------
diff --git a/pygmt/src/text.py b/pygmt/src/text.py
index 91b60b7187a..87ecce5eb66 100644
--- a/pygmt/src/text.py
+++ b/pygmt/src/text.py
@@ -179,6 +179,8 @@ def text_(
         kind = data_kind(textfiles)
         if kind == "vectors" and text is None:
             raise GMTInvalidInput("Must provide text with x/y pairs")
+        if kind != "vectors" and text is not None:
+            raise GMTInvalidInput("Must provide text with x/y pairs")
     else:
         if x is not None or y is not None or textfiles is not None:
             raise GMTInvalidInput(

From 2ee0df27d06297b7313cb13e15ccb5a2b4ede505 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 18:12:06 +0800
Subject: [PATCH 12/19] Fix linting issues

---
 pygmt/helpers/utils.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index ddc4c66ca40..8f8099184f8 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -92,15 +92,15 @@ def validate_data_input(
     >>> validate_data_input(data="infile", vectors=[[1, 2, 3], None])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, [4, 5, 6]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, None, [7, 8, 9]])
     Traceback (most recent call last):
         ...
-    pygmt.exceptions.GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
 
     Raises
     ------

From d5c83408bdb59ae899566cf803bdf1535c2edaad Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Tue, 17 Oct 2023 19:12:43 +0800
Subject: [PATCH 13/19] Fix linting issues

---
 pygmt/helpers/utils.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index 8f8099184f8..9a243b7fb28 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -92,15 +92,15 @@ def validate_data_input(
     >>> validate_data_input(data="infile", vectors=[[1, 2, 3], None])
     Traceback (most recent call last):
         ...
-    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    pygmt...GMTInvalidInput: Too much data. Use either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, [4, 5, 6]])
     Traceback (most recent call last):
         ...
-    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    pygmt...GMTInvalidInput: Too much data. Use either 'data' or 1-D arrays.
     >>> validate_data_input(data="infile", vectors=[None, None, [7, 8, 9]])
     Traceback (most recent call last):
         ...
-    ...GMTInvalidInput: Too much data. Pass in either 'data' or 1-D arrays.
+    pygmt...GMTInvalidInput: Too much data. Use either 'data' or 1-D arrays.
 
     Raises
     ------
@@ -122,7 +122,7 @@ def validate_data_input(
                 raise GMTInvalidInput(f"Column {i} ('{names[i]}') can't be None.")
     else:
         if vectors is not None and any(v is not None for v in vectors):
-            raise GMTInvalidInput("Too much data. Pass in either 'data' or 1-D arrays.")
+            raise GMTInvalidInput("Too much data. Use either 'data' or 1-D arrays.")
         if kind == "matrix":  # check number of columns for matrix-like data
             msg = f"data must have at least {len(names)} columns.\n" + " ".join(names)
             if hasattr(data, "shape"):  # np.ndarray or pd.DataFrame

From 30bacb13bd10e8c13e2faf617826e1b20861b1d6 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Wed, 18 Oct 2023 10:10:56 +0800
Subject: [PATCH 14/19] Fix linting issues

---
 pygmt/clib/session.py  | 2 +-
 pygmt/helpers/utils.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py
index 82321f83b6a..052ae04ca0d 100644
--- a/pygmt/clib/session.py
+++ b/pygmt/clib/session.py
@@ -1474,7 +1474,7 @@ def virtualfile_from_data(
         check_kind=None,
         data=None,
         vectors=None,
-        names=["x", "y"],
+        names="xy",
         required_data=True,
     ):
         """
diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index 9a243b7fb28..0c25aaf251a 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -16,7 +16,7 @@
 
 
 def validate_data_input(
-    data=None, vectors=None, names=["x", "y"], required_data=True, kind=None
+    data=None, vectors=None, names="xy", required_data=True, kind=None
 ):
     """
     Check if the data input is valid.

From 593f252178b9a5958dc8d82b90da78f6f59a509f Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Fri, 20 Oct 2023 15:59:42 +0800
Subject: [PATCH 15/19] Update pygmt/clib/session.py

---
 pygmt/clib/session.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py
index 052ae04ca0d..25260fb9581 100644
--- a/pygmt/clib/session.py
+++ b/pygmt/clib/session.py
@@ -1496,9 +1496,9 @@ def virtualfile_from_data(
         vectors : list of 1-D arrays or None
             A list of 1-D arrays. Each array will be a column in the table.
             All of these arrays must be of the same size.
-        names : list of str
+        names : str or list of str
             A list of names for each of the columns. Must be of the same size
-            as the number of vectors. Default is ``["x", "y"]``.
+            as the number of vectors. Default is ``"xy"``.
         required_data : bool
             Set to True when 'data' is required, or False when dealing with
             optional virtual files. [Default is True].

From 409337f7572fb3ed865c35596ae59ae41608d07d Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Wed, 25 Oct 2023 08:52:28 +0800
Subject: [PATCH 16/19] Apply suggestions from code review

Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com>
---
 pygmt/helpers/utils.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index ff7ecb589b0..413993c6b1b 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -33,7 +33,7 @@ def validate_data_input(
         List of column names.
     required_data : bool
         Set to True when 'data' is required, or False when dealing with
-        optional virtual files. [Default is True].
+        optional virtual files [Default is True].
     kind : str or None
         The kind of data that will be passed to a module. If not given, it
         will be determined by calling :func:`data_kind`.
@@ -145,7 +145,7 @@ def data_kind(data=None, required=True):
 
     Possible data kinds:
 
-    - ``'file'``: a file name or a pathlib.PurePath object providfed as 'data'
+    - ``'file'``: a file name or a pathlib.PurePath object provided as 'data'
     - ``'arg'``: an optional argument (None, bool, int or float) provided
       as 'data'
     - ``'grid'``: an xarray.DataArray with 2 dimensions provided as 'data'

From 5c10fc47482687158aac5292c7f6adf6aab984b7 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Thu, 11 Jul 2024 18:05:37 +0800
Subject: [PATCH 17/19] Fix plot and plot3d

---
 pygmt/src/plot.py   | 81 +++++++++++++++++++--------------------------
 pygmt/src/plot3d.py | 81 +++++++++++++++++++--------------------------
 2 files changed, 68 insertions(+), 94 deletions(-)

diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py
index ecf817d2b74..4d62db7ca4b 100644
--- a/pygmt/src/plot.py
+++ b/pygmt/src/plot.py
@@ -2,8 +2,6 @@
 plot - Plot in two dimensions.
 """
 
-from pathlib import Path
-
 from pygmt.clib import Session
 from pygmt.exceptions import GMTInvalidInput
 from pygmt.helpers import (
@@ -14,7 +12,6 @@
     kwargs_to_strings,
     use_alias,
 )
-from pygmt.src.which import which
 
 
 @fmt_docstring
@@ -210,50 +207,40 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
     vectors = [x, y]
     names = ["x", "y"]
 
-    if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
-        vectors.extend(direction)
-        names.extend(["x2", "y2"])
-    elif (
-        kwargs.get("S") is None
-        and kind == "geojson"
-        and data.geom_type.isin(["Point", "MultiPoint"]).all()
-    ):  # checking if the geometry of a geoDataFrame is Point or MultiPoint
-        kwargs["S"] = "s0.2c"
-    elif kwargs.get("S") is None and kind == "file" and str(data).endswith(".gmt"):
-        # checking that the data is a file path to set default style
-        try:
-            with Path.open(which(data), encoding="utf8") as file:
-                line = file.readline()
-            if "@GMULTIPOINT" in line or "@GPOINT" in line:
-                # if the file is gmt style and geometry is set to Point
-                kwargs["S"] = "s0.2c"
-        except FileNotFoundError:
-            pass
-    if is_nonstr_iter(kwargs.get("G")):
-        if kind != "vectors":
-            raise GMTInvalidInput(
-                "Can't use arrays for fill if data is matrix or file."
-            )
-        vectors.append(kwargs["G"])
-        names.append("fill")
-        del kwargs["G"]
-    if size is not None:
-        if kind != "vectors":
-            raise GMTInvalidInput(
-                "Can't use arrays for 'size' if data is a matrix or file."
-            )
-        vectors.append(size)
-        names.append("size")
-
-    for flag in ["I", "t"]:
-        if is_nonstr_iter(kwargs.get(flag)):
-            if kind != "vectors":
-                raise GMTInvalidInput(
-                    f"Can't use arrays for {plot.aliases[flag]} if data is matrix or file."
-                )
-            vectors.append(kwargs[flag])
-            names.append(plot.aliases[flag])
-            kwargs[flag] = ""
+    if kind == "vectors":  # Add more columns for vectors input
+        # Parameters for vector styles
+        if (
+            kwargs.get("S") is not None
+            and kwargs["S"][0] in "vV"
+            and is_nonstr_iter(direction)
+        ):
+            vectors.extend(direction)
+            names.extend(["x2", "y2"])
+        # Fill
+        if is_nonstr_iter(kwargs.get("G")):
+            vectors.append(kwargs["G"])
+            names.append("fill")
+            del kwargs["G"]
+        # Size
+        if is_nonstr_iter(size):
+            vectors.append(size)
+            names.append("size")
+        # Intensity and transparency
+        for flag in ["I", "t"]:
+            if is_nonstr_iter(kwargs.get(flag)):
+                vectors.append(kwargs[flag])
+                names.append(plot.aliases[flag])
+                kwargs[flag] = ""
+    else:
+        for name, value in [
+            ("direction", direction),
+            ("fill", kwargs.get("G")),
+            ("size", size),
+            ("intensity", kwargs.get("I")),
+            ("transparency", kwargs.get("t")),
+        ]:
+            if is_nonstr_iter(value):
+                raise GMTInvalidInput(f"'{name}' can't be 1-D array if 'data' is used.")
 
     with Session() as lib:
         file_context = lib.virtualfile_in(
diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py
index f85f8defccd..9b2e8da3610 100644
--- a/pygmt/src/plot3d.py
+++ b/pygmt/src/plot3d.py
@@ -2,8 +2,6 @@
 plot3d - Plot in three dimensions.
 """
 
-from pathlib import Path
-
 from pygmt.clib import Session
 from pygmt.exceptions import GMTInvalidInput
 from pygmt.helpers import (
@@ -14,7 +12,6 @@
     kwargs_to_strings,
     use_alias,
 )
-from pygmt.src.which import which
 
 
 @fmt_docstring
@@ -187,50 +184,40 @@ def plot3d(
     vectors = [x, y, z]
     names = ["x", "y", "z"]
 
-    if kwargs.get("S") is not None and kwargs["S"][0] in "vV" and direction is not None:
-        vectors.extend(direction)
-        names.extend(["x2", "y2"])
-    elif (
-        kwargs.get("S") is None
-        and kind == "geojson"
-        and data.geom_type.isin(["Point", "MultiPoint"]).all()
-    ):  # checking if the geometry of a geoDataFrame is Point or MultiPoint
-        kwargs["S"] = "u0.2c"
-    elif kwargs.get("S") is None and kind == "file" and str(data).endswith(".gmt"):
-        # checking that the data is a file path to set default style
-        try:
-            with Path.open(which(data), encoding="utf8") as file:
-                line = file.readline()
-            if "@GMULTIPOINT" in line or "@GPOINT" in line:
-                # if the file is gmt style and geometry is set to Point
-                kwargs["S"] = "u0.2c"
-        except FileNotFoundError:
-            pass
-    if is_nonstr_iter(kwargs.get("G")):
-        if kind != "vectors":
-            raise GMTInvalidInput(
-                "Can't use arrays for fill if data is matrix or file."
-            )
-        vectors.append(kwargs["G"])
-        names.append("fill")
-        del kwargs["G"]
-    if size is not None:
-        if kind != "vectors":
-            raise GMTInvalidInput(
-                "Can't use arrays for 'size' if data is a matrix or a file."
-            )
-        vectors.append(size)
-        names.append("size")
-
-    for flag in ["I", "t"]:
-        if is_nonstr_iter(kwargs.get(flag)):
-            if kind != "vectors":
-                raise GMTInvalidInput(
-                    f"Can't use arrays for {plot3d.aliases[flag]} if data is matrix or file."
-                )
-            vectors.append(kwargs[flag])
-            names.append(plot3d.aliases[flag])
-            kwargs[flag] = ""
+    if kind == "vectors":  # Add more columns for vectors input
+        # Parameters for vector styles
+        if (
+            kwargs.get("S") is not None
+            and kwargs["S"][0] in "vV"
+            and is_nonstr_iter(direction)
+        ):
+            vectors.extend(direction)
+            names.extend(["x2", "y2", "z2"])
+        # Fill
+        if is_nonstr_iter(kwargs.get("G")):
+            vectors.append(kwargs["G"])
+            names.append("fill")
+            del kwargs["G"]
+        # Size
+        if is_nonstr_iter(size):
+            vectors.append(size)
+            names.append("size")
+        # Intensity and transparency
+        for flag in ["I", "t"]:
+            if is_nonstr_iter(kwargs.get(flag)):
+                vectors.append(kwargs[flag])
+                names.append(plot3d.aliases[flag])
+                kwargs[flag] = ""
+    else:
+        for name, value in [
+            ("direction", direction),
+            ("fill", kwargs.get("G")),
+            ("size", size),
+            ("intensity", kwargs.get("I")),
+            ("transparency", kwargs.get("t")),
+        ]:
+            if is_nonstr_iter(value):
+                raise GMTInvalidInput(f"'{name}' can't be 1-D array if 'data' is used.")
 
     with Session() as lib:
         file_context = lib.virtualfile_in(

From 525a35339d37752efe3300c6ac9a28d3568bcecc Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Thu, 11 Jul 2024 18:09:00 +0800
Subject: [PATCH 18/19] Fix errors in merging the main branch

---
 pygmt/src/plot.py   | 19 +++++++++++++++++--
 pygmt/src/plot3d.py | 16 ++++++++++++++++
 2 files changed, 33 insertions(+), 2 deletions(-)

diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py
index 4d62db7ca4b..bc25f726d56 100644
--- a/pygmt/src/plot.py
+++ b/pygmt/src/plot.py
@@ -2,16 +2,19 @@
 plot - Plot in two dimensions.
 """
 
+from pathlib import Path
+
 from pygmt.clib import Session
 from pygmt.exceptions import GMTInvalidInput
 from pygmt.helpers import (
-    build_arg_string,
+    build_arg_list,
     data_kind,
     fmt_docstring,
     is_nonstr_iter,
     kwargs_to_strings,
     use_alias,
 )
+from pygmt.src import which
 
 
 @fmt_docstring
@@ -241,6 +244,18 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
         ]:
             if is_nonstr_iter(value):
                 raise GMTInvalidInput(f"'{name}' can't be 1-D array if 'data' is used.")
+    # Set the default style if data has a geometry of Point or MultiPoint
+    if kwargs.get("S") is None:
+        if kind == "geojson" and data.geom_type.isin(["Point", "MultiPoint"]).all():
+            kwargs["S"] = "s0.2c"
+        elif kind == "file" and str(data).endswith(".gmt"):  # OGR_GMT file
+            try:
+                with Path(which(data)).open(encoding="utf-8") as file:
+                    line = file.readline()
+                if "@GMULTIPOINT" in line or "@GPOINT" in line:
+                    kwargs["S"] = "s0.2c"
+            except FileNotFoundError:
+                pass
 
     with Session() as lib:
         file_context = lib.virtualfile_in(
@@ -248,4 +263,4 @@ def plot(self, data=None, x=None, y=None, size=None, direction=None, **kwargs):
         )
 
         with file_context as fname:
-            lib.call_module(module="plot", args=build_arg_string(kwargs, infile=fname))
+            lib.call_module(module="plot", args=build_arg_list(kwargs, infile=fname))
diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py
index 9b2e8da3610..d7fd54ba140 100644
--- a/pygmt/src/plot3d.py
+++ b/pygmt/src/plot3d.py
@@ -2,6 +2,8 @@
 plot3d - Plot in three dimensions.
 """
 
+from pathlib import Path
+
 from pygmt.clib import Session
 from pygmt.exceptions import GMTInvalidInput
 from pygmt.helpers import (
@@ -12,6 +14,7 @@
     kwargs_to_strings,
     use_alias,
 )
+from pygmt.src import which
 
 
 @fmt_docstring
@@ -219,6 +222,19 @@ def plot3d(
             if is_nonstr_iter(value):
                 raise GMTInvalidInput(f"'{name}' can't be 1-D array if 'data' is used.")
 
+    # Set the default style if data has a geometry of Point or MultiPoint
+    if kwargs.get("S") is None:
+        if kind == "geojson" and data.geom_type.isin(["Point", "MultiPoint"]).all():
+            kwargs["S"] = "u0.2c"
+        elif kind == "file" and str(data).endswith(".gmt"):  # OGR_GMT file
+            try:
+                with Path(which(data)).open(encoding="utf-8") as file:
+                    line = file.readline()
+                if "@GMULTIPOINT" in line or "@GPOINT" in line:
+                    kwargs["S"] = "u0.2c"
+            except FileNotFoundError:
+                pass
+
     with Session() as lib:
         file_context = lib.virtualfile_in(
             check_kind="vector", data=data, vectors=vectors, names=names

From b55a9ad384e258ce0a00715081d26e255fa31fb6 Mon Sep 17 00:00:00 2001
From: Dongdong Tian <seisman.info@gmail.com>
Date: Sat, 20 Jul 2024 14:09:11 +0800
Subject: [PATCH 19/19] Fix merging issue

---
 pygmt/clib/session.py     | 4 ++--
 pygmt/helpers/__init__.py | 1 -
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py
index 24ea1583f8f..dcab42d43b7 100644
--- a/pygmt/clib/session.py
+++ b/pygmt/clib/session.py
@@ -34,10 +34,10 @@
     GMTVersionError,
 )
 from pygmt.helpers import (
+    _validate_data_input,
     data_kind,
     tempfile_from_geojson,
     tempfile_from_image,
-    validate_data_input,
 )
 
 FAMILIES = [
@@ -1682,7 +1682,7 @@ def virtualfile_in(
         <vector memory>: N = 3 <7/9> <4/6> <1/3>
         """
         kind = data_kind(data, required=required_data)
-        validate_data_input(
+        _validate_data_input(
             data=data,
             vectors=vectors,
             names=names,
diff --git a/pygmt/helpers/__init__.py b/pygmt/helpers/__init__.py
index 337ac1df530..862abbbdd64 100644
--- a/pygmt/helpers/__init__.py
+++ b/pygmt/helpers/__init__.py
@@ -23,6 +23,5 @@
     is_nonstr_iter,
     launch_external_viewer,
     non_ascii_to_octal,
-    validate_data_input,
 )
 from pygmt.helpers.validators import validate_output_table_type