Skip to content

Commit 7ea51e7

Browse files
committed
ENH: Added TransformFormatter to matplotlib.ticker
Tests included. Example code provided with some recipes from previous attempt.
1 parent 70831bd commit 7ea51e7

File tree

6 files changed

+584
-49
lines changed

6 files changed

+584
-49
lines changed
+29
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

-6
This file was deleted.

examples/ticks_and_spines/tick-formatters.py

+11-13
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, 5))
25-
n = 6
27+
n = 7
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,29 +60,31 @@ 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

80+
# TransformFormatter
81+
ax = plt.subplot(n, 1, 7)
82+
setup(ax)
83+
ax.xaxis.set_major_formatter(ticker.TransformFormatter(lambda x: 7 - 2 * x))
84+
ax.text(0.0, 0.1, "TransformFormatter(lambda x: 7 - 2 * x)",
85+
fontsize=15, transform=ax.transAxes)
86+
87+
9088
# Push the top of the top axes outside the figure because we only show the
9189
# bottom spine.
9290
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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=None, in_end=None, out_start=None, 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+
self._in_offset = 0.0 if in_start is None else in_start
60+
self._out_offset = 0.0 if out_start is None else out_start
61+
62+
if in_end is None:
63+
if out_end is None:
64+
self._in_scale = 1.0
65+
else:
66+
self._in_scale = out_end - self._in_offset
67+
else:
68+
self._in_scale = in_end - self._in_offset
69+
70+
if out_end is None:
71+
if in_end is None:
72+
self._out_scale = 1.0
73+
else:
74+
self._out_scale = in_end - self._out_offset
75+
else:
76+
self._out_scale = out_end - self._out_offset
77+
78+
def __call__(self, x):
79+
"""
80+
Transforms the input value `x` according to the rule set up in
81+
`__init__`.
82+
"""
83+
return ((x - self._in_offset) * self._out_scale / self._in_scale +
84+
self._out_offset)
85+
86+
# X-data
87+
temp_C = np.arange(-5.0, 5.1, 0.25)
88+
# Y-data
89+
counts = 15.0 * np.exp(-temp_C**2 / 25)
90+
# Add some noise
91+
counts += np.random.normal(scale=4.0, size=counts.shape)
92+
if counts.min() < 0:
93+
counts += counts.min()
94+
95+
fig = plt.figure()
96+
ax1 = fig.add_subplot(111)
97+
ax2 = fig.add_subplot(111, sharex=ax1, sharey=ax1, frameon=False)
98+
99+
ax1.plot(temp_C, counts, drawstyle='steps-mid')
100+
101+
ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}'))
102+
103+
# This step is necessary to allow the shared x-axes to have different
104+
# Formatter and Locator objects.
105+
ax2.xaxis.major = Ticker()
106+
# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition)
107+
ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator())
108+
ax2.xaxis.set_major_formatter(
109+
TransformFormatter(LinearTransform(in_start=-273.15, in_end=0,
110+
out_end=491.67),
111+
StrMethodFormatter('{x:0.2f}')))
112+
113+
# The y-axes share their locators and formatters, so only one needs to
114+
# be set
115+
ax1.yaxis.set_major_locator(MaxNLocator(integer=True))
116+
# Setting the transfrom to `int` will only alter the type, not the
117+
# actual value of the ticks
118+
ax1.yaxis.set_major_formatter(
119+
TransformFormatter(int, StrMethodFormatter('{x:02X}')))
120+
121+
ax1.set_xlabel('Temperature (\u00B0C)')
122+
ax1.set_ylabel('Samples (Hex)')
123+
ax2.set_xlabel('Temperature (\u00B0R)')
124+
125+
ax1.xaxis.tick_top()
126+
ax1.xaxis.set_label_position('top')
127+
128+
plt.show()

0 commit comments

Comments
 (0)