Skip to content

Commit 8075451

Browse files
committed
Add function plotting.percentage_ticks()
1 parent 99faab8 commit 8075451

File tree

3 files changed

+75
-12
lines changed

3 files changed

+75
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111
- Argument `include_module=False` for function `misc.tname()`.
12+
- New function `plotting.percentage_ticks()`
1213

1314
### Removed
1415
- Module `rics.ml.time_split`. use the `time-split` [![PyPI - Version](https://img.shields.io/pypi/v/time-split.svg)](https://pypi.python.org/pypi/time-split) package instead.

src/rics/plotting.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
from matplotlib.axis import Axis as _Axis
66
from matplotlib.axis import XAxis as _XAxis
7+
from matplotlib.axis import YAxis as _YAxis
78
from matplotlib.ticker import FuncFormatter as _FuncFormatter
89
from matplotlib.ticker import IndexLocator as _IndexLocator
910

1011
ERROR_BAR_CAPSIZE: float = 0.1
1112

12-
HalfRep = _t.Literal["fraction", "decimal", "frac", "dec", "f", "d"]
13+
HalfRep = _t.Literal["fraction", "f", "decimal", "d"]
14+
Decimals = int | _t.Literal[False] | None
1315

1416

1517
class HasXAxis(_t.Protocol):
@@ -19,6 +21,13 @@ class HasXAxis(_t.Protocol):
1921
"""X-Axis attribute."""
2022

2123

24+
class HasYAxis(_t.Protocol):
25+
"""Protocol class indicating something that as an Y-axis."""
26+
27+
yaxis: _YAxis
28+
"""Y-Axis attribute."""
29+
30+
2231
def configure() -> None:
2332
"""Call all configure-functions in this module."""
2433
configure_matplotlib()
@@ -57,15 +66,10 @@ def configure_matplotlib() -> None:
5766
ModuleNotFoundError: If matplotlib is not installed.
5867
5968
"""
60-
import functools
61-
6269
import matplotlib.pyplot as plt
63-
import matplotlib.ticker as mtick
6470

6571
plt.rcParams["figure.figsize"] = (20, 5)
66-
plt.subplots = functools.partial(plt.subplots, tight_layout=True)
67-
68-
mtick.PercentFormatter = functools.partial(mtick.PercentFormatter, xmax=1) # type: ignore[misc, assignment]
72+
plt.rcParams["figure.autolayout"] = True
6973

7074

7175
def pi_ticks(ax: _Axis | HasXAxis, half_rep: HalfRep | None = None) -> None:
@@ -81,13 +85,13 @@ def pi_ticks(ax: _Axis | HasXAxis, half_rep: HalfRep | None = None) -> None:
8185
- Example output
8286
* - ``None``
8387
- Show integer multiples only.
84-
- `0, π, 2π, 3π..`
88+
- `0, π, 2π, 3π, ...`
8589
* - `'f'` or `'fraction'`
8690
- Halves of `π` use fractional representation.
87-
- `0/2π, 1/2π, 2/2π, 3/2π..`
91+
- `0/2π, 1/2π, 2/2π, 3/2π, ...`
8892
* - `'d'` or `'decimal'`
8993
- Halves of `π` use decimal representation.
90-
- `0.0π, 0.5π, 1.0π, 1.5π..`
94+
- `0.0π, 0.5π, 1.0π, 1.5π, ...`
9195
9296
Args:
9397
ax: An axis to decorate, or an object with an `xaxis` attribute.
@@ -131,7 +135,8 @@ def _parse_half_rep(
131135
if parsed_half.startswith("d"):
132136
return "dec"
133137

134-
raise TypeError(f"Argument {half_rep=} not in ('[f]raction', '[d]ecimal', None).")
138+
msg = f"Argument {half_rep=} not in ('[f]raction', '[d]ecimal', None)."
139+
raise TypeError(msg)
135140

136141
def _format(self, x: float, _pos: int) -> str:
137142
n = round(x / self.PI, 1)
@@ -148,3 +153,31 @@ def _format(self, x: float, _pos: int) -> str:
148153
return f"{n}π"
149154

150155
return f"{int(n * 2)}/2π"
156+
157+
158+
def percentage_ticks(
159+
ax: _Axis | HasYAxis,
160+
*,
161+
sign: bool = False,
162+
decimals: Decimals = 1,
163+
) -> None:
164+
"""Decorate an axis by formatting ticks as percentages.
165+
166+
Args:
167+
ax: An axis to decorate, or an object with a `yaxis` attribute.
168+
sign: If ``True``, show prepend `'+'` for positive ticks.
169+
decimals: Number of decimals to keep.
170+
171+
Return:
172+
The formatting string.
173+
"""
174+
axis: _Axis = ax.yaxis if hasattr(ax, "yaxis") else ax
175+
176+
formatter = _make_percent_formatter(sign, decimals)
177+
axis.set_major_formatter(formatter)
178+
179+
180+
def _make_percent_formatter(sign: bool, decimals: Decimals) -> str:
181+
plus = "+" if sign else ""
182+
decimals = decimals or 0
183+
return "{" + f"x:{plus}.{decimals}%" + "}"

tests/test_plotting.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import numpy as np
33
import pytest
44

5-
from rics.plotting import _PiTickHelper, pi_ticks
5+
from rics.plotting import _make_percent_formatter, _PiTickHelper, percentage_ticks, pi_ticks
66

77
PI = _PiTickHelper.PI
88
HALF_PI = PI / 2
@@ -42,3 +42,32 @@ def test_pi_tick_helper(half, start, start_rounded_to_pi, expected):
4242
# Test formatting
4343
actual = [helper._format(x, pos) for x, pos in values]
4444
assert actual == expected
45+
46+
47+
def test_percent_ticks_doesnt_crash():
48+
ax = plt.axes()
49+
ax.plot(range(10))
50+
percentage_ticks(ax)
51+
plt.close("all")
52+
53+
54+
@pytest.mark.parametrize(
55+
"x, sign, decimals, expected",
56+
[
57+
(-0.111, False, 0, "-11%"),
58+
(-0.111, False, 1, "-11.1%"),
59+
(-0.111, True, 1, "-11.1%"),
60+
(-0.000, False, 0, "-0%"),
61+
(-0.000, False, 1, "-0.0%"),
62+
(-0.000, True, 1, "-0.0%"),
63+
(+0.000, False, 0, "0%"),
64+
(+0.000, False, 1, "0.0%"),
65+
(+0.000, True, 1, "+0.0%"),
66+
(+0.111, False, 0, "11%"),
67+
(+0.111, False, 1, "11.1%"),
68+
(+0.111, True, 1, "+11.1%"),
69+
],
70+
)
71+
def test_percent_formatter(x, sign, decimals, expected):
72+
actual = _make_percent_formatter(sign, decimals).format(x=x)
73+
assert actual == expected

0 commit comments

Comments
 (0)