Skip to content

Commit 822b01f

Browse files
committed
ENH: Added TransformFormatter to matplotlib.ticker
Tests included. Example code provided with some recipes from previous attempt.
1 parent 5e825bd commit 822b01f

File tree

6 files changed

+540
-28
lines changed

6 files changed

+540
-28
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Two new Formatters added to `matplotlib.ticker`
2+
-----------------------------------------------
3+
4+
Two new formatters have been added for displaying some specialized
5+
tick labels:
6+
7+
- :class:`matplotlib.ticker.PercentFormatter`
8+
- :class:`matplotlib.ticker.TransformFormatter`
9+
10+
11+
:class:`matplotlib.ticker.PercentFormatter`
12+
```````````````````````````````````````````
13+
14+
This new formatter has some nice features like being able to convert
15+
from arbitrary data scales to percents, a customizable percent symbol
16+
and either automatic or manual control over the decimal points.
17+
18+
19+
:class:`matplotlib.ticker.TransformFormatter`
20+
```````````````````````````````````````````````
21+
22+
A more generic version of :class:`matplotlib.ticker.FuncFormatter` that
23+
allows the tick values to be transformed before being passed to an
24+
underlying formatter. The transformation can yield results of arbitrary
25+
type, so for example, using `int` as the transformation will allow
26+
:class:`matplotlib.ticker.StrMethodFormatter` to use integer format
27+
strings. If the underlying formatter is an instance of
28+
:class:`matplotlib.ticker.Formatter`, it will be configured correctly
29+
through this class.

doc/users/whats_new/percent_formatter.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.

examples/ticks_and_spines/tick-formatters.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,23 @@ def setup(ax):
1919
ax.set_xlim(0, 5)
2020
ax.set_ylim(0, 1)
2121
ax.patch.set_alpha(0.0)
22+
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
23+
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
2224

2325

2426
plt.figure(figsize=(8, 6))
25-
n = 7
27+
n = 8
2628

2729
# Null formatter
2830
ax = plt.subplot(n, 1, 1)
2931
setup(ax)
30-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
31-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
3232
ax.xaxis.set_major_formatter(ticker.NullFormatter())
3333
ax.xaxis.set_minor_formatter(ticker.NullFormatter())
3434
ax.text(0.0, 0.1, "NullFormatter()", fontsize=16, transform=ax.transAxes)
3535

3636
# Fixed formatter
3737
ax = plt.subplot(n, 1, 2)
3838
setup(ax)
39-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.0))
40-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
4139
majors = ["", "0", "1", "2", "3", "4", "5"]
4240
ax.xaxis.set_major_formatter(ticker.FixedFormatter(majors))
4341
minors = [""] + ["%.2f" % (x-int(x)) if (x-int(x))
@@ -54,8 +52,6 @@ def major_formatter(x, pos):
5452

5553
ax = plt.subplot(n, 1, 3)
5654
setup(ax)
57-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
58-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
5955
ax.xaxis.set_major_formatter(ticker.FuncFormatter(major_formatter))
6056
ax.text(0.0, 0.1, 'FuncFormatter(lambda x, pos: "[%.2f]" % x)',
6157
fontsize=15, transform=ax.transAxes)
@@ -64,40 +60,39 @@ def major_formatter(x, pos):
6460
# FormatStr formatter
6561
ax = plt.subplot(n, 1, 4)
6662
setup(ax)
67-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
68-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
6963
ax.xaxis.set_major_formatter(ticker.FormatStrFormatter(">%d<"))
7064
ax.text(0.0, 0.1, "FormatStrFormatter('>%d<')",
7165
fontsize=15, transform=ax.transAxes)
7266

7367
# Scalar formatter
7468
ax = plt.subplot(n, 1, 5)
7569
setup(ax)
76-
ax.xaxis.set_major_locator(ticker.AutoLocator())
77-
ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
7870
ax.xaxis.set_major_formatter(ticker.ScalarFormatter(useMathText=True))
7971
ax.text(0.0, 0.1, "ScalarFormatter()", fontsize=15, transform=ax.transAxes)
8072

8173
# StrMethod formatter
8274
ax = plt.subplot(n, 1, 6)
8375
setup(ax)
84-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
85-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
8676
ax.xaxis.set_major_formatter(ticker.StrMethodFormatter("{x}"))
8777
ax.text(0.0, 0.1, "StrMethodFormatter('{x}')",
8878
fontsize=15, transform=ax.transAxes)
8979

9080
# Percent formatter
9181
ax = plt.subplot(n, 1, 7)
9282
setup(ax)
93-
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
94-
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
9583
ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5))
9684
ax.text(0.0, 0.1, "PercentFormatter(xmax=5)",
9785
fontsize=15, transform=ax.transAxes)
9886

99-
# Push the top of the top axes outside the figure because we only show the
100-
# bottom spine.
87+
# TransformFormatter
88+
ax = plt.subplot(n, 1, 8)
89+
setup(ax)
90+
ax.xaxis.set_major_formatter(ticker.TransformFormatter(lambda x: 7 - 2 * x))
91+
ax.text(0.0, 0.1, "TransformFormatter(lambda x: 7 - 2 * x)",
92+
fontsize=15, transform=ax.transAxes)
93+
94+
# Push the top of the top axes outside the figure because we only show
95+
# the bottom spine.
10196
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)
10297

