scales-python 1.4.0.9000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scales/__init__.py +295 -0
- scales/_colors.py +272 -0
- scales/_palettes_data.py +595 -0
- scales/_utils.py +579 -0
- scales/bounds.py +512 -0
- scales/breaks.py +627 -0
- scales/breaks_log.py +268 -0
- scales/colour_manip.py +681 -0
- scales/colour_mapping.py +593 -0
- scales/colour_ramp.py +126 -0
- scales/labels.py +2144 -0
- scales/minor_breaks.py +197 -0
- scales/palettes.py +1328 -0
- scales/py.typed +0 -0
- scales/range.py +223 -0
- scales/scale_continuous.py +146 -0
- scales/scale_discrete.py +196 -0
- scales/transforms.py +1338 -0
- scales_python-1.4.0.9000.dist-info/METADATA +73 -0
- scales_python-1.4.0.9000.dist-info/RECORD +22 -0
- scales_python-1.4.0.9000.dist-info/WHEEL +4 -0
- scales_python-1.4.0.9000.dist-info/licenses/LICENSE +3 -0
scales/labels.py
ADDED
|
@@ -0,0 +1,2144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Label formatting functions for scales.
|
|
3
|
+
|
|
4
|
+
Python port of label functions from the R scales package
|
|
5
|
+
(https://github.com/r-lib/scales). Each ``label_*`` function is a
|
|
6
|
+
closure factory: it returns a callable that accepts a sequence of
|
|
7
|
+
values and returns a ``list[str]`` of formatted labels.
|
|
8
|
+
|
|
9
|
+
Direct formatting functions (``number``, ``comma``, ``dollar``, etc.)
|
|
10
|
+
are also provided for one-shot use without creating a closure first.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
import textwrap
|
|
18
|
+
from datetime import datetime, timezone, timedelta
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
|
|
24
|
+
from ._utils import precision as _precision_util
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Closure factories
|
|
28
|
+
"label_number",
|
|
29
|
+
"label_comma",
|
|
30
|
+
"label_percent",
|
|
31
|
+
"label_dollar",
|
|
32
|
+
"label_currency",
|
|
33
|
+
"label_scientific",
|
|
34
|
+
"label_bytes",
|
|
35
|
+
"label_ordinal",
|
|
36
|
+
"label_pvalue",
|
|
37
|
+
"label_date",
|
|
38
|
+
"label_date_short",
|
|
39
|
+
"label_time",
|
|
40
|
+
"label_timespan",
|
|
41
|
+
"label_wrap",
|
|
42
|
+
"label_glue",
|
|
43
|
+
"label_parse",
|
|
44
|
+
"label_math",
|
|
45
|
+
"label_log",
|
|
46
|
+
"label_number_auto",
|
|
47
|
+
"label_number_si",
|
|
48
|
+
"label_dictionary",
|
|
49
|
+
"compose_label",
|
|
50
|
+
# Ordinal helpers
|
|
51
|
+
"ordinal_english",
|
|
52
|
+
"ordinal_french",
|
|
53
|
+
"ordinal_spanish",
|
|
54
|
+
# Direct formatting functions
|
|
55
|
+
"number",
|
|
56
|
+
"comma",
|
|
57
|
+
"dollar",
|
|
58
|
+
"percent",
|
|
59
|
+
"scientific",
|
|
60
|
+
"ordinal",
|
|
61
|
+
"pvalue",
|
|
62
|
+
# Core log formatting
|
|
63
|
+
"format_log",
|
|
64
|
+
# Scale cut helpers
|
|
65
|
+
"cut_short_scale",
|
|
66
|
+
"cut_long_scale",
|
|
67
|
+
"cut_time_scale",
|
|
68
|
+
"cut_si",
|
|
69
|
+
# Date utilities
|
|
70
|
+
"date_breaks",
|
|
71
|
+
"date_format",
|
|
72
|
+
"time_format",
|
|
73
|
+
# Legacy aliases
|
|
74
|
+
"comma_format",
|
|
75
|
+
"dollar_format",
|
|
76
|
+
"percent_format",
|
|
77
|
+
"scientific_format",
|
|
78
|
+
"ordinal_format",
|
|
79
|
+
"pvalue_format",
|
|
80
|
+
"number_format",
|
|
81
|
+
"number_bytes_format",
|
|
82
|
+
"number_bytes",
|
|
83
|
+
"parse_format",
|
|
84
|
+
"math_format",
|
|
85
|
+
"wrap_format",
|
|
86
|
+
"unit_format",
|
|
87
|
+
"format_format",
|
|
88
|
+
"number_options",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Internal helpers
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _precision(x: np.ndarray) -> float:
|
|
98
|
+
"""Auto-detect precision for a numeric array."""
|
|
99
|
+
return _precision_util(x)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _format_number(
|
|
103
|
+
value: float,
|
|
104
|
+
accuracy: float,
|
|
105
|
+
big_mark: Optional[str],
|
|
106
|
+
decimal_mark: str,
|
|
107
|
+
trim: bool,
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Format a single number with the given accuracy and marks."""
|
|
110
|
+
if not np.isfinite(value):
|
|
111
|
+
if np.isnan(value):
|
|
112
|
+
return "NaN"
|
|
113
|
+
return "Inf" if value > 0 else "-Inf"
|
|
114
|
+
|
|
115
|
+
# Number of decimal places from accuracy
|
|
116
|
+
if accuracy >= 1:
|
|
117
|
+
ndigits = 0
|
|
118
|
+
else:
|
|
119
|
+
ndigits = max(0, -int(math.floor(math.log10(accuracy) - 1e-12)))
|
|
120
|
+
|
|
121
|
+
rounded = round(value, ndigits)
|
|
122
|
+
|
|
123
|
+
# Format with fixed decimals
|
|
124
|
+
formatted = f"{rounded:.{ndigits}f}"
|
|
125
|
+
|
|
126
|
+
if trim and ndigits > 0:
|
|
127
|
+
# Strip trailing zeros after decimal point
|
|
128
|
+
formatted = formatted.rstrip("0").rstrip(".")
|
|
129
|
+
|
|
130
|
+
# Apply big_mark (thousands separator)
|
|
131
|
+
if big_mark:
|
|
132
|
+
parts = formatted.split(".")
|
|
133
|
+
int_part = parts[0]
|
|
134
|
+
# Handle negative sign
|
|
135
|
+
sign = ""
|
|
136
|
+
if int_part.startswith("-"):
|
|
137
|
+
sign = "-"
|
|
138
|
+
int_part = int_part[1:]
|
|
139
|
+
# Insert separators from right
|
|
140
|
+
groups: list[str] = []
|
|
141
|
+
while len(int_part) > 3:
|
|
142
|
+
groups.append(int_part[-3:])
|
|
143
|
+
int_part = int_part[:-3]
|
|
144
|
+
groups.append(int_part)
|
|
145
|
+
int_part = big_mark.join(reversed(groups))
|
|
146
|
+
parts[0] = sign + int_part
|
|
147
|
+
formatted = ".".join(parts)
|
|
148
|
+
|
|
149
|
+
# Apply decimal_mark
|
|
150
|
+
if decimal_mark and decimal_mark != ".":
|
|
151
|
+
formatted = formatted.replace(".", decimal_mark, 1)
|
|
152
|
+
|
|
153
|
+
return formatted
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _apply_style(
|
|
157
|
+
formatted: str,
|
|
158
|
+
value: float,
|
|
159
|
+
style_positive: str,
|
|
160
|
+
style_negative: str,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Apply positive/negative styling to a formatted number string."""
|
|
163
|
+
if np.isnan(value):
|
|
164
|
+
return formatted
|
|
165
|
+
|
|
166
|
+
if value > 0:
|
|
167
|
+
if style_positive == "plus":
|
|
168
|
+
formatted = "+" + formatted
|
|
169
|
+
elif style_positive == "space":
|
|
170
|
+
formatted = " " + formatted
|
|
171
|
+
# "none" → no change
|
|
172
|
+
elif value < 0:
|
|
173
|
+
# The formatted string already has a '-' from Python formatting.
|
|
174
|
+
# We may need to replace it.
|
|
175
|
+
if style_negative == "minus":
|
|
176
|
+
formatted = formatted.replace("-", "\u2212", 1)
|
|
177
|
+
elif style_negative == "parens":
|
|
178
|
+
formatted = "(" + formatted.replace("-", "", 1) + ")"
|
|
179
|
+
# "hyphen" → keep the ASCII hyphen-minus (default)
|
|
180
|
+
|
|
181
|
+
return formatted
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _apply_scale_cut(
|
|
185
|
+
values: np.ndarray,
|
|
186
|
+
scale_cut: list[tuple[float, str]],
|
|
187
|
+
) -> tuple[np.ndarray, list[str]]:
|
|
188
|
+
"""
|
|
189
|
+
Apply scale_cut to values. Returns (scaled_values, suffixes).
|
|
190
|
+
|
|
191
|
+
Each entry in *scale_cut* is ``(threshold, suffix)``. Values are
|
|
192
|
+
divided by the largest threshold they exceed.
|
|
193
|
+
"""
|
|
194
|
+
# Sort scale_cut by threshold ascending
|
|
195
|
+
sc = sorted(scale_cut, key=lambda t: t[0])
|
|
196
|
+
scaled = values.copy()
|
|
197
|
+
suffixes: list[str] = []
|
|
198
|
+
|
|
199
|
+
for v_idx in range(len(values)):
|
|
200
|
+
val = values[v_idx]
|
|
201
|
+
chosen_suffix = ""
|
|
202
|
+
chosen_divisor = 1.0
|
|
203
|
+
for threshold, sfx in sc:
|
|
204
|
+
if threshold == 0:
|
|
205
|
+
chosen_suffix = sfx
|
|
206
|
+
chosen_divisor = 1.0
|
|
207
|
+
elif abs(val) >= threshold:
|
|
208
|
+
chosen_suffix = sfx
|
|
209
|
+
chosen_divisor = threshold
|
|
210
|
+
scaled[v_idx] = val / chosen_divisor
|
|
211
|
+
suffixes.append(chosen_suffix)
|
|
212
|
+
|
|
213
|
+
return scaled, suffixes
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Scale-cut helpers
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cut_short_scale(space: bool = False) -> list[tuple[float, str]]:
|
|
222
|
+
"""
|
|
223
|
+
Short scale suffixes: K, M, B, T.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
space : bool, optional
|
|
228
|
+
If ``True``, prepend a space before the suffix (default ``False``).
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
list of (float, str)
|
|
233
|
+
Scale-cut specification.
|
|
234
|
+
"""
|
|
235
|
+
sp = " " if space else ""
|
|
236
|
+
return [
|
|
237
|
+
(0, ""),
|
|
238
|
+
(1e3, f"{sp}K"),
|
|
239
|
+
(1e6, f"{sp}M"),
|
|
240
|
+
(1e9, f"{sp}B"),
|
|
241
|
+
(1e12, f"{sp}T"),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def cut_long_scale(space: bool = False) -> list[tuple[float, str]]:
|
|
246
|
+
"""
|
|
247
|
+
Long scale suffixes: K, M, B, T at 10^3, 10^6, 10^12, 10^18.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
space : bool, optional
|
|
252
|
+
If ``True``, prepend a space before the suffix (default ``False``).
|
|
253
|
+
|
|
254
|
+
Returns
|
|
255
|
+
-------
|
|
256
|
+
list of (float, str)
|
|
257
|
+
Scale-cut specification.
|
|
258
|
+
"""
|
|
259
|
+
sp = " " if space else ""
|
|
260
|
+
return [
|
|
261
|
+
(0, ""),
|
|
262
|
+
(1e3, f"{sp}K"),
|
|
263
|
+
(1e6, f"{sp}M"),
|
|
264
|
+
(1e12, f"{sp}B"),
|
|
265
|
+
(1e18, f"{sp}T"),
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cut_time_scale(space: bool = False) -> list[tuple[float, str]]:
|
|
270
|
+
"""
|
|
271
|
+
Time scale suffixes: ns, us, ms, s, m, h, d, w.
|
|
272
|
+
|
|
273
|
+
Values are assumed to be in seconds.
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
space : bool, optional
|
|
278
|
+
If ``True``, prepend a space before the suffix (default ``False``).
|
|
279
|
+
|
|
280
|
+
Returns
|
|
281
|
+
-------
|
|
282
|
+
list of (float, str)
|
|
283
|
+
Scale-cut specification.
|
|
284
|
+
"""
|
|
285
|
+
sp = " " if space else ""
|
|
286
|
+
# R uses "\u03BCs" (Greek small mu + s) when UTF-8 is available —
|
|
287
|
+
# Python assumes UTF-8 everywhere, so emit it unconditionally.
|
|
288
|
+
return [
|
|
289
|
+
(0, ""),
|
|
290
|
+
(1e-9, f"{sp}ns"),
|
|
291
|
+
(1e-6, f"{sp}\u03bcs"),
|
|
292
|
+
(1e-3, f"{sp}ms"),
|
|
293
|
+
(1, f"{sp}s"),
|
|
294
|
+
(60, f"{sp}m"),
|
|
295
|
+
(3600, f"{sp}h"),
|
|
296
|
+
(86400, f"{sp}d"),
|
|
297
|
+
(604800, f"{sp}w"),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def cut_si(unit: str) -> list[tuple[float, str]]:
|
|
302
|
+
"""
|
|
303
|
+
Full SI prefix scale cuts from yocto (10^-24) to yotta (10^24).
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
unit : str
|
|
308
|
+
Base unit string to append after the SI prefix.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
list of (float, str)
|
|
313
|
+
Scale-cut specification.
|
|
314
|
+
"""
|
|
315
|
+
prefixes = [
|
|
316
|
+
(1e-24, "y"),
|
|
317
|
+
(1e-21, "z"),
|
|
318
|
+
(1e-18, "a"),
|
|
319
|
+
(1e-15, "f"),
|
|
320
|
+
(1e-12, "p"),
|
|
321
|
+
(1e-9, "n"),
|
|
322
|
+
(1e-6, "\u00b5"), # micro sign
|
|
323
|
+
(1e-3, "m"),
|
|
324
|
+
(1, ""),
|
|
325
|
+
(1e3, "k"),
|
|
326
|
+
(1e6, "M"),
|
|
327
|
+
(1e9, "G"),
|
|
328
|
+
(1e12, "T"),
|
|
329
|
+
(1e15, "P"),
|
|
330
|
+
(1e18, "E"),
|
|
331
|
+
(1e21, "Z"),
|
|
332
|
+
(1e24, "Y"),
|
|
333
|
+
]
|
|
334
|
+
return [(0, "")] + [(val, f" {pfx}{unit}") for val, pfx in prefixes]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Core direct formatting function: number()
|
|
339
|
+
# ---------------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def number(
|
|
343
|
+
x: ArrayLike,
|
|
344
|
+
accuracy: Optional[float] = None,
|
|
345
|
+
scale: float = 1,
|
|
346
|
+
prefix: str = "",
|
|
347
|
+
suffix: str = "",
|
|
348
|
+
big_mark: Optional[str] = None,
|
|
349
|
+
decimal_mark: Optional[str] = None,
|
|
350
|
+
style_positive: Optional[str] = None,
|
|
351
|
+
style_negative: Optional[str] = None,
|
|
352
|
+
scale_cut: Optional[list[tuple[float, str]]] = None,
|
|
353
|
+
trim: bool = True,
|
|
354
|
+
) -> list[str]:
|
|
355
|
+
"""
|
|
356
|
+
Format a numeric vector.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
x : array-like
|
|
361
|
+
Numeric values to format.
|
|
362
|
+
accuracy : float, optional
|
|
363
|
+
Rounding precision. ``None`` for auto-detect.
|
|
364
|
+
scale : float, optional
|
|
365
|
+
Multiplicative scaling factor (default 1).
|
|
366
|
+
prefix : str, optional
|
|
367
|
+
String prepended to each label.
|
|
368
|
+
suffix : str, optional
|
|
369
|
+
String appended to each label.
|
|
370
|
+
big_mark : str, optional
|
|
371
|
+
Thousands separator. ``None`` means no separator.
|
|
372
|
+
decimal_mark : str, optional
|
|
373
|
+
Decimal separator. ``None`` defaults to ``"."``.
|
|
374
|
+
style_positive : str, optional
|
|
375
|
+
Treatment of positive values: ``"none"``, ``"plus"``, or
|
|
376
|
+
``"space"`` (default ``"none"``).
|
|
377
|
+
style_negative : str, optional
|
|
378
|
+
Treatment of negative values: ``"hyphen"`` (ASCII ``-``),
|
|
379
|
+
``"minus"`` (Unicode minus ``\u2212``), or ``"parens"``
|
|
380
|
+
(default ``"hyphen"``).
|
|
381
|
+
scale_cut : list of (float, str), optional
|
|
382
|
+
SI-style suffix specification (see :func:`cut_short_scale`).
|
|
383
|
+
trim : bool, optional
|
|
384
|
+
Strip trailing zeros (default ``True``).
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
list of str
|
|
389
|
+
Formatted strings.
|
|
390
|
+
"""
|
|
391
|
+
x_arr = np.asarray(x, dtype=float)
|
|
392
|
+
x_scaled = x_arr * scale
|
|
393
|
+
|
|
394
|
+
# Resolve defaults from the module-level option store
|
|
395
|
+
# (`number_options()` — mirrors R's getOption("scales.*")). Python
|
|
396
|
+
# keeps an empty big_mark default so label output round-trips
|
|
397
|
+
# through float(); R uses " " instead. Users can opt in via
|
|
398
|
+
# `number_options(big_mark=" ")`.
|
|
399
|
+
if big_mark is None:
|
|
400
|
+
big_mark = str(_NUMBER_OPTIONS.get("big_mark", ""))
|
|
401
|
+
if decimal_mark is None:
|
|
402
|
+
decimal_mark = str(_NUMBER_OPTIONS.get("decimal_mark", "."))
|
|
403
|
+
if style_positive is None:
|
|
404
|
+
style_positive = str(_NUMBER_OPTIONS.get("style_positive", "none"))
|
|
405
|
+
if style_negative is None:
|
|
406
|
+
style_negative = str(_NUMBER_OPTIONS.get("style_negative", "hyphen"))
|
|
407
|
+
|
|
408
|
+
# Apply scale_cut
|
|
409
|
+
per_value_suffix: Optional[list[str]] = None
|
|
410
|
+
if scale_cut is not None:
|
|
411
|
+
x_scaled, per_value_suffix = _apply_scale_cut(x_scaled, scale_cut)
|
|
412
|
+
|
|
413
|
+
if accuracy is None:
|
|
414
|
+
accuracy = _precision(x_scaled)
|
|
415
|
+
|
|
416
|
+
results: list[str] = []
|
|
417
|
+
for i, val in enumerate(x_scaled.flat):
|
|
418
|
+
fmt = _format_number(val, accuracy, big_mark, decimal_mark, trim)
|
|
419
|
+
fmt = _apply_style(fmt, val, style_positive, style_negative)
|
|
420
|
+
sc_sfx = per_value_suffix[i] if per_value_suffix is not None else ""
|
|
421
|
+
results.append(f"{prefix}{fmt}{sc_sfx}{suffix}")
|
|
422
|
+
|
|
423
|
+
return results
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ---------------------------------------------------------------------------
|
|
427
|
+
# Closure factory: label_number
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def label_number(
|
|
432
|
+
accuracy: Optional[float] = None,
|
|
433
|
+
scale: float = 1,
|
|
434
|
+
prefix: str = "",
|
|
435
|
+
suffix: str = "",
|
|
436
|
+
big_mark: Optional[str] = None,
|
|
437
|
+
decimal_mark: Optional[str] = None,
|
|
438
|
+
style_positive: str = "none",
|
|
439
|
+
style_negative: str = "hyphen",
|
|
440
|
+
scale_cut: Optional[list[tuple[float, str]]] = None,
|
|
441
|
+
trim: bool = True,
|
|
442
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
443
|
+
"""
|
|
444
|
+
Label numbers with flexible formatting.
|
|
445
|
+
|
|
446
|
+
Returns a closure that formats numeric values according to the
|
|
447
|
+
parameters captured at construction time.
|
|
448
|
+
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
accuracy : float, optional
|
|
452
|
+
Rounding precision. ``None`` for auto-detect.
|
|
453
|
+
scale : float, optional
|
|
454
|
+
Multiplicative scaling factor (default 1).
|
|
455
|
+
prefix : str, optional
|
|
456
|
+
Prepended to each label.
|
|
457
|
+
suffix : str, optional
|
|
458
|
+
Appended to each label.
|
|
459
|
+
big_mark : str, optional
|
|
460
|
+
Thousands separator.
|
|
461
|
+
decimal_mark : str, optional
|
|
462
|
+
Decimal separator.
|
|
463
|
+
style_positive : str, optional
|
|
464
|
+
``"none"``, ``"plus"``, or ``"space"``.
|
|
465
|
+
style_negative : str, optional
|
|
466
|
+
``"hyphen"``, ``"minus"``, or ``"parens"``.
|
|
467
|
+
scale_cut : list of (float, str), optional
|
|
468
|
+
SI-style suffix specification.
|
|
469
|
+
trim : bool, optional
|
|
470
|
+
Strip trailing zeros.
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
callable
|
|
475
|
+
``(x) -> list[str]``
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
479
|
+
return number(
|
|
480
|
+
x,
|
|
481
|
+
accuracy=accuracy,
|
|
482
|
+
scale=scale,
|
|
483
|
+
prefix=prefix,
|
|
484
|
+
suffix=suffix,
|
|
485
|
+
big_mark=big_mark,
|
|
486
|
+
decimal_mark=decimal_mark,
|
|
487
|
+
style_positive=style_positive,
|
|
488
|
+
style_negative=style_negative,
|
|
489
|
+
scale_cut=scale_cut,
|
|
490
|
+
trim=trim,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
return formatter
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
# label_comma / comma
|
|
498
|
+
# ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def label_comma(**kwargs: Any) -> Callable[[ArrayLike], list[str]]:
|
|
502
|
+
"""
|
|
503
|
+
Label numbers with comma as thousands separator.
|
|
504
|
+
|
|
505
|
+
Parameters
|
|
506
|
+
----------
|
|
507
|
+
**kwargs
|
|
508
|
+
Passed to :func:`label_number`. ``big_mark`` defaults to ``","``.
|
|
509
|
+
|
|
510
|
+
Returns
|
|
511
|
+
-------
|
|
512
|
+
callable
|
|
513
|
+
``(x) -> list[str]``
|
|
514
|
+
"""
|
|
515
|
+
kwargs.setdefault("big_mark", ",")
|
|
516
|
+
return label_number(**kwargs)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def comma(x: ArrayLike, **kwargs: Any) -> list[str]:
|
|
520
|
+
"""Format *x* with comma thousands separator."""
|
|
521
|
+
kwargs.setdefault("big_mark", ",")
|
|
522
|
+
return number(x, **kwargs)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
# label_percent / percent
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def label_percent(
|
|
531
|
+
accuracy: Optional[float] = None,
|
|
532
|
+
scale: float = 100,
|
|
533
|
+
suffix: str = "%",
|
|
534
|
+
**kwargs: Any,
|
|
535
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
536
|
+
"""
|
|
537
|
+
Label percentages.
|
|
538
|
+
|
|
539
|
+
Parameters
|
|
540
|
+
----------
|
|
541
|
+
accuracy : float, optional
|
|
542
|
+
Rounding precision.
|
|
543
|
+
scale : float, optional
|
|
544
|
+
Multiplicative factor (default 100 converts proportions to %).
|
|
545
|
+
suffix : str, optional
|
|
546
|
+
Appended string (default ``"%"``).
|
|
547
|
+
**kwargs
|
|
548
|
+
Passed to :func:`label_number`.
|
|
549
|
+
|
|
550
|
+
Returns
|
|
551
|
+
-------
|
|
552
|
+
callable
|
|
553
|
+
``(x) -> list[str]``
|
|
554
|
+
"""
|
|
555
|
+
return label_number(accuracy=accuracy, scale=scale, suffix=suffix, **kwargs)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def percent(x: ArrayLike, accuracy: Optional[float] = None, scale: float = 100,
|
|
559
|
+
suffix: str = "%", **kwargs: Any) -> list[str]:
|
|
560
|
+
"""Format *x* as percentages."""
|
|
561
|
+
return number(x, accuracy=accuracy, scale=scale, suffix=suffix, **kwargs)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# ---------------------------------------------------------------------------
|
|
565
|
+
# label_dollar / label_currency / dollar
|
|
566
|
+
# ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _needs_cents(x: np.ndarray, threshold: float) -> bool:
|
|
570
|
+
"""Mirror R's `needs_cents`: decide whether an auto-accuracy pass
|
|
571
|
+
should use 0.01 (cents) or 1 (whole units).
|
|
572
|
+
|
|
573
|
+
* Empty / all-NaN → False
|
|
574
|
+
* Max |x| > threshold → False (values too large; skip fractional)
|
|
575
|
+
* Otherwise → True iff **any** finite value is non-integer.
|
|
576
|
+
"""
|
|
577
|
+
x = np.asarray(x, dtype=float)
|
|
578
|
+
finite = x[np.isfinite(x)]
|
|
579
|
+
if finite.size == 0:
|
|
580
|
+
return False
|
|
581
|
+
if np.nanmax(np.abs(finite)) > threshold:
|
|
582
|
+
return False
|
|
583
|
+
return bool(np.any(finite != np.floor(finite)))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def dollar(
|
|
587
|
+
x: ArrayLike,
|
|
588
|
+
accuracy: Optional[float] = None,
|
|
589
|
+
scale: float = 1,
|
|
590
|
+
prefix: Optional[str] = None,
|
|
591
|
+
suffix: Optional[str] = None,
|
|
592
|
+
big_mark: Optional[str] = None,
|
|
593
|
+
decimal_mark: Optional[str] = None,
|
|
594
|
+
trim: bool = True,
|
|
595
|
+
largest_with_cents: float = 100000,
|
|
596
|
+
style_negative: Optional[str] = None,
|
|
597
|
+
scale_cut: Optional[list[tuple[float, str]]] = None,
|
|
598
|
+
**kwargs: Any,
|
|
599
|
+
) -> list[str]:
|
|
600
|
+
"""Format *x* as currency.
|
|
601
|
+
|
|
602
|
+
Matches R's ``dollar``:
|
|
603
|
+
|
|
604
|
+
* Currency-specific defaults fall through ``_NUMBER_OPTIONS``
|
|
605
|
+
(``currency_prefix``, ``currency_suffix``, ``currency_big_mark``,
|
|
606
|
+
``currency_decimal_mark``). The baked-in fallbacks are ``"$"``,
|
|
607
|
+
``""``, ``","``, ``"."`` respectively.
|
|
608
|
+
* When ``accuracy`` is ``None`` *and* no ``scale_cut`` is given, the
|
|
609
|
+
accuracy is chosen by the ``largest_with_cents`` heuristic: use
|
|
610
|
+
``0.01`` when ``max(|x * scale|) <= largest_with_cents`` *and* any
|
|
611
|
+
input has a fractional part; otherwise ``1``.
|
|
612
|
+
* When ``big_mark == decimal_mark == ","``, ``big_mark`` is swapped
|
|
613
|
+
to a space to avoid ambiguity.
|
|
614
|
+
"""
|
|
615
|
+
if prefix is None:
|
|
616
|
+
prefix = str(_NUMBER_OPTIONS.get("currency_prefix", "$"))
|
|
617
|
+
if suffix is None:
|
|
618
|
+
suffix = str(_NUMBER_OPTIONS.get("currency_suffix", ""))
|
|
619
|
+
if big_mark is None:
|
|
620
|
+
big_mark = str(_NUMBER_OPTIONS.get("currency_big_mark", ","))
|
|
621
|
+
if decimal_mark is None:
|
|
622
|
+
decimal_mark = str(_NUMBER_OPTIONS.get("currency_decimal_mark", "."))
|
|
623
|
+
|
|
624
|
+
x_arr = np.asarray(x, dtype=float)
|
|
625
|
+
if x_arr.size == 0:
|
|
626
|
+
return []
|
|
627
|
+
|
|
628
|
+
if accuracy is None and scale_cut is None:
|
|
629
|
+
if _needs_cents(x_arr * scale, largest_with_cents):
|
|
630
|
+
accuracy = 0.01
|
|
631
|
+
else:
|
|
632
|
+
accuracy = 1
|
|
633
|
+
|
|
634
|
+
if big_mark == "," and decimal_mark == ",":
|
|
635
|
+
big_mark = " "
|
|
636
|
+
|
|
637
|
+
return number(
|
|
638
|
+
x_arr,
|
|
639
|
+
accuracy=accuracy,
|
|
640
|
+
scale=scale,
|
|
641
|
+
prefix=prefix,
|
|
642
|
+
suffix=suffix,
|
|
643
|
+
big_mark=big_mark,
|
|
644
|
+
decimal_mark=decimal_mark,
|
|
645
|
+
trim=trim,
|
|
646
|
+
style_negative=style_negative,
|
|
647
|
+
scale_cut=scale_cut,
|
|
648
|
+
**kwargs,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def label_currency(
|
|
653
|
+
accuracy: Optional[float] = None,
|
|
654
|
+
scale: float = 1,
|
|
655
|
+
prefix: Optional[str] = None,
|
|
656
|
+
suffix: Optional[str] = None,
|
|
657
|
+
big_mark: Optional[str] = None,
|
|
658
|
+
decimal_mark: Optional[str] = None,
|
|
659
|
+
trim: bool = True,
|
|
660
|
+
largest_with_fractional: float = 100000,
|
|
661
|
+
**kwargs: Any,
|
|
662
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
663
|
+
"""
|
|
664
|
+
Label currency values.
|
|
665
|
+
|
|
666
|
+
Thin closure around :func:`dollar` — matches R's ``label_currency``
|
|
667
|
+
wrapping ``dollar(..., largest_with_cents = largest_with_fractional)``.
|
|
668
|
+
|
|
669
|
+
Parameters
|
|
670
|
+
----------
|
|
671
|
+
accuracy : float, optional
|
|
672
|
+
Fixed rounding precision. When ``None`` (default), accuracy is
|
|
673
|
+
auto-detected via the ``largest_with_fractional`` heuristic.
|
|
674
|
+
largest_with_fractional : float, optional
|
|
675
|
+
Threshold above which fractional accuracy is suppressed
|
|
676
|
+
(default ``100000``). See :func:`dollar`.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
680
|
+
return dollar(
|
|
681
|
+
x,
|
|
682
|
+
accuracy=accuracy,
|
|
683
|
+
scale=scale,
|
|
684
|
+
prefix=prefix,
|
|
685
|
+
suffix=suffix,
|
|
686
|
+
big_mark=big_mark,
|
|
687
|
+
decimal_mark=decimal_mark,
|
|
688
|
+
trim=trim,
|
|
689
|
+
largest_with_cents=largest_with_fractional,
|
|
690
|
+
**kwargs,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
return formatter
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def label_dollar(
|
|
697
|
+
accuracy: Optional[float] = None,
|
|
698
|
+
scale: float = 1,
|
|
699
|
+
prefix: Optional[str] = None,
|
|
700
|
+
suffix: Optional[str] = None,
|
|
701
|
+
big_mark: Optional[str] = None,
|
|
702
|
+
decimal_mark: Optional[str] = None,
|
|
703
|
+
trim: bool = True,
|
|
704
|
+
largest_with_cents: float = 100000,
|
|
705
|
+
**kwargs: Any,
|
|
706
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
707
|
+
"""Label currency values (superseded alias of :func:`label_currency`)."""
|
|
708
|
+
|
|
709
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
710
|
+
return dollar(
|
|
711
|
+
x,
|
|
712
|
+
accuracy=accuracy,
|
|
713
|
+
scale=scale,
|
|
714
|
+
prefix=prefix,
|
|
715
|
+
suffix=suffix,
|
|
716
|
+
big_mark=big_mark,
|
|
717
|
+
decimal_mark=decimal_mark,
|
|
718
|
+
trim=trim,
|
|
719
|
+
largest_with_cents=largest_with_cents,
|
|
720
|
+
**kwargs,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
return formatter
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
# label_scientific / scientific
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _format_scientific_single(
|
|
732
|
+
value: float,
|
|
733
|
+
digits: int,
|
|
734
|
+
decimal_mark: str,
|
|
735
|
+
trim: bool,
|
|
736
|
+
) -> str:
|
|
737
|
+
"""Format a single value in scientific notation."""
|
|
738
|
+
if not np.isfinite(value):
|
|
739
|
+
if np.isnan(value):
|
|
740
|
+
return "NaN"
|
|
741
|
+
return "Inf" if value > 0 else "-Inf"
|
|
742
|
+
|
|
743
|
+
if value == 0:
|
|
744
|
+
if trim:
|
|
745
|
+
return "0"
|
|
746
|
+
return f"0.{'0' * (digits - 1)}e+00"
|
|
747
|
+
|
|
748
|
+
exp = int(math.floor(math.log10(abs(value))))
|
|
749
|
+
coeff = value / (10.0 ** exp)
|
|
750
|
+
# Round coefficient
|
|
751
|
+
coeff = round(coeff, digits - 1)
|
|
752
|
+
|
|
753
|
+
# Format coefficient
|
|
754
|
+
ndecimals = max(0, digits - 1)
|
|
755
|
+
coeff_str = f"{coeff:.{ndecimals}f}"
|
|
756
|
+
|
|
757
|
+
if trim and ndecimals > 0:
|
|
758
|
+
coeff_str = coeff_str.rstrip("0").rstrip(".")
|
|
759
|
+
|
|
760
|
+
if decimal_mark != ".":
|
|
761
|
+
coeff_str = coeff_str.replace(".", decimal_mark, 1)
|
|
762
|
+
|
|
763
|
+
# Format exponent
|
|
764
|
+
exp_sign = "+" if exp >= 0 else "-"
|
|
765
|
+
exp_str = f"{abs(exp):02d}"
|
|
766
|
+
|
|
767
|
+
return f"{coeff_str}e{exp_sign}{exp_str}"
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def scientific(
|
|
771
|
+
x: ArrayLike,
|
|
772
|
+
digits: int = 3,
|
|
773
|
+
scale: float = 1,
|
|
774
|
+
prefix: str = "",
|
|
775
|
+
suffix: str = "",
|
|
776
|
+
decimal_mark: Optional[str] = None,
|
|
777
|
+
trim: bool = True,
|
|
778
|
+
) -> list[str]:
|
|
779
|
+
"""
|
|
780
|
+
Format *x* in scientific notation.
|
|
781
|
+
|
|
782
|
+
Parameters
|
|
783
|
+
----------
|
|
784
|
+
x : array-like
|
|
785
|
+
Numeric values.
|
|
786
|
+
digits : int, optional
|
|
787
|
+
Significant digits (default 3).
|
|
788
|
+
scale : float, optional
|
|
789
|
+
Multiplicative factor.
|
|
790
|
+
prefix : str, optional
|
|
791
|
+
Prepended string.
|
|
792
|
+
suffix : str, optional
|
|
793
|
+
Appended string.
|
|
794
|
+
decimal_mark : str, optional
|
|
795
|
+
Decimal separator.
|
|
796
|
+
trim : bool, optional
|
|
797
|
+
Strip trailing zeros.
|
|
798
|
+
|
|
799
|
+
Returns
|
|
800
|
+
-------
|
|
801
|
+
list of str
|
|
802
|
+
"""
|
|
803
|
+
x_arr = np.asarray(x, dtype=float) * scale
|
|
804
|
+
if decimal_mark is None:
|
|
805
|
+
decimal_mark = "."
|
|
806
|
+
|
|
807
|
+
results: list[str] = []
|
|
808
|
+
for val in x_arr.flat:
|
|
809
|
+
fmt = _format_scientific_single(val, digits, decimal_mark, trim)
|
|
810
|
+
results.append(f"{prefix}{fmt}{suffix}")
|
|
811
|
+
return results
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def label_scientific(
|
|
815
|
+
digits: int = 3,
|
|
816
|
+
scale: float = 1,
|
|
817
|
+
prefix: str = "",
|
|
818
|
+
suffix: str = "",
|
|
819
|
+
decimal_mark: Optional[str] = None,
|
|
820
|
+
trim: bool = True,
|
|
821
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
822
|
+
"""
|
|
823
|
+
Label numbers in scientific notation.
|
|
824
|
+
|
|
825
|
+
Parameters
|
|
826
|
+
----------
|
|
827
|
+
digits : int, optional
|
|
828
|
+
Significant digits (default 3).
|
|
829
|
+
scale : float, optional
|
|
830
|
+
Multiplicative factor.
|
|
831
|
+
prefix : str, optional
|
|
832
|
+
Prepended string.
|
|
833
|
+
suffix : str, optional
|
|
834
|
+
Appended string.
|
|
835
|
+
decimal_mark : str, optional
|
|
836
|
+
Decimal separator.
|
|
837
|
+
trim : bool, optional
|
|
838
|
+
Strip trailing zeros.
|
|
839
|
+
|
|
840
|
+
Returns
|
|
841
|
+
-------
|
|
842
|
+
callable
|
|
843
|
+
``(x) -> list[str]``
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
847
|
+
return scientific(
|
|
848
|
+
x, digits=digits, scale=scale, prefix=prefix,
|
|
849
|
+
suffix=suffix, decimal_mark=decimal_mark, trim=trim,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
return formatter
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
# ---------------------------------------------------------------------------
|
|
856
|
+
# label_bytes
|
|
857
|
+
# ---------------------------------------------------------------------------
|
|
858
|
+
|
|
859
|
+
_SI_BYTE_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
860
|
+
_BINARY_BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def label_bytes(
|
|
864
|
+
units: str = "auto_si",
|
|
865
|
+
accuracy: float = 1,
|
|
866
|
+
scale: float = 1,
|
|
867
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
868
|
+
"""
|
|
869
|
+
Label byte sizes (e.g. 1 kB, 2 MiB).
|
|
870
|
+
|
|
871
|
+
Parameters
|
|
872
|
+
----------
|
|
873
|
+
units : str, optional
|
|
874
|
+
``"auto_si"`` (powers of 1000), ``"auto_binary"`` (powers of 1024),
|
|
875
|
+
or an explicit unit name like ``"kB"`` or ``"MiB"``.
|
|
876
|
+
accuracy : float, optional
|
|
877
|
+
Rounding precision (default 1).
|
|
878
|
+
scale : float, optional
|
|
879
|
+
Multiplicative factor applied before formatting.
|
|
880
|
+
|
|
881
|
+
Returns
|
|
882
|
+
-------
|
|
883
|
+
callable
|
|
884
|
+
``(x) -> list[str]``
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
888
|
+
x_arr = np.asarray(x, dtype=float) * scale
|
|
889
|
+
results: list[str] = []
|
|
890
|
+
|
|
891
|
+
for val in x_arr.flat:
|
|
892
|
+
if not np.isfinite(val):
|
|
893
|
+
results.append("NaN" if np.isnan(val) else ("Inf" if val > 0 else "-Inf"))
|
|
894
|
+
continue
|
|
895
|
+
|
|
896
|
+
if units == "auto_si":
|
|
897
|
+
base = 1000
|
|
898
|
+
unit_list = _SI_BYTE_UNITS
|
|
899
|
+
idx = 0
|
|
900
|
+
abs_val = abs(val)
|
|
901
|
+
while abs_val >= base and idx < len(unit_list) - 1:
|
|
902
|
+
abs_val /= base
|
|
903
|
+
idx += 1
|
|
904
|
+
divisor = base ** idx
|
|
905
|
+
scaled_val = val / divisor if divisor else val
|
|
906
|
+
unit_str = unit_list[idx]
|
|
907
|
+
elif units == "auto_binary":
|
|
908
|
+
base = 1024
|
|
909
|
+
unit_list = _BINARY_BYTE_UNITS
|
|
910
|
+
idx = 0
|
|
911
|
+
abs_val = abs(val)
|
|
912
|
+
while abs_val >= base and idx < len(unit_list) - 1:
|
|
913
|
+
abs_val /= base
|
|
914
|
+
idx += 1
|
|
915
|
+
divisor = base ** idx
|
|
916
|
+
scaled_val = val / divisor if divisor else val
|
|
917
|
+
unit_str = unit_list[idx]
|
|
918
|
+
else:
|
|
919
|
+
# Explicit unit
|
|
920
|
+
unit_str = units
|
|
921
|
+
if units in _SI_BYTE_UNITS:
|
|
922
|
+
idx = _SI_BYTE_UNITS.index(units)
|
|
923
|
+
divisor = 1000 ** idx
|
|
924
|
+
elif units in _BINARY_BYTE_UNITS:
|
|
925
|
+
idx = _BINARY_BYTE_UNITS.index(units)
|
|
926
|
+
divisor = 1024 ** idx
|
|
927
|
+
else:
|
|
928
|
+
divisor = 1
|
|
929
|
+
scaled_val = val / divisor if divisor else val
|
|
930
|
+
|
|
931
|
+
fmt = _format_number(scaled_val, accuracy, None, ".", True)
|
|
932
|
+
results.append(f"{fmt} {unit_str}")
|
|
933
|
+
|
|
934
|
+
return results
|
|
935
|
+
|
|
936
|
+
return formatter
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
# ---------------------------------------------------------------------------
|
|
940
|
+
# Ordinal helpers
|
|
941
|
+
# ---------------------------------------------------------------------------
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
class OrdinalRules(list):
|
|
945
|
+
"""R-style ordinal rule set: an ordered list of ``(suffix, regex)``.
|
|
946
|
+
|
|
947
|
+
Iterating yields ``(suffix, pattern)`` pairs in priority order — this
|
|
948
|
+
mirrors R's ``ordinal_english()`` return value (a named list of
|
|
949
|
+
regex strings where the **name** is the suffix and the **value** is
|
|
950
|
+
the pattern).
|
|
951
|
+
|
|
952
|
+
For backwards compatibility, instances are *also callable*: calling
|
|
953
|
+
with an integer returns the first matching suffix (Python's
|
|
954
|
+
historical API).
|
|
955
|
+
"""
|
|
956
|
+
|
|
957
|
+
__slots__ = ()
|
|
958
|
+
|
|
959
|
+
def __call__(self, n: Any) -> str:
|
|
960
|
+
import re as _re
|
|
961
|
+
s = str(int(n))
|
|
962
|
+
for suffix, pattern in self:
|
|
963
|
+
if _re.search(pattern, s):
|
|
964
|
+
return suffix
|
|
965
|
+
return ""
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def ordinal_english() -> OrdinalRules:
|
|
969
|
+
"""
|
|
970
|
+
Return the English ordinal rule set.
|
|
971
|
+
|
|
972
|
+
Mirrors R's ``ordinal_english``: a list of ``(suffix, regex)`` pairs
|
|
973
|
+
applied in order, first match wins. Handles the 11/12/13 quirk via
|
|
974
|
+
lookbehind assertions.
|
|
975
|
+
|
|
976
|
+
Returns
|
|
977
|
+
-------
|
|
978
|
+
OrdinalRules
|
|
979
|
+
Priority-ordered ``[(suffix, pattern), ...]``, also callable as
|
|
980
|
+
``rules(n) -> suffix``.
|
|
981
|
+
"""
|
|
982
|
+
return OrdinalRules([
|
|
983
|
+
("st", r"(?<!1)1$"),
|
|
984
|
+
("nd", r"(?<!1)2$"),
|
|
985
|
+
("rd", r"(?<!1)3$"),
|
|
986
|
+
("th", r"(?<=1)[123]$"),
|
|
987
|
+
("th", r"[0456789]$"),
|
|
988
|
+
("th", r"."),
|
|
989
|
+
])
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def ordinal_french(
|
|
993
|
+
gender: str = "masculin",
|
|
994
|
+
plural: bool = False,
|
|
995
|
+
) -> OrdinalRules:
|
|
996
|
+
"""
|
|
997
|
+
Return the French ordinal rule set.
|
|
998
|
+
|
|
999
|
+
Mirrors R's ``ordinal_french``: only ``1`` gets the masculine/feminine
|
|
1000
|
+
first-position suffix (``"er"`` / ``"re"``); everything else gets
|
|
1001
|
+
``"e"``. When ``plural`` is ``True``, both suffixes receive a
|
|
1002
|
+
trailing ``"s"``.
|
|
1003
|
+
|
|
1004
|
+
Parameters
|
|
1005
|
+
----------
|
|
1006
|
+
gender : str, optional
|
|
1007
|
+
``"masculin"`` or ``"feminin"`` (default ``"masculin"``).
|
|
1008
|
+
plural : bool, optional
|
|
1009
|
+
Use plural forms (default ``False``).
|
|
1010
|
+
|
|
1011
|
+
Returns
|
|
1012
|
+
-------
|
|
1013
|
+
OrdinalRules
|
|
1014
|
+
"""
|
|
1015
|
+
if gender not in ("masculin", "feminin"):
|
|
1016
|
+
raise ValueError("gender must be 'masculin' or 'feminin'")
|
|
1017
|
+
first = "er" if gender == "masculin" else "re"
|
|
1018
|
+
rest = "e"
|
|
1019
|
+
if plural:
|
|
1020
|
+
first += "s"
|
|
1021
|
+
rest += "s"
|
|
1022
|
+
return OrdinalRules([(first, r"^1$"), (rest, r".")])
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def ordinal_spanish() -> OrdinalRules:
|
|
1026
|
+
"""
|
|
1027
|
+
Return the Spanish ordinal rule set.
|
|
1028
|
+
|
|
1029
|
+
Mirrors R's ``ordinal_spanish``: a single rule with suffix ``".º"``
|
|
1030
|
+
matching every number.
|
|
1031
|
+
"""
|
|
1032
|
+
return OrdinalRules([(".\u00ba", r".")])
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# ---------------------------------------------------------------------------
|
|
1036
|
+
# label_ordinal / ordinal
|
|
1037
|
+
# ---------------------------------------------------------------------------
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def ordinal(
|
|
1041
|
+
x: ArrayLike,
|
|
1042
|
+
prefix: str = "",
|
|
1043
|
+
suffix: str = "",
|
|
1044
|
+
big_mark: Optional[str] = None,
|
|
1045
|
+
rules: Optional[Any] = None,
|
|
1046
|
+
) -> list[str]:
|
|
1047
|
+
"""
|
|
1048
|
+
Format *x* as ordinals (1st, 2nd, 3rd, ...).
|
|
1049
|
+
|
|
1050
|
+
Parameters
|
|
1051
|
+
----------
|
|
1052
|
+
x : array-like
|
|
1053
|
+
Numeric values (rounded to integers before formatting).
|
|
1054
|
+
prefix : str, optional
|
|
1055
|
+
Prepended string.
|
|
1056
|
+
suffix : str, optional
|
|
1057
|
+
Appended string.
|
|
1058
|
+
big_mark : str, optional
|
|
1059
|
+
Thousands separator.
|
|
1060
|
+
rules : :class:`OrdinalRules` or callable, optional
|
|
1061
|
+
Suffix rule set. May be:
|
|
1062
|
+
|
|
1063
|
+
* An :class:`OrdinalRules` instance (R-style list of
|
|
1064
|
+
``(suffix, regex)``) — first match wins.
|
|
1065
|
+
* Any iterable of ``(suffix, regex)`` pairs — same behaviour.
|
|
1066
|
+
* A plain callable ``(int) -> str``.
|
|
1067
|
+
* ``None`` — defaults to :func:`ordinal_english` as in R.
|
|
1068
|
+
|
|
1069
|
+
Returns
|
|
1070
|
+
-------
|
|
1071
|
+
list of str
|
|
1072
|
+
"""
|
|
1073
|
+
import re as _re
|
|
1074
|
+
|
|
1075
|
+
if rules is None:
|
|
1076
|
+
rules = ordinal_english()
|
|
1077
|
+
|
|
1078
|
+
# Normalise the rule set into a callable that maps int -> suffix.
|
|
1079
|
+
if callable(rules) and not isinstance(rules, (list, tuple)):
|
|
1080
|
+
rule_fn = rules
|
|
1081
|
+
else:
|
|
1082
|
+
rule_list = list(rules)
|
|
1083
|
+
|
|
1084
|
+
def rule_fn(n: int) -> str:
|
|
1085
|
+
s = str(int(n))
|
|
1086
|
+
for sfx, pat in rule_list:
|
|
1087
|
+
if _re.search(pat, s):
|
|
1088
|
+
return sfx
|
|
1089
|
+
return ""
|
|
1090
|
+
|
|
1091
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1092
|
+
results: list[str] = []
|
|
1093
|
+
|
|
1094
|
+
for val in x_arr.flat:
|
|
1095
|
+
if not np.isfinite(val):
|
|
1096
|
+
results.append("NaN" if np.isnan(val) else ("Inf" if val > 0 else "-Inf"))
|
|
1097
|
+
continue
|
|
1098
|
+
int_val = int(round(val))
|
|
1099
|
+
num_str = _format_number(float(int_val), 1, big_mark, ".", True)
|
|
1100
|
+
ord_suffix = rule_fn(int_val)
|
|
1101
|
+
results.append(f"{prefix}{num_str}{ord_suffix}{suffix}")
|
|
1102
|
+
|
|
1103
|
+
return results
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def label_ordinal(
|
|
1107
|
+
prefix: str = "",
|
|
1108
|
+
suffix: str = "",
|
|
1109
|
+
big_mark: Optional[str] = None,
|
|
1110
|
+
rules: Optional[Callable[[int], str]] = None,
|
|
1111
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1112
|
+
"""
|
|
1113
|
+
Label numbers as ordinals.
|
|
1114
|
+
|
|
1115
|
+
Parameters
|
|
1116
|
+
----------
|
|
1117
|
+
prefix : str, optional
|
|
1118
|
+
Prepended string.
|
|
1119
|
+
suffix : str, optional
|
|
1120
|
+
Appended string.
|
|
1121
|
+
big_mark : str, optional
|
|
1122
|
+
Thousands separator.
|
|
1123
|
+
rules : callable, optional
|
|
1124
|
+
Ordinal suffix function (default :func:`ordinal_english`).
|
|
1125
|
+
|
|
1126
|
+
Returns
|
|
1127
|
+
-------
|
|
1128
|
+
callable
|
|
1129
|
+
``(x) -> list[str]``
|
|
1130
|
+
"""
|
|
1131
|
+
|
|
1132
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1133
|
+
return ordinal(x, prefix=prefix, suffix=suffix,
|
|
1134
|
+
big_mark=big_mark, rules=rules)
|
|
1135
|
+
|
|
1136
|
+
return formatter
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
# ---------------------------------------------------------------------------
|
|
1140
|
+
# label_pvalue / pvalue
|
|
1141
|
+
# ---------------------------------------------------------------------------
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def pvalue(
|
|
1145
|
+
x: ArrayLike,
|
|
1146
|
+
accuracy: float = 0.001,
|
|
1147
|
+
decimal_mark: Optional[str] = None,
|
|
1148
|
+
prefix: Optional[list[str]] = None,
|
|
1149
|
+
add_p: bool = False,
|
|
1150
|
+
) -> list[str]:
|
|
1151
|
+
"""
|
|
1152
|
+
Format p-values.
|
|
1153
|
+
|
|
1154
|
+
Parameters
|
|
1155
|
+
----------
|
|
1156
|
+
x : array-like
|
|
1157
|
+
P-values to format.
|
|
1158
|
+
accuracy : float, optional
|
|
1159
|
+
Smallest displayable value (default 0.001).
|
|
1160
|
+
decimal_mark : str, optional
|
|
1161
|
+
Decimal separator.
|
|
1162
|
+
prefix : list of str, optional
|
|
1163
|
+
Length-3 list ``[less_than, normal, greater_than]``.
|
|
1164
|
+
Defaults to ``["<", "", ">"]``.
|
|
1165
|
+
add_p : bool, optional
|
|
1166
|
+
If ``True``, prepend ``"p"`` or ``"p "`` to each label.
|
|
1167
|
+
|
|
1168
|
+
Returns
|
|
1169
|
+
-------
|
|
1170
|
+
list of str
|
|
1171
|
+
"""
|
|
1172
|
+
# Mirrors R's pvalue: prefix default is ("p<","p=","p>") if add_p else
|
|
1173
|
+
# ("<","",">"). The prefix is prepended verbatim — *no* added spaces.
|
|
1174
|
+
if prefix is None:
|
|
1175
|
+
prefix_list = ["p<", "p=", "p>"] if add_p else ["<", "", ">"]
|
|
1176
|
+
else:
|
|
1177
|
+
prefix_list = list(prefix)
|
|
1178
|
+
if len(prefix_list) != 3:
|
|
1179
|
+
raise ValueError("prefix must be a length-3 sequence")
|
|
1180
|
+
|
|
1181
|
+
if decimal_mark is None:
|
|
1182
|
+
decimal_mark = str(_NUMBER_OPTIONS.get("decimal_mark", "."))
|
|
1183
|
+
|
|
1184
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1185
|
+
results: list[str] = []
|
|
1186
|
+
|
|
1187
|
+
for val in x_arr.flat:
|
|
1188
|
+
if not np.isfinite(val):
|
|
1189
|
+
results.append("NaN" if np.isnan(val) else str(val))
|
|
1190
|
+
continue
|
|
1191
|
+
|
|
1192
|
+
if val < accuracy:
|
|
1193
|
+
fmt = _format_number(accuracy, accuracy, None, decimal_mark, True)
|
|
1194
|
+
s = f"{prefix_list[0]}{fmt}"
|
|
1195
|
+
elif val > 1 - accuracy:
|
|
1196
|
+
fmt = _format_number(1 - accuracy, accuracy, None, decimal_mark, True)
|
|
1197
|
+
s = f"{prefix_list[2]}{fmt}"
|
|
1198
|
+
else:
|
|
1199
|
+
fmt = _format_number(val, accuracy, None, decimal_mark, False)
|
|
1200
|
+
s = f"{prefix_list[1]}{fmt}"
|
|
1201
|
+
|
|
1202
|
+
results.append(s)
|
|
1203
|
+
|
|
1204
|
+
return results
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def label_pvalue(
|
|
1208
|
+
accuracy: float = 0.001,
|
|
1209
|
+
decimal_mark: Optional[str] = None,
|
|
1210
|
+
prefix: Optional[list[str]] = None,
|
|
1211
|
+
add_p: bool = False,
|
|
1212
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1213
|
+
"""
|
|
1214
|
+
Label p-values.
|
|
1215
|
+
|
|
1216
|
+
Parameters
|
|
1217
|
+
----------
|
|
1218
|
+
accuracy : float, optional
|
|
1219
|
+
Smallest displayable value (default 0.001).
|
|
1220
|
+
decimal_mark : str, optional
|
|
1221
|
+
Decimal separator.
|
|
1222
|
+
prefix : list of str, optional
|
|
1223
|
+
Length-3 list ``[less_than, normal, greater_than]``.
|
|
1224
|
+
add_p : bool, optional
|
|
1225
|
+
Prepend ``"p"`` to labels.
|
|
1226
|
+
|
|
1227
|
+
Returns
|
|
1228
|
+
-------
|
|
1229
|
+
callable
|
|
1230
|
+
``(x) -> list[str]``
|
|
1231
|
+
"""
|
|
1232
|
+
|
|
1233
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1234
|
+
return pvalue(x, accuracy=accuracy, decimal_mark=decimal_mark,
|
|
1235
|
+
prefix=prefix, add_p=add_p)
|
|
1236
|
+
|
|
1237
|
+
return formatter
|
|
1238
|
+
|
|
1239
|
+
|
|
1240
|
+
# ---------------------------------------------------------------------------
|
|
1241
|
+
# Date / time labels
|
|
1242
|
+
# ---------------------------------------------------------------------------
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
def _to_datetime(val: Any, tz_obj: Any) -> Optional[datetime]:
|
|
1246
|
+
"""Convert a value to a datetime, handling numpy datetime64 etc."""
|
|
1247
|
+
if isinstance(val, datetime):
|
|
1248
|
+
return val.astimezone(tz_obj) if val.tzinfo else val.replace(tzinfo=tz_obj)
|
|
1249
|
+
if isinstance(val, np.datetime64):
|
|
1250
|
+
# Convert to Python datetime via timestamp
|
|
1251
|
+
ts = (val - np.datetime64("1970-01-01T00:00:00")) / np.timedelta64(1, "s")
|
|
1252
|
+
return datetime.fromtimestamp(float(ts), tz=tz_obj)
|
|
1253
|
+
if isinstance(val, (int, float)):
|
|
1254
|
+
if not np.isfinite(val):
|
|
1255
|
+
return None
|
|
1256
|
+
return datetime.fromtimestamp(float(val), tz=tz_obj)
|
|
1257
|
+
return None
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _make_tz(tz: str) -> timezone:
|
|
1261
|
+
"""Create a timezone from a string. Supports 'UTC' and offset forms."""
|
|
1262
|
+
if tz.upper() == "UTC":
|
|
1263
|
+
return timezone.utc
|
|
1264
|
+
# Simple offset parsing for common cases
|
|
1265
|
+
m = re.match(r"^UTC([+-])(\d{1,2}):?(\d{2})?$", tz, re.IGNORECASE)
|
|
1266
|
+
if m:
|
|
1267
|
+
sign = 1 if m.group(1) == "+" else -1
|
|
1268
|
+
hours = int(m.group(2))
|
|
1269
|
+
minutes = int(m.group(3) or 0)
|
|
1270
|
+
return timezone(timedelta(hours=sign * hours, minutes=sign * minutes))
|
|
1271
|
+
# Fallback: try as UTC
|
|
1272
|
+
return timezone.utc
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def label_date(
|
|
1276
|
+
format: str = "%Y-%m-%d",
|
|
1277
|
+
tz: str = "UTC",
|
|
1278
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1279
|
+
"""
|
|
1280
|
+
Label dates.
|
|
1281
|
+
|
|
1282
|
+
Parameters
|
|
1283
|
+
----------
|
|
1284
|
+
format : str, optional
|
|
1285
|
+
``strftime``-compatible format string (default ``"%Y-%m-%d"``).
|
|
1286
|
+
tz : str, optional
|
|
1287
|
+
Timezone (default ``"UTC"``).
|
|
1288
|
+
|
|
1289
|
+
Returns
|
|
1290
|
+
-------
|
|
1291
|
+
callable
|
|
1292
|
+
``(x) -> list[str]``
|
|
1293
|
+
"""
|
|
1294
|
+
tz_obj = _make_tz(tz)
|
|
1295
|
+
|
|
1296
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1297
|
+
results: list[str] = []
|
|
1298
|
+
x_arr = np.asarray(x)
|
|
1299
|
+
for val in x_arr.flat:
|
|
1300
|
+
dt = _to_datetime(val, tz_obj)
|
|
1301
|
+
if dt is None:
|
|
1302
|
+
results.append("NA")
|
|
1303
|
+
else:
|
|
1304
|
+
results.append(dt.strftime(format))
|
|
1305
|
+
return results
|
|
1306
|
+
|
|
1307
|
+
return formatter
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
def label_date_short(
|
|
1311
|
+
format: Optional[Sequence[Optional[str]]] = None,
|
|
1312
|
+
sep: str = "\n",
|
|
1313
|
+
leading: str = "0",
|
|
1314
|
+
tz: str = "UTC",
|
|
1315
|
+
locale: Optional[str] = None,
|
|
1316
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1317
|
+
"""
|
|
1318
|
+
Label dates compactly, showing each component only when it changes.
|
|
1319
|
+
|
|
1320
|
+
Faithful port of R's ``label_date_short``:
|
|
1321
|
+
|
|
1322
|
+
* ``format`` is a length-4 vector of ``strftime`` codes for
|
|
1323
|
+
``(year, month, day, hour)``. Default
|
|
1324
|
+
``("%Y", "%b", "%d", "%H:%M")``.
|
|
1325
|
+
* A component is rendered for a given date only if that component
|
|
1326
|
+
(or any *larger* component) differs from the previous date — i.e.
|
|
1327
|
+
``cumsum(changed(component)) >= 1`` per R.
|
|
1328
|
+
* Components that are *always* zero / first-of-period across the
|
|
1329
|
+
whole input are trimmed from the smallest up (e.g. the hour line
|
|
1330
|
+
is dropped when every value is at 00:00).
|
|
1331
|
+
* ``leading`` controls the character replacing a leading ``"0"`` in
|
|
1332
|
+
each rendered component. ``"0"`` keeps the zero (default);
|
|
1333
|
+
``""`` removes it; ``"\\u2007"`` is a typographic figure space.
|
|
1334
|
+
|
|
1335
|
+
Parameters
|
|
1336
|
+
----------
|
|
1337
|
+
format : sequence of 4 str, optional
|
|
1338
|
+
``(year_fmt, month_fmt, day_fmt, hour_fmt)``.
|
|
1339
|
+
sep : str, optional
|
|
1340
|
+
Separator between rendered components (default newline).
|
|
1341
|
+
leading : str, optional
|
|
1342
|
+
Replacement for each component's leading ``"0"`` digit
|
|
1343
|
+
(default ``"0"``, i.e. no replacement).
|
|
1344
|
+
tz : str, optional
|
|
1345
|
+
Timezone name (default ``"UTC"``).
|
|
1346
|
+
locale : str, optional
|
|
1347
|
+
Locale name. Not implemented for month/day names (Python's
|
|
1348
|
+
``strftime`` respects ``LC_TIME`` at the OS level); accepted for
|
|
1349
|
+
API parity.
|
|
1350
|
+
|
|
1351
|
+
Returns
|
|
1352
|
+
-------
|
|
1353
|
+
callable
|
|
1354
|
+
``(x) -> list[str]``
|
|
1355
|
+
"""
|
|
1356
|
+
_ = locale # accepted for API parity; OS-level locale is used
|
|
1357
|
+
tz_obj = _make_tz(tz)
|
|
1358
|
+
default_format = ["%Y", "%b", "%d", "%H:%M"]
|
|
1359
|
+
fmt = list(default_format) if format is None else list(format)
|
|
1360
|
+
if len(fmt) != 4:
|
|
1361
|
+
raise ValueError("format must be length 4 (year, month, day, hour)")
|
|
1362
|
+
|
|
1363
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1364
|
+
x_arr = np.asarray(x)
|
|
1365
|
+
dts: list[Optional[datetime]] = [
|
|
1366
|
+
_to_datetime(v, tz_obj) for v in x_arr.flat
|
|
1367
|
+
]
|
|
1368
|
+
|
|
1369
|
+
n = len(dts)
|
|
1370
|
+
if n == 0:
|
|
1371
|
+
return []
|
|
1372
|
+
|
|
1373
|
+
# Extract year/month/day/hour/minute arrays; None for NA.
|
|
1374
|
+
year = [d.year if d is not None else None for d in dts]
|
|
1375
|
+
month = [d.month if d is not None else None for d in dts]
|
|
1376
|
+
day = [d.day if d is not None else None for d in dts]
|
|
1377
|
+
hour = [d.hour if d is not None else None for d in dts]
|
|
1378
|
+
minute = [d.minute if d is not None else None for d in dts]
|
|
1379
|
+
|
|
1380
|
+
# changed[i]: True if i-th value differs from (i-1)-th, always
|
|
1381
|
+
# True at i==0 or when either neighbour is NA. Matches R's
|
|
1382
|
+
# `changed <- function(x) c(TRUE, is.na(x[-1]) | x[-1] != x[-1])`.
|
|
1383
|
+
def _changed(vals: list[Any]) -> list[bool]:
|
|
1384
|
+
out = [True] * n
|
|
1385
|
+
for i in range(1, n):
|
|
1386
|
+
if vals[i] is None or vals[i - 1] is None:
|
|
1387
|
+
out[i] = True
|
|
1388
|
+
else:
|
|
1389
|
+
out[i] = vals[i] != vals[i - 1]
|
|
1390
|
+
return out
|
|
1391
|
+
|
|
1392
|
+
ch_year = _changed(year)
|
|
1393
|
+
ch_month = _changed(month)
|
|
1394
|
+
ch_day = _changed(day)
|
|
1395
|
+
|
|
1396
|
+
# R's `cumsum(changes) >= 1` ensures that once a larger unit
|
|
1397
|
+
# changes, all smaller units are re-shown for that row.
|
|
1398
|
+
cum_year = [c for c in ch_year]
|
|
1399
|
+
cum_month = [(cy or cm) for cy, cm in zip(cum_year, ch_month)]
|
|
1400
|
+
cum_day = [(cym or cd) for cym, cd in zip(cum_month, ch_day)]
|
|
1401
|
+
|
|
1402
|
+
# Decide which *positions* are worth ever showing (the "firsts"
|
|
1403
|
+
# trim). Matches R's nested if-block.
|
|
1404
|
+
show_hour = not all(
|
|
1405
|
+
(h == 0 and m == 0) for h, m in zip(hour, minute) if h is not None
|
|
1406
|
+
)
|
|
1407
|
+
show_day = show_hour or not all(
|
|
1408
|
+
d == 1 for d in day if d is not None
|
|
1409
|
+
)
|
|
1410
|
+
show_month = show_day or not all(
|
|
1411
|
+
mo == 1 for mo in month if mo is not None
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
fmt_year = fmt[0]
|
|
1415
|
+
fmt_month = fmt[1] if show_month else None
|
|
1416
|
+
fmt_day = fmt[2] if show_day else None
|
|
1417
|
+
fmt_hour = fmt[3] if show_hour else None
|
|
1418
|
+
|
|
1419
|
+
def _rstrip_leading(s: str) -> str:
|
|
1420
|
+
if leading == "0":
|
|
1421
|
+
return s
|
|
1422
|
+
# Replace a leading "0" digit in the whole string, and any
|
|
1423
|
+
# "0" directly after a separator (matches R's gsub).
|
|
1424
|
+
out = re.sub(r"^0", leading, s)
|
|
1425
|
+
if sep:
|
|
1426
|
+
out = out.replace(sep + "0", sep + leading)
|
|
1427
|
+
return out
|
|
1428
|
+
|
|
1429
|
+
results: list[str] = []
|
|
1430
|
+
for i, dt in enumerate(dts):
|
|
1431
|
+
if dt is None:
|
|
1432
|
+
results.append("NA")
|
|
1433
|
+
continue
|
|
1434
|
+
|
|
1435
|
+
parts: list[str] = []
|
|
1436
|
+
if fmt_hour is not None:
|
|
1437
|
+
parts.append(dt.strftime(fmt_hour))
|
|
1438
|
+
if fmt_day is not None and cum_day[i]:
|
|
1439
|
+
parts.append(dt.strftime(fmt_day))
|
|
1440
|
+
if fmt_month is not None and cum_month[i]:
|
|
1441
|
+
parts.append(dt.strftime(fmt_month))
|
|
1442
|
+
if cum_year[i]:
|
|
1443
|
+
parts.append(dt.strftime(fmt_year))
|
|
1444
|
+
|
|
1445
|
+
# R builds the matrix with smallest-first then reverses when
|
|
1446
|
+
# joining — i.e. largest unit first visually. We built
|
|
1447
|
+
# smallest-first too, so reverse here.
|
|
1448
|
+
parts = list(reversed(parts))
|
|
1449
|
+
results.append(_rstrip_leading(sep.join(parts)))
|
|
1450
|
+
|
|
1451
|
+
return results
|
|
1452
|
+
|
|
1453
|
+
return formatter
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def label_time(
|
|
1457
|
+
format: str = "%H:%M:%S",
|
|
1458
|
+
tz: str = "UTC",
|
|
1459
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1460
|
+
"""
|
|
1461
|
+
Label times.
|
|
1462
|
+
|
|
1463
|
+
Parameters
|
|
1464
|
+
----------
|
|
1465
|
+
format : str, optional
|
|
1466
|
+
``strftime``-compatible format string (default ``"%H:%M:%S"``).
|
|
1467
|
+
tz : str, optional
|
|
1468
|
+
Timezone (default ``"UTC"``).
|
|
1469
|
+
|
|
1470
|
+
Returns
|
|
1471
|
+
-------
|
|
1472
|
+
callable
|
|
1473
|
+
``(x) -> list[str]``
|
|
1474
|
+
"""
|
|
1475
|
+
tz_obj = _make_tz(tz)
|
|
1476
|
+
|
|
1477
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1478
|
+
results: list[str] = []
|
|
1479
|
+
x_arr = np.asarray(x)
|
|
1480
|
+
for val in x_arr.flat:
|
|
1481
|
+
dt = _to_datetime(val, tz_obj)
|
|
1482
|
+
if dt is None:
|
|
1483
|
+
results.append("NA")
|
|
1484
|
+
else:
|
|
1485
|
+
results.append(dt.strftime(format))
|
|
1486
|
+
return results
|
|
1487
|
+
|
|
1488
|
+
return formatter
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def label_timespan(
|
|
1492
|
+
unit: str = "secs",
|
|
1493
|
+
space: bool = False,
|
|
1494
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1495
|
+
"""
|
|
1496
|
+
Label timespans with human-friendly units.
|
|
1497
|
+
|
|
1498
|
+
Parameters
|
|
1499
|
+
----------
|
|
1500
|
+
unit : str, optional
|
|
1501
|
+
Input unit: ``"secs"``, ``"mins"``, ``"hours"``, ``"days"``,
|
|
1502
|
+
``"weeks"`` (default ``"secs"``).
|
|
1503
|
+
space : bool, optional
|
|
1504
|
+
Insert space between number and unit (default ``False``).
|
|
1505
|
+
|
|
1506
|
+
Returns
|
|
1507
|
+
-------
|
|
1508
|
+
callable
|
|
1509
|
+
``(x) -> list[str]``
|
|
1510
|
+
"""
|
|
1511
|
+
unit_seconds: dict[str, float] = {
|
|
1512
|
+
"secs": 1,
|
|
1513
|
+
"mins": 60,
|
|
1514
|
+
"hours": 3600,
|
|
1515
|
+
"days": 86400,
|
|
1516
|
+
"weeks": 604800,
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
# Unicode \u03bc mirrors R's cut_time_scale when UTF-8 is active.
|
|
1520
|
+
_thresholds = [
|
|
1521
|
+
(604800, "w"),
|
|
1522
|
+
(86400, "d"),
|
|
1523
|
+
(3600, "h"),
|
|
1524
|
+
(60, "m"),
|
|
1525
|
+
(1, "s"),
|
|
1526
|
+
(1e-3, "ms"),
|
|
1527
|
+
(1e-6, "\u03bcs"),
|
|
1528
|
+
(1e-9, "ns"),
|
|
1529
|
+
]
|
|
1530
|
+
|
|
1531
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1532
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1533
|
+
scale_factor = unit_seconds.get(unit, 1)
|
|
1534
|
+
x_secs = x_arr * scale_factor
|
|
1535
|
+
sp = " " if space else ""
|
|
1536
|
+
|
|
1537
|
+
results: list[str] = []
|
|
1538
|
+
for val in x_secs.flat:
|
|
1539
|
+
if not np.isfinite(val):
|
|
1540
|
+
results.append("NaN" if np.isnan(val) else str(val))
|
|
1541
|
+
continue
|
|
1542
|
+
|
|
1543
|
+
if val == 0:
|
|
1544
|
+
results.append(f"0{sp}s")
|
|
1545
|
+
continue
|
|
1546
|
+
|
|
1547
|
+
abs_val = abs(val)
|
|
1548
|
+
chosen_div = 1.0
|
|
1549
|
+
chosen_unit = "s"
|
|
1550
|
+
for threshold, u in _thresholds:
|
|
1551
|
+
if abs_val >= threshold:
|
|
1552
|
+
chosen_div = threshold
|
|
1553
|
+
chosen_unit = u
|
|
1554
|
+
break
|
|
1555
|
+
else:
|
|
1556
|
+
# Smaller than 1 ns
|
|
1557
|
+
chosen_div = 1e-9
|
|
1558
|
+
chosen_unit = "ns"
|
|
1559
|
+
|
|
1560
|
+
scaled = val / chosen_div
|
|
1561
|
+
fmt = _format_number(scaled, _precision(np.array([scaled])), None, ".", True)
|
|
1562
|
+
results.append(f"{fmt}{sp}{chosen_unit}")
|
|
1563
|
+
|
|
1564
|
+
return results
|
|
1565
|
+
|
|
1566
|
+
return formatter
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
# ---------------------------------------------------------------------------
|
|
1570
|
+
# label_wrap
|
|
1571
|
+
# ---------------------------------------------------------------------------
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def label_wrap(width: int) -> Callable[[ArrayLike], list[str]]:
|
|
1575
|
+
"""
|
|
1576
|
+
Wrap label text at *width* characters.
|
|
1577
|
+
|
|
1578
|
+
Parameters
|
|
1579
|
+
----------
|
|
1580
|
+
width : int
|
|
1581
|
+
Maximum line width.
|
|
1582
|
+
|
|
1583
|
+
Returns
|
|
1584
|
+
-------
|
|
1585
|
+
callable
|
|
1586
|
+
``(x) -> list[str]``
|
|
1587
|
+
"""
|
|
1588
|
+
|
|
1589
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1590
|
+
if isinstance(x, str):
|
|
1591
|
+
x = [x]
|
|
1592
|
+
return [textwrap.fill(str(v), width=width) for v in np.asarray(x).flat]
|
|
1593
|
+
|
|
1594
|
+
return formatter
|
|
1595
|
+
|
|
1596
|
+
|
|
1597
|
+
# ---------------------------------------------------------------------------
|
|
1598
|
+
# label_glue
|
|
1599
|
+
# ---------------------------------------------------------------------------
|
|
1600
|
+
|
|
1601
|
+
|
|
1602
|
+
def label_glue(
|
|
1603
|
+
pattern: str = "{x}",
|
|
1604
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1605
|
+
"""
|
|
1606
|
+
Label with ``str.format``-style patterns.
|
|
1607
|
+
|
|
1608
|
+
Parameters
|
|
1609
|
+
----------
|
|
1610
|
+
pattern : str, optional
|
|
1611
|
+
Format pattern where ``{x}`` is replaced with the value
|
|
1612
|
+
(default ``"{x}"``).
|
|
1613
|
+
|
|
1614
|
+
Returns
|
|
1615
|
+
-------
|
|
1616
|
+
callable
|
|
1617
|
+
``(x) -> list[str]``
|
|
1618
|
+
"""
|
|
1619
|
+
|
|
1620
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1621
|
+
return [pattern.format(x=v) for v in np.asarray(x).flat]
|
|
1622
|
+
|
|
1623
|
+
return formatter
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
# ---------------------------------------------------------------------------
|
|
1627
|
+
# label_parse / label_math
|
|
1628
|
+
# ---------------------------------------------------------------------------
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
def label_parse() -> Callable[[ArrayLike], list[str]]:
|
|
1632
|
+
"""
|
|
1633
|
+
Return labels as-is (identity formatter).
|
|
1634
|
+
|
|
1635
|
+
In R this parses plotmath expressions; in Python it simply converts
|
|
1636
|
+
values to strings.
|
|
1637
|
+
|
|
1638
|
+
Returns
|
|
1639
|
+
-------
|
|
1640
|
+
callable
|
|
1641
|
+
``(x) -> list[str]``
|
|
1642
|
+
"""
|
|
1643
|
+
|
|
1644
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1645
|
+
return [str(v) for v in np.asarray(x).flat]
|
|
1646
|
+
|
|
1647
|
+
return formatter
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def label_math(
|
|
1651
|
+
expr: Optional[str] = None,
|
|
1652
|
+
format_func: Optional[Callable[[ArrayLike], list[str]]] = None,
|
|
1653
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1654
|
+
"""
|
|
1655
|
+
Label with mathematical formatting.
|
|
1656
|
+
|
|
1657
|
+
In R this wraps with plotmath expressions. In Python it applies
|
|
1658
|
+
*format_func* first and then optionally wraps each result with *expr*.
|
|
1659
|
+
|
|
1660
|
+
Parameters
|
|
1661
|
+
----------
|
|
1662
|
+
expr : str, optional
|
|
1663
|
+
A pattern containing ``{x}`` to wrap each label.
|
|
1664
|
+
format_func : callable, optional
|
|
1665
|
+
Pre-formatter applied to values before wrapping.
|
|
1666
|
+
|
|
1667
|
+
Returns
|
|
1668
|
+
-------
|
|
1669
|
+
callable
|
|
1670
|
+
``(x) -> list[str]``
|
|
1671
|
+
"""
|
|
1672
|
+
|
|
1673
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1674
|
+
if format_func is not None:
|
|
1675
|
+
labels = format_func(x)
|
|
1676
|
+
else:
|
|
1677
|
+
labels = [str(v) for v in np.asarray(x).flat]
|
|
1678
|
+
if expr is not None:
|
|
1679
|
+
labels = [expr.format(x=lbl) for lbl in labels]
|
|
1680
|
+
return labels
|
|
1681
|
+
|
|
1682
|
+
return formatter
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
# ---------------------------------------------------------------------------
|
|
1686
|
+
# label_log / format_log
|
|
1687
|
+
# ---------------------------------------------------------------------------
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def format_log(
|
|
1691
|
+
x: ArrayLike,
|
|
1692
|
+
base: float = 10,
|
|
1693
|
+
signed: Optional[bool] = None,
|
|
1694
|
+
digits: int = 3,
|
|
1695
|
+
) -> list[str]:
|
|
1696
|
+
"""
|
|
1697
|
+
Format values as log expressions (e.g. ``"10^3"``).
|
|
1698
|
+
|
|
1699
|
+
Mirrors R's ``scales::format_log``: accepts **raw values** ``x`` and
|
|
1700
|
+
internally computes ``log(x, base)`` to obtain the exponent.
|
|
1701
|
+
|
|
1702
|
+
Parameters
|
|
1703
|
+
----------
|
|
1704
|
+
x : array-like
|
|
1705
|
+
Raw numeric values (not already-logged).
|
|
1706
|
+
base : float, optional
|
|
1707
|
+
Logarithmic base (default 10).
|
|
1708
|
+
signed : bool, optional
|
|
1709
|
+
If ``None`` (default), sign prefixes are shown when any finite
|
|
1710
|
+
value is ``<= 0``. ``True`` forces signed formatting; ``False``
|
|
1711
|
+
disables it.
|
|
1712
|
+
digits : int, optional
|
|
1713
|
+
Significant digits for the exponent (default 3).
|
|
1714
|
+
|
|
1715
|
+
Returns
|
|
1716
|
+
-------
|
|
1717
|
+
list of str
|
|
1718
|
+
"""
|
|
1719
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1720
|
+
if x_arr.size == 0:
|
|
1721
|
+
return []
|
|
1722
|
+
|
|
1723
|
+
n = x_arr.size
|
|
1724
|
+
flat = x_arr.flatten()
|
|
1725
|
+
prefix = [""] * n
|
|
1726
|
+
|
|
1727
|
+
finite = flat[np.isfinite(flat)]
|
|
1728
|
+
if signed is None:
|
|
1729
|
+
signed = bool(np.any(finite <= 0))
|
|
1730
|
+
|
|
1731
|
+
signs = np.zeros(n, dtype=int)
|
|
1732
|
+
if signed:
|
|
1733
|
+
# sign(NaN) = 0 for our purposes; don't overwrite NaNs
|
|
1734
|
+
for i, v in enumerate(flat):
|
|
1735
|
+
if np.isnan(v):
|
|
1736
|
+
continue
|
|
1737
|
+
if v > 0:
|
|
1738
|
+
signs[i] = 1
|
|
1739
|
+
prefix[i] = "+"
|
|
1740
|
+
elif v < 0:
|
|
1741
|
+
signs[i] = -1
|
|
1742
|
+
prefix[i] = "-"
|
|
1743
|
+
else:
|
|
1744
|
+
signs[i] = 0
|
|
1745
|
+
flat = np.abs(flat)
|
|
1746
|
+
flat = np.where(flat == 0, 1.0, flat)
|
|
1747
|
+
|
|
1748
|
+
base_str = str(int(base)) if float(base).is_integer() else str(base)
|
|
1749
|
+
log_base = math.log(base)
|
|
1750
|
+
|
|
1751
|
+
def _zapsmall(v: float, tol: float = 1e-10) -> float:
|
|
1752
|
+
# Match R's zapsmall: values close to zero become exactly zero.
|
|
1753
|
+
return 0.0 if abs(v) < tol else v
|
|
1754
|
+
|
|
1755
|
+
def _fmt_exponent(v: float) -> str:
|
|
1756
|
+
v = _zapsmall(v)
|
|
1757
|
+
if float(v).is_integer():
|
|
1758
|
+
return str(int(v))
|
|
1759
|
+
# R's format(x, digits=3) uses significant digits.
|
|
1760
|
+
return f"{v:.{max(digits - 1, 0)}g}" if digits > 0 else f"{v:g}"
|
|
1761
|
+
|
|
1762
|
+
results: list[str] = []
|
|
1763
|
+
for i, v in enumerate(flat):
|
|
1764
|
+
if np.isnan(v):
|
|
1765
|
+
results.append("NaN")
|
|
1766
|
+
continue
|
|
1767
|
+
if np.isinf(v):
|
|
1768
|
+
results.append(str(v))
|
|
1769
|
+
continue
|
|
1770
|
+
exponent = math.log(v) / log_base
|
|
1771
|
+
exponent_str = _fmt_exponent(exponent)
|
|
1772
|
+
text = f"{prefix[i]}{base_str}^{exponent_str}"
|
|
1773
|
+
if signed and signs[i] == 0:
|
|
1774
|
+
text = "0"
|
|
1775
|
+
results.append(text)
|
|
1776
|
+
return results
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def label_log(
|
|
1780
|
+
base: float = 10,
|
|
1781
|
+
digits: int = 3,
|
|
1782
|
+
signed: Optional[bool] = None,
|
|
1783
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1784
|
+
"""
|
|
1785
|
+
Label values on a log scale (e.g. ``"10^3"``).
|
|
1786
|
+
|
|
1787
|
+
Parameters
|
|
1788
|
+
----------
|
|
1789
|
+
base : float, optional
|
|
1790
|
+
Logarithmic base (default 10).
|
|
1791
|
+
digits : int, optional
|
|
1792
|
+
Significant digits for non-integer exponents (default 3).
|
|
1793
|
+
signed : bool, optional
|
|
1794
|
+
Show sign on exponents. When ``None`` (default), signs appear
|
|
1795
|
+
whenever any finite input is ``<= 0``.
|
|
1796
|
+
|
|
1797
|
+
Returns
|
|
1798
|
+
-------
|
|
1799
|
+
callable
|
|
1800
|
+
``(x) -> list[str]``
|
|
1801
|
+
"""
|
|
1802
|
+
|
|
1803
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1804
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1805
|
+
text = format_log(x_arr, base=base, signed=signed, digits=digits)
|
|
1806
|
+
# Restore NaN labels like R's label_log (ret[is.na(x)] <- NA).
|
|
1807
|
+
for i, v in enumerate(x_arr.flatten()):
|
|
1808
|
+
if np.isnan(v):
|
|
1809
|
+
text[i] = "NaN"
|
|
1810
|
+
return text
|
|
1811
|
+
|
|
1812
|
+
return formatter
|
|
1813
|
+
|
|
1814
|
+
|
|
1815
|
+
# ---------------------------------------------------------------------------
|
|
1816
|
+
# label_number_auto
|
|
1817
|
+
# ---------------------------------------------------------------------------
|
|
1818
|
+
|
|
1819
|
+
|
|
1820
|
+
def label_number_auto() -> Callable[[ArrayLike], list[str]]:
|
|
1821
|
+
"""
|
|
1822
|
+
Automatically choose between regular and scientific notation.
|
|
1823
|
+
|
|
1824
|
+
Switches to scientific notation when values are very large or very small.
|
|
1825
|
+
|
|
1826
|
+
Returns
|
|
1827
|
+
-------
|
|
1828
|
+
callable
|
|
1829
|
+
``(x) -> list[str]``
|
|
1830
|
+
"""
|
|
1831
|
+
|
|
1832
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1833
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1834
|
+
finite = x_arr[np.isfinite(x_arr)]
|
|
1835
|
+
if len(finite) == 0:
|
|
1836
|
+
return number(x_arr)
|
|
1837
|
+
|
|
1838
|
+
abs_max = np.max(np.abs(finite)) if len(finite) else 0
|
|
1839
|
+
abs_min_nonzero = np.min(np.abs(finite[finite != 0])) if np.any(finite != 0) else 1
|
|
1840
|
+
|
|
1841
|
+
# Use scientific if range spans many orders of magnitude or extreme values
|
|
1842
|
+
if abs_max >= 1e9 or (abs_min_nonzero > 0 and abs_min_nonzero < 1e-3):
|
|
1843
|
+
return scientific(x_arr)
|
|
1844
|
+
return number(x_arr)
|
|
1845
|
+
|
|
1846
|
+
return formatter
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
# ---------------------------------------------------------------------------
|
|
1850
|
+
# label_number_si (deprecated)
|
|
1851
|
+
# ---------------------------------------------------------------------------
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def label_number_si(
|
|
1855
|
+
unit: str = "",
|
|
1856
|
+
accuracy: Optional[float] = None,
|
|
1857
|
+
scale: float = 1,
|
|
1858
|
+
suffix: str = "",
|
|
1859
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1860
|
+
"""
|
|
1861
|
+
Label numbers with SI prefixes (deprecated).
|
|
1862
|
+
|
|
1863
|
+
Use :func:`label_number` with ``scale_cut=cut_si(unit)`` instead.
|
|
1864
|
+
|
|
1865
|
+
Parameters
|
|
1866
|
+
----------
|
|
1867
|
+
unit : str, optional
|
|
1868
|
+
Base unit.
|
|
1869
|
+
accuracy : float, optional
|
|
1870
|
+
Rounding precision.
|
|
1871
|
+
scale : float, optional
|
|
1872
|
+
Multiplicative factor.
|
|
1873
|
+
suffix : str, optional
|
|
1874
|
+
Extra suffix after SI unit.
|
|
1875
|
+
|
|
1876
|
+
Returns
|
|
1877
|
+
-------
|
|
1878
|
+
callable
|
|
1879
|
+
``(x) -> list[str]``
|
|
1880
|
+
"""
|
|
1881
|
+
import warnings
|
|
1882
|
+
warnings.warn(
|
|
1883
|
+
"label_number_si() is deprecated. Use label_number(scale_cut=cut_si(unit)) instead.",
|
|
1884
|
+
DeprecationWarning,
|
|
1885
|
+
stacklevel=2,
|
|
1886
|
+
)
|
|
1887
|
+
return label_number(
|
|
1888
|
+
accuracy=accuracy,
|
|
1889
|
+
scale=scale,
|
|
1890
|
+
suffix=suffix,
|
|
1891
|
+
scale_cut=cut_si(unit),
|
|
1892
|
+
)
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
# ---------------------------------------------------------------------------
|
|
1896
|
+
# label_dictionary
|
|
1897
|
+
# ---------------------------------------------------------------------------
|
|
1898
|
+
|
|
1899
|
+
|
|
1900
|
+
def label_dictionary(
|
|
1901
|
+
dictionary: Optional[Dict[Any, str]] = None,
|
|
1902
|
+
nomatch: Optional[str] = None,
|
|
1903
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1904
|
+
"""
|
|
1905
|
+
Label values by dictionary lookup.
|
|
1906
|
+
|
|
1907
|
+
Parameters
|
|
1908
|
+
----------
|
|
1909
|
+
dictionary : dict, optional
|
|
1910
|
+
Mapping from data values to label strings.
|
|
1911
|
+
nomatch : str, optional
|
|
1912
|
+
Fallback string when a value is not in the dictionary.
|
|
1913
|
+
``None`` means the original value is converted to string.
|
|
1914
|
+
|
|
1915
|
+
Returns
|
|
1916
|
+
-------
|
|
1917
|
+
callable
|
|
1918
|
+
``(x) -> list[str]``
|
|
1919
|
+
"""
|
|
1920
|
+
if dictionary is None:
|
|
1921
|
+
dictionary = {}
|
|
1922
|
+
|
|
1923
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1924
|
+
results: list[str] = []
|
|
1925
|
+
for val in np.asarray(x).flat:
|
|
1926
|
+
# Try various key types
|
|
1927
|
+
key = val.item() if hasattr(val, "item") else val
|
|
1928
|
+
if key in dictionary:
|
|
1929
|
+
results.append(str(dictionary[key]))
|
|
1930
|
+
elif nomatch is not None:
|
|
1931
|
+
results.append(nomatch)
|
|
1932
|
+
else:
|
|
1933
|
+
results.append(str(key))
|
|
1934
|
+
return results
|
|
1935
|
+
|
|
1936
|
+
return formatter
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
# ---------------------------------------------------------------------------
|
|
1940
|
+
# compose_label
|
|
1941
|
+
# ---------------------------------------------------------------------------
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def compose_label(
|
|
1945
|
+
*formatters: Callable[[ArrayLike], list[str]],
|
|
1946
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1947
|
+
"""
|
|
1948
|
+
Compose multiple label formatters, applying them in sequence.
|
|
1949
|
+
|
|
1950
|
+
Each formatter receives the output of the previous one.
|
|
1951
|
+
|
|
1952
|
+
Parameters
|
|
1953
|
+
----------
|
|
1954
|
+
*formatters : callable
|
|
1955
|
+
Label functions to compose.
|
|
1956
|
+
|
|
1957
|
+
Returns
|
|
1958
|
+
-------
|
|
1959
|
+
callable
|
|
1960
|
+
``(x) -> list[str]``
|
|
1961
|
+
"""
|
|
1962
|
+
|
|
1963
|
+
def formatter(x: ArrayLike) -> list[str]:
|
|
1964
|
+
result = x
|
|
1965
|
+
for f in formatters:
|
|
1966
|
+
result = f(result)
|
|
1967
|
+
return result # type: ignore[return-value]
|
|
1968
|
+
|
|
1969
|
+
return formatter
|
|
1970
|
+
|
|
1971
|
+
|
|
1972
|
+
# ---------------------------------------------------------------------------
|
|
1973
|
+
# unit_format
|
|
1974
|
+
# ---------------------------------------------------------------------------
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
def unit_format(
|
|
1978
|
+
unit: str = "m",
|
|
1979
|
+
scale: float = 1,
|
|
1980
|
+
sep: str = " ",
|
|
1981
|
+
**kwargs: Any,
|
|
1982
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
1983
|
+
"""
|
|
1984
|
+
Append a unit to formatted numbers.
|
|
1985
|
+
|
|
1986
|
+
Parameters
|
|
1987
|
+
----------
|
|
1988
|
+
unit : str, optional
|
|
1989
|
+
Unit string (default ``"m"``).
|
|
1990
|
+
scale : float, optional
|
|
1991
|
+
Multiplicative factor.
|
|
1992
|
+
sep : str, optional
|
|
1993
|
+
Separator between number and unit (default ``" "``).
|
|
1994
|
+
**kwargs
|
|
1995
|
+
Passed to :func:`label_number`.
|
|
1996
|
+
|
|
1997
|
+
Returns
|
|
1998
|
+
-------
|
|
1999
|
+
callable
|
|
2000
|
+
``(x) -> list[str]``
|
|
2001
|
+
"""
|
|
2002
|
+
return label_number(scale=scale, suffix=f"{sep}{unit}", **kwargs)
|
|
2003
|
+
|
|
2004
|
+
|
|
2005
|
+
# ---------------------------------------------------------------------------
|
|
2006
|
+
# Date utility aliases
|
|
2007
|
+
# ---------------------------------------------------------------------------
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def date_breaks(width: str) -> Callable:
|
|
2011
|
+
"""
|
|
2012
|
+
Return a break function for date axes.
|
|
2013
|
+
|
|
2014
|
+
Parameters
|
|
2015
|
+
----------
|
|
2016
|
+
width : str
|
|
2017
|
+
Break width specification (e.g. ``"1 month"``). Delegates to
|
|
2018
|
+
``breaks_width``.
|
|
2019
|
+
|
|
2020
|
+
Returns
|
|
2021
|
+
-------
|
|
2022
|
+
callable
|
|
2023
|
+
"""
|
|
2024
|
+
from .breaks import breaks_width
|
|
2025
|
+
return breaks_width(width)
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
def date_format(
|
|
2029
|
+
format: str = "%Y-%m-%d",
|
|
2030
|
+
tz: str = "UTC",
|
|
2031
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
2032
|
+
"""Alias for :func:`label_date`."""
|
|
2033
|
+
return label_date(format=format, tz=tz)
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
def time_format(
|
|
2037
|
+
format: str = "%H:%M:%S",
|
|
2038
|
+
tz: str = "UTC",
|
|
2039
|
+
) -> Callable[[ArrayLike], list[str]]:
|
|
2040
|
+
"""Alias for :func:`label_time`."""
|
|
2041
|
+
return label_time(format=format, tz=tz)
|
|
2042
|
+
|
|
2043
|
+
|
|
2044
|
+
# ---------------------------------------------------------------------------
|
|
2045
|
+
# Legacy aliases
|
|
2046
|
+
# ---------------------------------------------------------------------------
|
|
2047
|
+
|
|
2048
|
+
comma_format = label_comma
|
|
2049
|
+
dollar_format = label_dollar
|
|
2050
|
+
percent_format = label_percent
|
|
2051
|
+
scientific_format = label_scientific
|
|
2052
|
+
ordinal_format = label_ordinal
|
|
2053
|
+
pvalue_format = label_pvalue
|
|
2054
|
+
number_format = label_number
|
|
2055
|
+
number_bytes_format = label_bytes
|
|
2056
|
+
number_bytes = label_bytes
|
|
2057
|
+
parse_format = label_parse
|
|
2058
|
+
math_format = label_math
|
|
2059
|
+
wrap_format = label_wrap
|
|
2060
|
+
format_format = label_glue
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
# ---------------------------------------------------------------------------
|
|
2064
|
+
# Global number-formatting options
|
|
2065
|
+
# ---------------------------------------------------------------------------
|
|
2066
|
+
|
|
2067
|
+
# Module-level option store – mirrors R's `options(scales.*)` mechanism.
|
|
2068
|
+
# NOTE on `big_mark`: the default is an empty string rather than R's
|
|
2069
|
+
# figure space `" "`. This is an intentional Python divergence decided
|
|
2070
|
+
# 2026-04-16 so that label output round-trips through float(). Users
|
|
2071
|
+
# wanting R's visual style call `number_options(big_mark=" ")`.
|
|
2072
|
+
_NUMBER_OPTIONS: dict[str, object] = {
|
|
2073
|
+
"decimal_mark": ".",
|
|
2074
|
+
"big_mark": "",
|
|
2075
|
+
"style_positive": "none",
|
|
2076
|
+
"style_negative": "hyphen",
|
|
2077
|
+
"currency_prefix": "$",
|
|
2078
|
+
"currency_suffix": "",
|
|
2079
|
+
"currency_decimal_mark": ".",
|
|
2080
|
+
"currency_big_mark": ",",
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def number_options(
|
|
2085
|
+
decimal_mark: str = ".",
|
|
2086
|
+
big_mark: str = "",
|
|
2087
|
+
style_positive: str = "none",
|
|
2088
|
+
style_negative: str = "hyphen",
|
|
2089
|
+
currency_prefix: str = "$",
|
|
2090
|
+
currency_suffix: str = "",
|
|
2091
|
+
currency_decimal_mark: str | None = None,
|
|
2092
|
+
currency_big_mark: str | None = None,
|
|
2093
|
+
) -> dict[str, object]:
|
|
2094
|
+
"""Set global default options for number formatting.
|
|
2095
|
+
|
|
2096
|
+
In R this sets ``options(scales.*)``. In Python the values are
|
|
2097
|
+
stored in the module-level ``_NUMBER_OPTIONS`` dict and can be
|
|
2098
|
+
read by label functions that wish to honour defaults.
|
|
2099
|
+
|
|
2100
|
+
Calling with no arguments resets all options to their defaults.
|
|
2101
|
+
|
|
2102
|
+
Parameters
|
|
2103
|
+
----------
|
|
2104
|
+
decimal_mark : str
|
|
2105
|
+
Default decimal separator.
|
|
2106
|
+
big_mark : str
|
|
2107
|
+
Default thousands separator.
|
|
2108
|
+
style_positive : str
|
|
2109
|
+
How to display positive numbers: ``"none"``, ``"plus"``, or ``"space"``.
|
|
2110
|
+
style_negative : str
|
|
2111
|
+
How to display negative numbers: ``"hyphen"``, ``"minus"``, or ``"parens"``.
|
|
2112
|
+
currency_prefix : str
|
|
2113
|
+
Default currency prefix.
|
|
2114
|
+
currency_suffix : str
|
|
2115
|
+
Default currency suffix.
|
|
2116
|
+
currency_decimal_mark : str or None
|
|
2117
|
+
Decimal mark for currency (defaults to *decimal_mark*).
|
|
2118
|
+
currency_big_mark : str or None
|
|
2119
|
+
Big mark for currency (defaults to ``","`` if *currency_decimal_mark*
|
|
2120
|
+
is ``"."``, else ``"."``).
|
|
2121
|
+
|
|
2122
|
+
Returns
|
|
2123
|
+
-------
|
|
2124
|
+
dict
|
|
2125
|
+
Previous option values (before this call changed them).
|
|
2126
|
+
"""
|
|
2127
|
+
prev = dict(_NUMBER_OPTIONS)
|
|
2128
|
+
|
|
2129
|
+
if currency_decimal_mark is None:
|
|
2130
|
+
currency_decimal_mark = decimal_mark
|
|
2131
|
+
if currency_big_mark is None:
|
|
2132
|
+
currency_big_mark = "," if currency_decimal_mark == "." else "."
|
|
2133
|
+
|
|
2134
|
+
_NUMBER_OPTIONS.update(
|
|
2135
|
+
decimal_mark=decimal_mark,
|
|
2136
|
+
big_mark=big_mark,
|
|
2137
|
+
style_positive=style_positive,
|
|
2138
|
+
style_negative=style_negative,
|
|
2139
|
+
currency_prefix=currency_prefix,
|
|
2140
|
+
currency_suffix=currency_suffix,
|
|
2141
|
+
currency_decimal_mark=currency_decimal_mark,
|
|
2142
|
+
currency_big_mark=currency_big_mark,
|
|
2143
|
+
)
|
|
2144
|
+
return prev
|