Skip to content

Commit cf6852c

Browse files
committed
ENH: Added TransformFormatter to matplotlib.ticker
Tests included. Example code provided with some recipes from previous attempt.
1 parent 22cf7e0 commit cf6852c

File tree

5 files changed

+375
-32
lines changed

5 files changed

+375
-32
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Four 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 yeild 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.
30+

doc/users/whats_new/percent_formatter.rst

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
class LinearTransform:
31+
"""
32+
A callable class that transforms input values to output according to
33+
a linear transformation.
34+
"""
35+
36+
def __init__(self, in_start=None, in_end=None, out_start=None, out_end=None):
37+
"""
38+
Sets up the transformation such that `in_start` gets mapped to
39+
`out_start` and `in_end` gets mapped to `out_end`. The following
40+
shortcuts apply when only some of the inputs are specified:
41+
42+
- none: no-op
43+
- in_start: translation to zero
44+
- out_start: translation from zero
45+
- in_end: scaling to one (divide input by in_end)
46+
- out_end: scaling from one (multiply input by in_end)
47+
- in_start, out_start: translation
48+
- in_end, out_end: scaling (in_start and out_start zero)
49+
- in_start, out_end: in_end=out_end, out_start=0
50+
- in_end, out_start: in_start=0, out_end=in_end
51+
52+
Based on the following rules:
53+
54+
- start missing: set start to zero
55+
- both ends are missing: set ranges to 1.0
56+
- one end is missing: set it to the other end
57+
"""
58+
self._in_offset = 0.0 if in_start is None else in_start
59+
self._out_offset = 0.0 if out_start is None else out_start
60+
61+
if in_end is None:
62+
if out_end is None:
63+
self._in_scale = 1.0
64+
else:
65+
self._in_scale = out_end - self._in_offset
66+
else:
67+
self._in_scale = in_end - self._in_offset
68+
69+
if out_end is None:
70+
if in_end is None:
71+
self._out_scale = 1.0
72+
else:
73+
self._out_scale = in_end - self._out_offset
74+
else:
75+
self._out_scale = out_end - self._out_offset
76+
77+
def __call__(self, x):
78+
"""
79+
Transforms the input value `x` according to the rule set up in
80+
`__init__`.
81+
"""
82+
return ((x - self._in_offset) * self._out_scale / self._in_scale +
83+
self._out_offset)
84+
85+
# X-data
86+
temp_C = np.arange(-5.0, 5.1, 0.25)
87+
# Y-data
88+
counts = 15.0 * np.exp(-temp_C**2 / 25)
89+
# Add some noise
90+
counts += np.random.normal(scale=4.0, size=counts.shape)
91+
if counts.min() < 0:
92+
counts += counts.min()
93+
94+
fig = plt.figure()
95+
fig.subplots_adjust(hspace=0.3)
96+
97+
ax1 = fig.add_subplot(211)
98+
ax2 = fig.add_subplot(212, sharex=ax1, sharey=ax1)
99+
100+
ax1.plot(temp_C, counts, drawstyle='steps-mid')
101+
ax2.plot(temp_C, counts, drawstyle='steps-mid')
102+
103+
ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}'))
104+
105+
# This step is necessary to allow the shared x-axes to have different
106+
# Formatter and Locator objects.
107+
ax2.xaxis.major = Ticker()
108+
# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition)
109+
ax2.xaxis.set_major_formatter(
110+
TransformFormatter(LinearTransform(in_start=-273.15, in_end=0,
111+
out_end=491.67),
112+
StrMethodFormatter('{x:0.2f}')))
113+
ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator())
114+
115+
# The y-axes share their locators and formatters, so only one needs to
116+
# be set
117+
ax1.yaxis.set_major_locator(MaxNLocator(integer=True))
118+
# Setting the transfrom to `int` will only alter the type, not the
119+
# actual value of the ticks
120+
ax1.yaxis.set_major_formatter(
121+
TransformFormatter(int, StrMethodFormatter('{x:02X}')))
122+
123+
fig.suptitle('Temperature vs Counts')
124+
ax1.set_xlabel('Temp (\u00B0C)')
125+
ax1.set_ylabel('Samples (Hex)')
126+
ax2.set_xlabel('Temp (\u00B0R)')
127+
ax2.set_ylabel('Samples (Hex)')
128+
129+
plt.show()
130+

lib/matplotlib/tests/test_ticker.py

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -504,37 +504,108 @@ def test_formatstrformatter():
504504
assert '002-01' == tmp_form(2, 1)
505505

506506

