Skip to content

Commit dda2b4e

Browse files
committed
Impl, test compact_xaxis_units
1 parent a3ef533 commit dda2b4e

File tree

4 files changed

+180
-0
lines changed

4 files changed

+180
-0
lines changed

conduitpylib/test/test_viz/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from matplotlib import pyplot as plt
2+
3+
from conduitpylib.viz import compact_xaxis_units
4+
5+
6+
def test_prefix_removal():
7+
# setup
8+
ax = plt.gca()
9+
ax.ticklabel_format(style="sci", scilimits=(-2, 2)),
10+
ax.plot([1000, 2000, 3000], [1, 2, 3])
11+
ax.set_xlabel("Time (ms)")
12+
13+
# apply
14+
compact_xaxis_units(ax, base_unit="s")
15+
16+
# verify
17+
expected_label = "Time (s)"
18+
assert ax.get_xlabel() == expected_label
19+
20+
# clean up
21+
plt.clf()
22+
23+
24+
def test_prefix_addition():
25+
# setup
26+
ax = plt.gca()
27+
ax.ticklabel_format(style="sci", scilimits=(-2, 2)),
28+
ax.plot([1000, 2000, 3000], [1, 2, 3])
29+
ax.set_xlabel("Weight (g)")
30+
# apply
31+
compact_xaxis_units(ax, base_unit="g")
32+
33+
# verify
34+
expected_label = "Weight (kg)"
35+
assert ax.get_xlabel() == expected_label
36+
37+
# clean up
38+
plt.clf()
39+
40+
41+
def test_prefix_nop():
42+
# setup
43+
ax = plt.gca()
44+
ax.ticklabel_format(style="sci", scilimits=(-2, 2)),
45+
ax.plot([1, 2, 3], [1, 2, 3])
46+
ax.set_xlabel("Weight (g)")
47+
# apply
48+
compact_xaxis_units(ax, base_unit="g")
49+
50+
# verify
51+
expected_label = "Weight (g)"
52+
assert ax.get_xlabel() == expected_label
53+
54+
# clean up
55+
plt.clf()
56+
57+
58+
def test_nanoseconds_to_milliseconds():
59+
# setup
60+
ax = plt.gca()
61+
ax.ticklabel_format(style="sci", scilimits=(-2, 2)),
62+
ax.plot([1e6, 2e6, 3e6], [1, 2, 3])
63+
ax.set_xlabel("Time (ns)")
64+
65+
# apply
66+
compact_xaxis_units(ax, base_unit="s")
67+
68+
# verify
69+
expected_label = "Time (ms)"
70+
assert ax.get_xlabel() == expected_label
71+
72+
# clean up
73+
plt.clf()

conduitpylib/viz/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
"""Visualization tools."""
22

3+
from ._compact_xaxis_units import compact_xaxis_units
34
from ._frame_scatter_subsets import frame_scatter_subsets
45
from ._performance_semantics_plot import performance_semantics_plot
56
from ._set_kde_lims import set_kde_lims
67

78

89
# adapted from https://stackoverflow.com/a/31079085
910
__all__ = [
11+
"compact_xaxis_units",
1012
"frame_scatter_subsets",
1113
"performance_semantics_plot",
1214
"set_kde_lims",
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import re
2+
3+
from bidict import frozenbidict
4+
from matplotlib import axis
5+
import numpy as np
6+
7+
from ..utils import round_to_multiple, splice
8+
9+
_si_prefixes = frozenbidict(
10+
{
11+
"Y": 24, # yotta
12+
"Z": 21, # zetta
13+
"E": 18, # exa
14+
"P": 15, # peta
15+
"T": 12, # tera
16+
"G": 9, # giga
17+
"M": 6, # mega
18+
"k": 3, # kilo
19+
"": 0, # base
20+
"m": -3, # milli
21+
"u": -6, # micro
22+
"n": -9, # nano
23+
"p": -12, # pico
24+
"f": -15, # femto
25+
"a": -18, # atto
26+
"z": -21, # zepto
27+
"y": -24, # yocto
28+
},
29+
)
30+
31+
32+
def compact_xaxis_units(
33+
ax: axis.Axis,
34+
base_unit: str = "s",
35+
regex_pattern_template: str = r"\((.*?){base_unit}\)",
36+
) -> None:
37+
r"""Adjusts the units on the x-axis of a matplotlib axis object to a more
38+
compact form.
39+
40+
This function finds the multiplier (such as 10^3, 10^6, etc.) used by
41+
matplotlib for the axis, identifies the current unit prefix (like k for
42+
kilo, M for mega), and then consolidates these into a new, more compact unit
43+
prefix. This is particularly useful for graphs where the axis values are
44+
very large or very small, and a more readable unit is desired.
45+
46+
Parameters
47+
----------
48+
ax : axis.Axis
49+
Axis object whose x-axis units are to be compacted inplace.
50+
base_unit : str, default "s"
51+
The base unit (without prefix) for the axis.
52+
regex_pattern_template : str, default r"\((.*?){base_unit}\)"
53+
A regular expression template used to identify and extract the unit
54+
prefix from the axis label.
55+
56+
The template must contain `{base_unit}` as a
57+
placeholder for the actual base unit. Default matches parenthesized expressions like "(ks)" for kiloseconds.
58+
"""
59+
regex_pattern = regex_pattern_template.format(base_unit=base_unit)
60+
regex = re.compile(regex_pattern)
61+
62+
# force population of
63+
fig = ax.get_figure()
64+
fig.canvas.draw()
65+
66+
# get unit multiplier chosen by matplotlib
67+
offset_text = ax.xaxis.get_offset_text()
68+
if offset_text is None: # handle unit (i.e., non) multiplier
69+
return
70+
offset_string = offset_text.get_text()
71+
if offset_string == "": # handle unit (i.e., non) multiplier
72+
return
73+
offset_amount = float(offset_string)
74+
75+
assert str(offset_amount).count("1") == 1 # power of 10
76+
77+
# get unit prefix and multiplier in axis label
78+
old_label_string = ax.get_xlabel()
79+
(old_prefix_match,) = regex.finditer(old_label_string)
80+
old_prefix = old_prefix_match.group(1)
81+
old_multiplier_pow = _si_prefixes[old_prefix]
82+
old_multiplier = 10.0**old_multiplier_pow
83+
84+
# calculate new prefix from net multiplier
85+
new_multiplier = offset_amount * old_multiplier
86+
approx_pow = np.log10(new_multiplier)
87+
new_multiplier_pow = round_to_multiple(approx_pow, multiple=3)
88+
assert np.isclose(approx_pow, new_multiplier_pow)
89+
new_prefix = _si_prefixes.inv[new_multiplier_pow]
90+
91+
# apply new multiplier, consolidating offset axis scale and label unit
92+
def replace_group(match):
93+
# Extracting the captured group
94+
captured_group = match.group(1)
95+
# Defining the replacement value for the captured group
96+
return replacement_value
97+
98+
ax.xaxis.get_offset_text().set(visible=False) # remove offset axis scale
99+
old_prefix_span = old_prefix_match.span(1)
100+
new_label_string = splice(
101+
old_label_string,
102+
old_prefix_span,
103+
new_prefix,
104+
)
105+
ax.set_xlabel(new_label_string)

0 commit comments

Comments
 (0)