Skip to content

Commit 998808e

Browse files
authored
Merge branch 'main' into doc/alias
2 parents 52476f8 + ebc2c8b commit 998808e

4 files changed

Lines changed: 268 additions & 40 deletions

File tree

‎doc/_static/style.css‎

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
/* To stick the footer to the bottom of the page */
2-
html {
3-
}
4-
51
body {
62
font-family: "Atkinson Hyperlegible", sans-serif;
73
font-size: 1.05em;
@@ -42,8 +38,9 @@ p {
4238
max-width: 1000px;
4339
}
4440

45-
/* Compatibility with sphinx_rtd_theme v3.x.x */
46-
/* https://github.com/TexasInstruments/processor-sdk-doc/commit/4fa6276ddcf7f9db919af2e5b3aec7e70a1afa16 */
41+
/* Workaround for the upstream bug that version section is not shown for self-hosted documentation */
42+
/* Upstream bug: https://github.com/readthedocs/sphinx_rtd_theme/issues/1624
43+
/* Workaround from https://github.com/TexasInstruments/processor-sdk-doc/commit/4fa6276ddcf7f9db919af2e5b3aec7e70a1afa16 */
4744
.wy-side-nav-search>div.version {
4845
margin-top: -.4045em;
4946
margin-bottom: .809em;
@@ -74,12 +71,12 @@ p {
7471
margin-bottom: 10px;
7572
}
7673

77-
/* Format parameters section similar to sphinx_rtd_theme v4.x.x (not a grid) */
74+
/* Format parameters section similar to sphinx_rtd_theme v0.4.x (not a grid) */
7875
html.writer-html5 .rst-content dl.field-list {
7976
display: initial;
8077
}
8178

82-
/* Add a grey box around parameters similar to sphinx_rtd_theme v4.x.x) */
79+
/* Add a grey box around parameters similar to sphinx_rtd_theme v0.4.x */
8380
.rst-content dl:not(.docutils) dl dt {
8481
margin-bottom: 4px;
8582
border: none;
@@ -100,20 +97,6 @@ html.writer-html5 .rst-content dl.field-list {
10097
position: relative;
10198
}
10299

103-
/* Remove the padding from the Parameters table */
104-
.rst-content table.field-list .field-name {
105-
padding-left: 0px;
106-
}
107-
108-
/* Lign up the Parameters section with the descriptions */
109-
.rst-content table.field-list td {
110-
padding-top: 8px;
111-
}
112-
113-
.rst-content .highlight > pre {
114-
font-size: 14px;
115-
}
116-
117100
.source-link {
118101
float: right;
119102
}

‎pygmt/src/histogram.py‎

Lines changed: 163 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
from typing import Literal
77

88
from pygmt._typing import PathLike, TableLike
9-
from pygmt.alias import Alias, AliasSystem
9+
from pygmt.alias import Alias, AliasSystem, _to_string
1010
from pygmt.clib import Session
11-
from pygmt.exceptions import GMTParameterError
11+
from pygmt.exceptions import GMTParameterError, GMTTypeError
1212
from pygmt.helpers import (
1313
build_arg_list,
14+
data_kind,
1415
deprecate_parameter,
1516
fmt_docstring,
17+
is_nonstr_iter,
1618
kwargs_to_strings,
1719
use_alias,
1820
)
@@ -70,13 +72,122 @@ def _alias_option_D( # ruff: ignore[invalid-function-name]
7072
]
7173

7274

75+
def _alias_option_N( # ruff: ignore[invalid-function-name]
76+
distribution=None, distribution_pen=None
77+
):
78+
"""
79+
Helper function to create the alias for the -N option.
80+
81+
The ``-N`` option may be repeated to draw several distribution curves, so
82+
``distribution`` also accepts a sequence of modes. ``distribution_pen`` is either a
83+
single pen, used for every curve, or one pen per curve.
84+
85+
Examples
86+
--------
87+
>>> def parse(**kwargs):
88+
... return build_arg_list(AliasSystem(N=_alias_option_N(**kwargs)))
89+
>>> parse()
90+
[]
91+
>>> # A single curve
92+
>>> parse(distribution="mean")
93+
['-N0']
94+
>>> parse(distribution="median", distribution_pen="1p,blue")
95+
['-N1+p1p,blue']
96+
97+
>>> # Multiple curves
98+
>>> parse(distribution=["mean", "lms"])
99+
['-N0', '-N2']
100+
101+
>>> # A single pen is used for every curve
102+
>>> parse(distribution=["mean", "lms"], distribution_pen="1p,red")
103+
['-N0+p1p,red', '-N2+p1p,red']
104+
105+
>>> # Several curves, each with its own pen
106+
>>> parse(
107+
... distribution=["mean", "median", "lms"],
108+
... distribution_pen=["1p,red", "1p,blue", "1p,green"],
109+
... )
110+
['-N0+p1p,red', '-N1+p1p,blue', '-N2+p1p,green']
111+
112+
>>> # A pen alone without distribution is ignored.
113+
>>> parse(distribution_pen="1p,red")
114+
[]
115+
116+
>>> # Backward compatibility: the legacy syntax combines the mode and the pen into
117+
>>> # a single string, and is passed through as is.
118+
>>> parse(distribution="0+p1p,blue")
119+
['-N0+p1p,blue']
120+
>>> parse(distribution=["0+p1p,blue", "1+p1p,red"])
121+
['-N0+p1p,blue', '-N1+p1p,red']
122+
>>> parse(distribution="+p1p,blue")
123+
['-N+p1p,blue']
124+
>>> parse(distribution="1")
125+
['-N1']
126+
>>> parse(distribution=True)
127+
['-N']
128+
129+
>>> # But the legacy syntax cannot be mixed with 'distribution_pen'.
130+
>>> parse(distribution="0+p1p,blue", distribution_pen="1p,red")
131+
Traceback (most recent call last):
132+
...
133+
pygmt.exceptions.GMTParameterError: Conflicting parameters: 'distribution_pen' ...
134+
135+
>>> parse(distribution="invalid")
136+
Traceback (most recent call last):
137+
...
138+
pygmt.exceptions.GMTValueError: Invalid value for parameter 'distribution': ...
139+
140+
>>> parse(distribution=["mean", "lms"], distribution_pen=["1p,red"])
141+
Traceback (most recent call last):
142+
...
143+
pygmt.exceptions.GMTParameterError: 'distribution_pen' must be a single pen or ...
144+
"""
145+
# Do nothing if distribution is not specified. Ignoring distribution_pen.
146+
if distribution is None:
147+
return Alias(None, name="distribution")
148+
149+
modes = distribution if is_nonstr_iter(distribution) else [distribution]
150+
# The legacy syntax gives the mode and the pen as a single string (e.g., "1+p1p,red"
151+
# or "+p1p,red"), or the mode as a string (e.g. "1"). Pass it as is.
152+
if any(isinstance(mode, str) and ("+" in mode or mode.isdigit()) for mode in modes):
153+
if distribution_pen is not None:
154+
raise GMTParameterError(
155+
conflicts_with=("distribution_pen", ["distribution"]),
156+
reason="'distribution' is using the legacy syntax.",
157+
)
158+
return Alias(distribution, name="distribution")
159+
160+
pens = (
161+
distribution_pen
162+
if is_nonstr_iter(distribution_pen)
163+
else [distribution_pen] * len(modes)
164+
)
165+
if len(pens) != len(modes):
166+
raise GMTParameterError(
167+
reason=(
168+
"'distribution_pen' must be a single pen or one pen per curve, but "
169+
f"got {len(pens)} pen(s) for {len(modes)} curve(s)."
170+
)
171+
)
172+
173+
values = []
174+
for mode, pen in zip(modes, pens, strict=True):
175+
_mode = _to_string(
176+
mode, mapping={"mean": 0, "median": 1, "lms": 2}, name="distribution"
177+
)
178+
_pen = _to_string(pen, prefix="+p", name="distribution_pen")
179+
if _mode is None: # e.g. distribution=False means no curve at all.
180+
continue
181+
values.append(_mode if _pen is None else f"{_mode}{_pen}")
182+
return Alias(values, name="distribution")
183+
184+
73185
@fmt_docstring
74186
# TODO(PyGMT>=0.22.0): Remove the deprecated "extreme" parameter.
75187
# TODO(PyGMT>=0.22.0): Remove the deprecated "annotate" parameter.
76188
@deprecate_parameter("extreme", "out_range", "0.20.0", remove_version="0.22.0")
77189
@deprecate_parameter("annotate", "annot", "0.20.0", remove_version="0.22.0")
78190
@use_alias(
79-
N="distribution",
80191
T="series",
81192
Z="histtype",
82193
b="binary",
@@ -90,6 +201,7 @@ def _alias_option_D( # ruff: ignore[invalid-function-name]
90201
def histogram(
91202
self,
92203
data: PathLike | TableLike,
204+
weights: bool | Sequence[float] = False,
93205
bar_width: float | str | None = None,
94206
bar_offset: float | str | None = None,
95207
cmap: str | bool = False,
@@ -105,6 +217,10 @@ def histogram(
105217
out_range: Literal["first", "last", "both"] | None = None,
106218
stairs: bool = False,
107219
cumulative: bool | Literal["reverse"] = False,
220+
distribution: Literal["mean", "median", "lms"]
221+
| Sequence[Literal["mean", "median", "lms"]]
222+
| None = None,
223+
distribution_pen: str | Sequence[str] | None = None,
108224
projection: str | None = None,
109225
region: Sequence[float | str] | str | None = None,
110226
frame: Frame | Axis | Literal["none"] | str | Sequence[str] | bool = False,
@@ -131,6 +247,7 @@ def histogram(
131247
- G = fill
132248
- J = projection
133249
- L = out_range
250+
- N = distribution, **+p**: distribution_pen
134251
- Q = cumulative
135252
- R = region
136253
- S = stairs
@@ -146,6 +263,14 @@ def histogram(
146263
data
147264
Pass in either a file name to an ASCII data table, a Python list, a 2-D
148265
$table_classes.
266+
weights
267+
Use weighted counts instead of pure counts [Default is ``False``, i.e., pure
268+
counts are used]. It can be:
269+
270+
- ``True``: Weights are provided in the second column of ``data``, if ``data``
271+
is a file name or a 2-D sequence.
272+
- A 1-D array of weights, one per data point, requiring that ``data`` is a 1-D
273+
sequence of values.
149274
$cmap
150275
pen
151276
Draw bar outline (or stair-case curve) using the specified pen thickness
@@ -177,15 +302,6 @@ def histogram(
177302
center
178303
Center bin on each value specified via ``series`` [Default uses the values to
179304
define the left edge of each bin].
180-
distribution : bool, float, or str
181-
[*mode*][**+p**\ *pen*].
182-
Draw the equivalent normal distribution; append desired
183-
*pen* [Default is ``"0.25p,black,solid"``].
184-
The *mode* selects which central location and scale to use:
185-
186-
* 0 = mean and standard deviation [Default];
187-
* 1 = median and L1 scale (1.4826 \* median absolute deviation; MAD);
188-
* 2 = LMS (least median of squares) mode and scale.
189305
out_range
190306
Handle values that fall outside the range set by ``series``. By default, these
191307
values are ignored. Valid values are:
@@ -207,8 +323,9 @@ def histogram(
207323
[*min*\ /*max*\ /]\ *inc*\ [**+n**\ ].
208324
Set the interval for the width of each bar in the histogram.
209325
histtype : int or str
210-
[*type*][**+w**].
211-
Choose between 6 types of histograms:
326+
[*type*].
327+
By default, pure counts are determined. To get weighted counts, use the
328+
``weights`` parameter. Choose between 6 types of histograms:
212329
213330
* 0 = counts [Default]
214331
* 1 = frequency_percent
@@ -217,8 +334,23 @@ def histogram(
217334
* 4 = log10 (1.0 + count)
218335
* 5 = log10 (1.0 + frequency_percent).
219336
220-
To use weights provided as a second data column instead of pure counts,
221-
append **+w**.
337+
distribution
338+
Draw the equivalent normal distribution. Select which central location and scale
339+
to use:
340+
341+
- ``"mean"``: mean and standard deviation
342+
- ``"median"``: median and L1 scale (1.4826 \* median absolute deviation)
343+
- ``"lms"``: least median of squares (LMS) mode and scale
344+
345+
Pass a sequence of modes to draw several curves at once, e.g.,
346+
``["mean", "lms"]``.
347+
348+
**Note**: If ``wrap`` is used, only ``"mean"`` is available and the circular
349+
von Mises distribution is determined instead.
350+
distribution_pen
351+
Pen used to draw the distribution curve [Default is ``"0.25p,black,solid"``].
352+
Pass a sequence of pens to use a different pen for each curve; a single pen is
353+
used for all of them. If ``distribution`` is not set, this parameter is ignored.
222354
$projection
223355
$region
224356
$frame
@@ -251,6 +383,16 @@ def histogram(
251383
required="bar_width", reason="Required when 'bar_offset' is set."
252384
)
253385

386+
# weights can be given as a 1-D array, or as a boolean to indicate that the second
387+
# column of data contains weights. If weights is an array, then data must be a 1-D
388+
# sequence of values.
389+
_weight_is_array = is_nonstr_iter(weights)
390+
if data_kind(data) == "file" and _weight_is_array:
391+
raise GMTTypeError(
392+
type(weights),
393+
reason="'weights' must be boolean when 'data' is a file name.",
394+
)
395+
254396
aliasdict = AliasSystem(
255397
A=Alias(horizontal, name="horizontal"),
256398
C=Alias(cmap, name="cmap"),
@@ -272,6 +414,7 @@ def histogram(
272414
name="out_range",
273415
mapping={"first": "l", "last": "h", "both": "b"},
274416
),
417+
N=_alias_option_N(distribution, distribution_pen),
275418
Q=Alias(cumulative, name="cumulative", mapping={"reverse": "r"}),
276419
S=Alias(stairs, name="stairs"),
277420
W=Alias(pen, name="pen"),
@@ -286,10 +429,13 @@ def histogram(
286429
t=transparency,
287430
)
288431
aliasdict.merge(kwargs)
432+
if weights is not False:
433+
aliasdict["Z"] = f"{aliasdict.get('Z', '')}+w"
289434

290435
self._activate_figure()
291436
with Session() as lib:
292-
with lib.virtualfile_in(check_kind="vector", data=data) as vintbl:
437+
vfargs = {"x": data, "y": weights} if _weight_is_array else {"data": data}
438+
with lib.virtualfile_in(check_kind="vector", **vfargs) as vintbl:
293439
lib.call_module(
294440
module="histogram", args=build_arg_list(aliasdict, infile=vintbl)
295441
)
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)