10398
plt.show()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""
2+
Demo of the `matplotlib.ticker.TransformFormatter` class.
3+
4+
This code demonstrates two features:
5+
6+
1. A linear transformation of the input values. A callable class for
7+
doing the transformation is presented as a recipe here. The data
8+
type of the inputs does not change.
9+
2. A transformation of the input type. The example here allows
10+
`matplotlib.ticker.StrMethodFormatter` to handle integer formats
11+
('b', 'o', 'd', 'n', 'x', 'X'), which will normally raise an error
12+
if used directly. This transformation is associated with a
13+
`matplotlib.ticker.MaxNLocator` which has `integer` set to True to
14+
ensure that the inputs are indeed integers.
15+
16+
The same histogram is plotted in two sub-plots with a shared x-axis.
17+
Each axis shows a different temperature scale: one in degrees Celsius,
18+
one in degrees Rankine (the Fahrenheit analogue of Kelvins). This is one
19+
of the few examples of recognized scientific units that have both a
20+
scale and an offset relative to each other.
21+
"""
22+
23+
import numpy as np
24+
from matplotlib import pyplot as plt
25+
from matplotlib.axis import Ticker
26+
from matplotlib.ticker import (
27+
TransformFormatter, StrMethodFormatter, MaxNLocator
28+
)
29+
30+
31+
class LinearTransform:
32+
"""
33+
A callable class that transforms input values to output according to
34+
a linear transformation.
35+
"""
36+
37+
def __init__(self, in_start=0.0, in_end=None, out_start=0.0, out_end=None):
38+
"""
39+
Sets up the transformation such that `in_start` gets mapped to
40+
`out_start` and `in_end` gets mapped to `out_end`. The following
41+
shortcuts apply when only some of the inputs are specified:
42+
43+
- none: no-op
44+
- in_start: translation to zero
45+
- out_start: translation from zero
46+
- in_end: scaling to one (divide input by in_end)
47+
- out_end: scaling from one (multiply input by in_end)
48+
- in_start, out_start: translation
49+
- in_end, out_end: scaling (in_start and out_start zero)
50+
- in_start, out_end: in_end=out_end, out_start=0
51+
- in_end, out_start: in_start=0, out_end=in_end
52+
53+
Based on the following rules:
54+
55+
- start missing: set start to zero
56+
- both ends are missing: set ranges to 1.0
57+
- one end is missing: set it to the other end
58+
"""
59+
if in_end is not None:
60+
in_scale = in_end - in_start
61+
elif out_end is not None:
62+
in_scale = out_end - in_start
63+
else:
64+
in_scale = 1.0
65+
66+
if out_end is not None:
67+
out_scale = out_end - out_start
68+
elif in_end is not None:
69+
out_scale = in_end - out_start
70+
else:
71+
out_scale = 1.0
72+
73+
self._scale = out_scale / in_scale
74+
self._offset = out_start - self._scale * in_start
75+
76+
def __call__(self, x):
77+
"""
78+
Transforms the input value `x` according to the rule set up in
79+
`__init__`.
80+
"""
81+
return x * self._scale + self._offset
82+
83+
# X-data
84+
temp_C = np.arange(-5.0, 5.1, 0.25)
85+
# Y-data
86+
counts = 15.0 * np.exp(-temp_C**2 / 25)
87+
# Add some noise
88+
counts += np.random.normal(scale=4.0, size=counts.shape)
89+
if counts.min() < 0:
90+
counts += counts.min()
91+
92+
fig, ax1 = plt.subplots()
93+
ax2 = fig.add_subplot(111, sharex=ax1, sharey=ax1, frameon=False)
94+
95+
ax1.plot(temp_C, counts, drawstyle='steps-mid')
96+
97+
ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}'))
98+
99+
# This step is necessary to allow the shared x-axes to have different
100+
# Formatter and Locator objects.
101+
ax2.xaxis.major = Ticker()
102+
# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition)
103+
ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator())
104+
ax2.xaxis.set_major_formatter(
105+
TransformFormatter(LinearTransform(in_start=-273.15, in_end=0,
106+
out_end=491.67),
107+
StrMethodFormatter('{x:0.2f}')))
108+
109+
# The y-axes share their locators and formatters, so only one needs to
110+
# be set
111+
ax1.yaxis.set_major_locator(MaxNLocator(integer=True))
112+
# Setting the transfrom to `int` will only alter the type, not the
113+
# actual value of the ticks
114+
ax1.yaxis.set_major_formatter(
115+
TransformFormatter(int, StrMethodFormatter('{x:02X}')))
116+
117+
ax1.set_xlabel('Temperature (\u00B0C)')
118+
ax1.set_ylabel('Samples (Hex)')
119+
ax2.set_xlabel('Temperature (\u00B0R)')
120+
121+
ax1.xaxis.tick_top()
122+
ax1.xaxis.set_label_position('top')
123+
124+
plt.show()

0 commit comments

Comments
 (0)