diff --git a/pygmt/src/_common.py b/pygmt/src/_common.py index 8fff5e8e976..09208b7468e 100644 --- a/pygmt/src/_common.py +++ b/pygmt/src/_common.py @@ -8,7 +8,7 @@ from typing import Any, ClassVar, Literal from pygmt.exceptions import GMTParameterError, GMTTypeError, GMTValueError -from pygmt.helpers import is_given +from pygmt.helpers import is_given, is_nonstr_iter from pygmt.params.position import Position from pygmt.src.which import which @@ -395,3 +395,51 @@ def _parse_position( ), ) return position + + +def _parse_series( + series: float | str | Sequence[float] | None = None, +) -> str | Sequence[float] | None: + """ + Parse the "series" parameter for array creation. + + The rules are: + + - A list/tuple of three values is interpreted as *min*/*max*/*inc* (joined by "/") + - Other iterables are interpreted as comma-separated values, and should be passed + via a virtual file, because GMT can't deal with very long CLI arguments + - Other values are converted to a string + + Parameters + ---------- + series + The ``series`` parameter to parse. + + Returns + ------- + series + A string to be passed to GMT, the sequence if the values must be passed via a + virtual file, or ``None`` if not given. + + Examples + -------- + >>> _parse_series() + >>> _parse_series(1) + '1' + >>> _parse_series("0/9/1") + '0/9/1' + >>> _parse_series([0, 9, 1]) + '0/9/1' + >>> _parse_series((0, 9, 1)) + '0/9/1' + >>> _parse_series([0, 1, 2, 3, 5]) + [0, 1, 2, 3, 5] + >>> import numpy as np + >>> _parse_series(np.arange(1, 10, 1)) + array([1, 2, 3, 4, 5, 6, 7, 8, 9]) + """ + if isinstance(series, (list, tuple)) and len(series) == 3: + return "/".join(str(item) for item in series) + if is_nonstr_iter(series): + return series + return str(series) if series is not None else None diff --git a/pygmt/src/histogram.py b/pygmt/src/histogram.py index b459628d654..a9b1f1e865e 100644 --- a/pygmt/src/histogram.py +++ b/pygmt/src/histogram.py @@ -13,10 +13,10 @@ build_arg_list, deprecate_parameter, fmt_docstring, - kwargs_to_strings, use_alias, ) from pygmt.params import Axis, Frame +from pygmt.src._common import _parse_series __doctest_skip__ = ["histogram"] @@ -27,7 +27,6 @@ @use_alias( D="annotate", N="distribution", - T="series", Z="histtype", b="binary", d="nodata", @@ -36,12 +35,12 @@ l="label", w="wrap", ) -@kwargs_to_strings(T="sequence") def histogram( self, data: PathLike | TableLike, bar_width: float | str | None = None, bar_offset: float | str | None = None, + series: float | str | Sequence[float] | None = None, cmap: str | bool = False, pen: str | None = None, fill: str | None = None, @@ -77,6 +76,7 @@ def histogram( - Q = cumulative - R = region - S = stairs + - T = series - V = verbose - W = pen - c = panel @@ -89,6 +89,15 @@ def histogram( data Pass in either a file name to an ASCII data table, a Python list, a 2-D $table_classes. + series + Set the histogram binning. It can take one of four forms: + + - A list/tuple of three values to set the minimum, maximum, and increment for + the binning. + - A single float value to set the bin increment only, with the minimum and + maximum automatically determined from the data or ``region``. + - A sequence of any other length to set the bin boundaries explicitly. + - A string for any GMT CLI syntax. $cmap pen Draw bar outline (or stair-case curve) using the specified pen thickness @@ -139,9 +148,6 @@ def histogram( Plot the histogram horizontally from x = 0 [Default is vertically from y = 0]. The plot dimensions remain the same, but the two axes are flipped, i.e., the x-axis is plotted vertically and the y-axis is plotted horizontally. - series : int, str, or list - [*min*\ /*max*\ /]\ *inc*\ [**+n**\ ]. - Set the interval for the width of each bar in the histogram. histtype : int or str [*type*][**+w**]. Choose between 6 types of histograms: @@ -186,7 +192,6 @@ def histogram( raise GMTParameterError( required="bar_width", reason="Required when 'bar_offset' is set." ) - aliasdict = AliasSystem( A=Alias(horizontal, name="horizontal"), C=Alias(cmap, name="cmap"), @@ -218,7 +223,15 @@ def histogram( self._activate_figure() with Session() as lib: - with lib.virtualfile_in(check_kind="vector", data=data) as vintbl: + with ( + lib.virtualfile_in(check_kind="vector", data=data) as vintbl, + lib.virtualfile_in( + check_kind="vector", + data=_parse_series(series), + required=False, + ) as vseries, + ): + aliasdict["T"] = vseries lib.call_module( module="histogram", args=build_arg_list(aliasdict, infile=vintbl) ) diff --git a/pygmt/tests/test_histogram.py b/pygmt/tests/test_histogram.py index c839105f995..b2cad33de0f 100644 --- a/pygmt/tests/test_histogram.py +++ b/pygmt/tests/test_histogram.py @@ -2,6 +2,7 @@ Test Figure.histogram. """ +import numpy as np import pandas as pd import pytest from pygmt import Figure @@ -36,6 +37,23 @@ def test_histogram(data): return fig +@pytest.mark.mpl_image_compare(filename="test_histogram.png") +def test_histogram_series_nparray(data): + """ + Test plotting a histogram with bin boundaries passed as a numpy array. + """ + fig = Figure() + fig.histogram( + data=data, + projection="X10c/10c", + region=[0, 9, 0, 6], + series=np.arange(0, 10), + frame=Axis(annot=True), + fill="green", + ) + return fig + + def test_histogram_baroffset(data): """ Test passing bar_offset requires bar_width.