Skip to content

Commit e3eb134

Browse files
committed
DEV: Added three new Formatters to mpl.ticker
- BinaryIntFormatter - HexIntFormatter - LinearScaleFormatter Also, changed the annotation in PercentFormatter tests slightly (non-functional change).
1 parent 22cf7e0 commit e3eb134

File tree

4 files changed

+297
-33
lines changed

4 files changed

+297
-33
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Four new Formatters added to `matplotlib.ticker`
2+
-----------------------------------------------
3+
4+
Four new formatters have been added for displaying some specialized
5+
tick labels:
6+
7+
- :class:`matplotlib.ticker.PercentFormatter`
8+
- :class:`matplotlib.ticker.LinearScaleFormatter`
9+
- :class:`matplotlib.ticker.BinaryIntFormatter`
10+
- :class:`matplotlib.ticker.HexIntFormatter`
11+
12+
13+
:class:`matplotlib.ticker.PercentFormatter`
14+
```````````````````````````````````````````
15+
16+
This new formatter has some nice features like being able to convert
17+
from arbitrary data scales to percents, a customizable percent symbol
18+
and either automatic or manual control over the decimal points.
19+
20+
21+
:class:`matplotlib.ticker.LinearScaleFormatter`
22+
```````````````````````````````````````````````
23+
24+
This is a wrapper around any other formatter of the user's choice.
25+
However, instead of passing in the actual tick values, it performs a
26+
linear transformation on them first.
27+
28+
29+
:class:`matplotlib.ticker.BinaryIntFormatter`
30+
`````````````````````````````````````````````
31+
32+
Displays ticks as binary numbers with a given precision. This formatter
33+
is intended to be used with :class:`matplotlib.ticker.MaxNLocator` with
34+
the ``integer`` parameter set to ``True``.
35+
36+
37+
:class:`matplotlib.ticker.HexIntFormatter`
38+
``````````````````````````````````````````
39+
40+
Displays ticks as hexadecimal numbers with a given precision. This
41+
formatter is intended to be used with
42+
:class:`matplotlib.ticker.MaxNLocator` with the ``integer`` parameter
43+
set to ``True``.
44+

doc/users/whats_new/percent_formatter.rst

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

lib/matplotlib/tests/test_ticker.py

Lines changed: 111 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -504,37 +504,123 @@ 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+
@pytest.mark.parametrize('n, unsigned, value, expected', [
536+
# Positive, <8 bits
537+
(8, False, 25, '00011001'),
538+
(8, True, 25, '00011001'),
539+
# Positive, >8 bits
540+
(8, False, 257, '100000001'),
541+
(8, True, 257, '100000001'),
542+
# Positive float truncation
543+
(8, False, 25.3, '00011001'),
544+
(8, True, 25.3, '00011001'),
545+
(8, False, 257.99, '100000001'),
546+
(8, True, 257.99, '100000001'),
547+
# Negative <8 bits
548+
(8, False, -25, '-00011001'),
549+
(8, True, -25, '11100111'),
550+
# Negative >8 bits
551+
(8, False, -257, '-100000001'),
552+
(8, True, -257, '11111111'),
553+
# Negative float truncation
554+
(8, False, -25.3, '-00011001'),
555+
(8, True, -25.3, '11100111'),
556+
(8, False, -257.99, '-100000001'),
557+
(8, True, -257.99, '111111111'),
558+
])
559+
def test_BinaryIntFormatter(n, unsigned, value, expected):
560+
"""
561+
Verifies that numbers get converted to binary correctly, and that
562+
floating point numbers get truncated.
563+
"""
564+
fmt = mticker.BinaryIntFormatter(n, unsigned)
565+
assert fmt(value) == expected
566+
567+
568+
@pytest.mark.parametrize('n, unsigned, value, expected',[
569+
# Positive, <4 digits
570+
(4, False, 37, '0025'),
571+
(4, True, 37, '0025'),
572+
# Positive, >4 digits
573+
(4, False, 341041, '53431'),
574+
(4, True, 341041, '53431'),
575+
# Positive float truncation
576+
(4, False, 37.8, '0025'),
577+
(4, True, 37.8, '0025'),
578+
(4, False, 341041.45, '53431'),
579+
(4, True, 341041.45, '53431'),
580+
# Negative <4 digits
581+
(4, False, -37, '-0025'),
582+
(4, True, -37, 'FFDB'),
583+
# Negative >4 digits
584+
(4, False, -341041, '-0025'),
585+
(4, True, -341041, 'CBCF'),
586+
# Negative float truncation
587+
(4, False, -37.8, '-0025'),
588+
(4, True, -37.8, 'FFDB'),
589+
(4, False, -341041.45, '-53431'),
590+
(4, True, -341041.45, 'CBCF'),
591+
])
592+
def test_HexIntFormatter(n, unsigned, value, expected):
593+
"""
594+
Verifies that numbers get converted to hex correctly, and that
595+
floating point numbers get truncated.
596+
"""
597+
fmt = mticker.HexIntFormatter(n, unsigned)
598+
assert fmt(value) == expected
599+
600+
601+
def test_LinearScaleFormatter():
602+
"""
603+
Verifies that the linear transformations are being done correctly.
604+
"""
605+
# Make a formatter that truncates to two decimal places always
606+
base = lambda x, pos=None: '{:0.2f}'.format(x)
607+
608+
inputs = np.arange(-2.5, 13, 2.5)
609+
outputs = ['{:0.2f}'.format(x) for x in np.arange(7.0)]
610+
611+
# Check a simple case
612+
fmt = mticker.LinearScaleFormatter(10, (1, 5), formatter=base)
613+
for i, o in zip(inputs, outputs):
614+
print(i, fmt(i), o)
615+
assert fmt(i) == o
616+
617+
# Check a reverse mapping
618+
fmt = mticker.LinearScaleFormatter(10, (5, 1), formatter=base)
619+
for i, o in zip(inputs, reversed(outputs)):
620+
print(i, fmt(i), o)
621+
assert fmt(i) == o
622+
623+
538624
def test_EngFormatter_formatting():
539625
"""
540626
Create two instances of EngFormatter with default parameters, with and

lib/matplotlib/ticker.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@
154154
:class:`PercentFormatter`
155155
Format labels as a percentage
156156
157+
:class:`BinaryIntFormatter`
158+
Format labels as binary integers.
159+
160+
:class:`HexIntFormatter`
161+
Format labels as hexadecimal integers.
162+
163+
:class:`LinearScaleFormatter`
164+
Wrap another formatter to display transformed values.
165+
157166
You can derive your own formatter from the Formatter base class by
158167
simply overriding the ``__call__`` method. The formatter class has
159168
access to the axis view and data limits.
@@ -195,8 +204,9 @@
195204
'LogFormatterExponent', 'LogFormatterMathtext',
196205
'IndexFormatter', 'LogFormatterSciNotation',
197206
'LogitFormatter', 'EngFormatter', 'PercentFormatter',
198-
'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator',
199-
'LinearLocator', 'LogLocator', 'AutoLocator',
207+
'LinearScaleFormatter', 'BinaryIntFormatter',
208+
'HexIntFormatter', 'Locator', 'IndexLocator', 'FixedLocator',
209+
'NullLocator', 'LinearLocator', 'LogLocator', 'AutoLocator',
200210
'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator',
201211
'SymmetricalLogLocator', 'LogitLocator')
202212

@@ -1292,6 +1302,136 @@ def convert_to_pct(self, x):
12921302
return 100.0 * (x / self.xmax)
12931303

12941304

1305+
class BinaryIntFormatter(Formatter):
1306+
"""
1307+
Formats the input as an ``n``-bit binary number, where ``n`` is
1308+
specified in the constructor.
1309+
1310+
``n`` just specifies the minimum width to which numbers will be
1311+
padded with leading zeros. Numbers containing more than ``n`` bits
1312+
are allowed.
1313+
1314+
``unsigned`` determines if negative numbers are printed with a minus sign
1315+
or if their binary representation is truncated to ``n`` bits and treated
1316+
as a positive number.
1317+
1318+
The input will be truncated to an integer, so this `Formatter` is best used
1319+
with a `MaxNLocator` with ``integer=True``.
1320+
"""
1321+
def __init__(self, n, unsigned=False):
1322+
self.n = n
1323+
self.unsigned = unsigned
1324+
1325+
def format(self, x):
1326+
n = self.n
1327+
if x < 0:
1328+
if self.unsigned:
1329+
x = x & ((1 << n) - 1)
1330+
else:
1331+
n = self.n + 1
1332+
return '{x:0{n}b}'.format(x=0, n=n)
1333+
1334+
def __call__(self, x, pos=None):
1335+
return self.format(x)
1336+
1337+
1338+
class HexIntFormatter(Formatter):
1339+
"""
1340+
Formats the input as an ``n``-digit hexadecimal number, where ``n``
1341+
is specified in the constructor.
1342+
1343+
``n`` just specifies the minimum width to which numbers will be
1344+
padded with leading zeros. Numbers containing more than ``n`` digits
1345+
are allowed.
1346+
1347+
``unsigned`` determines if negative numbers are printed with a minus sign
1348+
or if their binary representation is truncated to ``n`` hex digits and
1349+
treated as a positive number.
1350+
1351+
The input will be truncated to an integer, so this `Formatter` is best
1352+
used with a `MaxNLocator` with ``integer=True``.
1353+
"""
1354+
def __init__(self, n, unsigned=False):
1355+
self.n = n
1356+
self.unsigned = unsigned
1357+
1358+
def format(self, x):
1359+
n = self.n
1360+
if x < 0:
1361+
if self.unsigned:
1362+
x = x & ((1 << (4 * n)) - 1)
1363+
else:
1364+
n = self.n + 1
1365+
return '{x:0{n}X}'.format(x=0, n=n)
1366+
1367+
def __call__(self, x, pos=None):
1368+
return self.format(x)
1369+
1370+
1371+
class LinearScaleFormatter(Formatter):
1372+
"""
1373+
Displays labels that are scaled and offset versions of the actual
1374+
axis values.
1375+
1376+
This formatter can use any other formatter to actually render the
1377+
ticks.
1378+
1379+
inRef: number or 2-element iterable
1380+
Bounds on the input range to match to the output range. These
1381+
numbers do not actually restrict the input in any way. They are
1382+
just reference points. If the range is a scalar, it will be
1383+
interpreted as ``(0, inRef)``.
1384+
outRef: number or 2-element iterable
1385+
Bounds on the output range to match to the input range. These
1386+
numbers do not actually restrict the output in any way. They are
1387+
just reference points. If the range is a scalar, it will be
1388+
interpreted as ``(0, outRef)``.
1389+
formatter: matplotlib.ticker.Formatter
1390+
The instance to delegate the actual formatting of the
1391+
transformed value to.
1392+
"""
1393+
def __init__(self, inRef=1.0, outRef=1.0, formatter=ScalarFormatter()):
1394+
def unpack(ref, name):
1395+
if np.iterable(ref):
1396+
ref = tuple(ref)
1397+
if len(ref) != 2:
1398+
raise ValueError('Expected 2-element iterable for `{}`, '
1399+
'got {}.'.format(name, len(ref)))
1400+
return ref
1401+
return 0, ref
1402+
1403+
# All these values are retained for debugging/extension.
1404+
# Only the minima are used explicitly.
1405+
self.iMin, self.iMax = unpack(inRef, 'in')
1406+
self.oMin, self.oMax = unpack(outRef, 'out')
1407+
self.formatter = formatter
1408+
1409+
# Precomputing the values that are used in addition to the minima
1410+
self.iRange = self.iMax - self.iMin
1411+
self.oRange = self.oMax - self.oMin
1412+
1413+
def transform(self, x):
1414+
"""
1415+
Transforms a value from the input scale to the output scale.
1416+
"""
1417+
return (x - self.iMin) / self.iRange * self.oRange + self.oMin
1418+
1419+
def __call__(self, x, pos=None):
1420+
return self.formatter(self.transform(x), pos)
1421+
1422+
def set_axis(self, ax):
1423+
self.formatter.set_axis(ax)
1424+
1425+
def get_offset(self):
1426+
return self.formatter.get_offset()
1427+
1428+
def set_locs(self, locs):
1429+
self.formatter.set_locs([self.transform(x) for x in locs])
1430+
1431+
def fix_minus(self, s):
1432+
return self.formatter.fix_minus(s)
1433+
1434+
12951435
class Locator(TickHelper):
12961436
"""
12971437
Determine the tick locations;

0 commit comments

Comments
 (0)