507-
percentformatter_test_cases = (
508-
# Check explicitly set decimals over different intervals and values
509-
(100, 0, '%', 120, 100, '120%'),
510-
(100, 0, '%', 100, 90, '100%'),
511-
(100, 0, '%', 90, 50, '90%'),
512-
(100, 0, '%', 1.7, 40, '2%'),
513-
(100, 1, '%', 90.0, 100, '90.0%'),
514-
(100, 1, '%', 80.1, 90, '80.1%'),
515-
(100, 1, '%', 70.23, 50, '70.2%'),
516-
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
517-
(100, 1, '%', 60.554, 40, '60.6%'),
518-
# Check auto decimals over different intervals and values
519-
(100, None, '%', 95, 1, '95.00%'),
520-
(1.0, None, '%', 3, 6, '300%'),
521-
(17.0, None, '%', 1, 8.5, '6%'),
522-
(17.0, None, '%', 1, 8.4, '5.9%'),
523-
(5, None, '%', -100, 0.000001, '-2000.00000%'),
524-
# Check percent symbol
525-
(1.0, 2, None, 1.2, 100, '120.00'),
526-
(75, 3, '', 50, 100, '66.667'),
527-
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
528-
)
529-
530-
531507
@pytest.mark.parametrize('xmax, decimals, symbol, x, display_range, expected',
532-
percentformatter_test_cases)
508+
[
509+
# Check explicitly set decimals over different intervals and values
510+
(100, 0, '%', 120, 100, '120%'),
511+
(100, 0, '%', 100, 90, '100%'),
512+
(100, 0, '%', 90, 50, '90%'),
513+
(100, 0, '%', 1.7, 40, '2%'),
514+
(100, 1, '%', 90.0, 100, '90.0%'),
515+
(100, 1, '%', 80.1, 90, '80.1%'),
516+
(100, 1, '%', 70.23, 50, '70.2%'),
517+
# 60.554 instead of 60.55: see https://bugs.python.org/issue5118
518+
(100, 1, '%', 60.554, 40, '60.6%'),
519+
# Check auto decimals over different intervals and values
520+
(100, None, '%', 95, 1, '95.00%'),
521+
(1.0, None, '%', 3, 6, '300%'),
522+
(17.0, None, '%', 1, 8.5, '6%'),
523+
(17.0, None, '%', 1, 8.4, '5.9%'),
524+
(5, None, '%', -100, 0.000001, '-2000.00000%'),
525+
# Check percent symbol
526+
(1.0, 2, None, 1.2, 100, '120.00'),
527+
(75, 3, '', 50, 100, '66.667'),
528+
(42, None, '^^Foobar$$', 21, 12, '50.0^^Foobar$$'),
529+
])
533530
def test_percentformatter(xmax, decimals, symbol, x, display_range, expected):
534531
formatter = mticker.PercentFormatter(xmax, decimals, symbol)
535532
assert formatter.format_pct(x, display_range) == expected
536533

537534

535+
def test_TransformFormatter():
536+
"""
537+
Verifies that the linear transformations are being done correctly.
538+
"""
539+
def transform(x):
540+
return -x
541+
542+
# Make a formatter using the default underlying formatter,
543+
# which is a Formatter instance, not just a generic callable
544+
fmt = mticker.TransformFormatter(transform)
545+
546+
# Public (non-method) attributes
547+
assert fmt.transform is transform
548+
assert isinstance(fmt.formatter, mticker.ScalarFormatter)
549+
550+
# .create_dummy_axis
551+
assert fmt.axis is None
552+
fmt.create_dummy_axis()
553+
assert fmt.axis is not None
554+
assert fmt.axis is fmt.formatter.axis
555+
556+
# .set_axis
557+
prev_axis = fmt.axis
558+
fmt.set_axis(mticker._DummyAxis())
559+
assert fmt.axis is fmt.formatter.axis
560+
assert fmt.axis is not prev_axis
561+
562+
# .set_view_interval
563+
fmt.set_view_interval(100, 200)
564+
assert np.array_equal(fmt.axis.get_view_interval(), [100, 200])
565+
566+
# .set_data_interval
567+
fmt.set_data_interval(50, 60)
568+
assert np.array_equal(fmt.axis.get_data_interval(), [50, 60])
569+
570+
# .set_bounds
571+
bounds = [-7, 7]
572+
fmt.set_bounds(*bounds)
573+
assert np.array_equal(fmt.axis.get_view_interval(), bounds)
574+
assert np.array_equal(fmt.axis.get_data_interval(), bounds)
575+
576+
# .format_data, .format_data_short
577+
assert fmt.format_data(100.0) == '\u22121e2'
578+
assert fmt.format_data_short(-200.0) == '{:<12g}'.format(200)
579+
580+
# .get_offset
581+
assert fmt.get_offset() == fmt.formatter.get_offset()
582+
583+
# .set_locs
584+
locs = [1.0, 2.0, 3.0]
585+
transformed_locs = [-1.0, -2.0, -3.0]
586+
fmt.set_locs(locs)
587+
assert fmt.locs is locs
588+
assert fmt.formatter.locs == transformed_locs
589+
590+
# .fix_minus
591+
val = '-19.0'
592+
assert fmt.fix_minus(val) == '\u221219.0'
593+
assert fmt.fix_minus(val) == fmt.formatter.fix_minus(val)
594+
595+
# .__call__ needs to be tested after `set_locs` has been called at
596+
# least once.
597+
assert fmt(5.0) == '\u22125'
598+
599+
# .set_formatter
600+
prev_axis = fmt.axis
601+
fmt.set_formatter(mticker.PercentFormatter())
602+
assert isinstance(fmt.formatter, mticker.PercentFormatter)
603+
assert fmt.axis is prev_axis
604+
assert fmt.formatter.axis is fmt.axis
605+
assert fmt.locs is locs
606+
assert fmt.formatter.locs == transformed_locs
607+
608+
538609
def test_EngFormatter_formatting():
539610
"""
540611
Create two instances of EngFormatter with default parameters, with and

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy