3
3
Various `~matplotlib.ticker.Locator` and `~matplotlib.ticker.Formatter`
4
4
classes.
5
5
"""
6
+ import re
6
7
import numpy as np
7
8
import matplotlib .ticker as mticker
9
+ import locale
8
10
from fractions import Fraction
9
11
from .internals import ic # noqa: F401
10
12
from .internals import _not_none
16
18
'SimpleFormatter' ,
17
19
]
18
20
19
-
20
- def _sanitize_label (string , zerotrim = False ):
21
- """
22
- Sanitize tick label strings.
23
- """
24
- if zerotrim and '.' in string :
25
- string = string .rstrip ('0' ).rstrip ('.' )
26
- string = string .replace ('-' , '\N{MINUS SIGN} ' )
27
- if string == '\N{MINUS SIGN} 0' :
28
- string = '0'
29
- return string
21
+ REGEX_ZERO = re .compile ('\\ A[-\N{MINUS SIGN} ]?0(.0*)?\\ Z' )
22
+ REGEX_MINUS = re .compile ('\\ A[-\N{MINUS SIGN} ]\\ Z' )
23
+ REGEX_MINUS_ZERO = re .compile ('\\ A[-\N{MINUS SIGN} ]0(.0*)?\\ Z' )
30
24
31
25
32
26
class AutoFormatter (mticker .ScalarFormatter ):
33
27
"""
34
- The new default formatter, a simple wrapper around
35
- `~matplotlib.ticker.ScalarFormatter`. Differs from
36
- `~matplotlib.ticker.ScalarFormatter` in the following ways:
37
-
38
- 1. Trims trailing zeros if any exist.
39
- 2. Allows user to specify *range* within which major tick marks
40
- are labelled.
41
- 3. Allows user to add arbitrary prefix or suffix to every
42
- tick label string.
28
+ The new default formatter. Differs from `~matplotlib.ticker.ScalarFormatter`
29
+ in the following ways:
30
+
31
+ 1. Trims trailing decimal zeros by default.
32
+ 2. Permits specifying *range* within which major tick marks are labeled.
33
+ 3. Permits adding arbitrary prefix or suffix to every tick label string.
34
+ 4. Permits adding "negative" and "positive" indicator.
43
35
"""
44
36
def __init__ (
45
- self , * args ,
37
+ self ,
46
38
zerotrim = None , tickrange = None ,
47
39
prefix = None , suffix = None , negpos = None , ** kwargs
48
40
):
@@ -63,7 +55,7 @@ def __init__(
63
55
64
56
Other parameters
65
57
----------------
66
- *args, * *kwargs
58
+ **kwargs
67
59
Passed to `~matplotlib.ticker.ScalarFormatter`.
68
60
69
61
Warning
@@ -75,7 +67,7 @@ def __init__(
75
67
this behavior with a patch.
76
68
"""
77
69
tickrange = tickrange or (- np .inf , np .inf )
78
- super ().__init__ (* args , * *kwargs )
70
+ super ().__init__ (** kwargs )
79
71
from .config import rc
80
72
zerotrim = _not_none (zerotrim , rc ['axes.formatter.zerotrim' ])
81
73
self ._zerotrim = zerotrim
@@ -96,55 +88,135 @@ def __call__(self, x, pos=None):
96
88
The position.
97
89
"""
98
90
# Tick range limitation
99
- eps = abs (x ) / 1000
100
- tickrange = self ._tickrange
101
- if (x + eps ) < tickrange [0 ] or (x - eps ) > tickrange [1 ]:
102
- return '' # avoid some ticks
91
+ if self ._outside_tick_range (x , self ._tickrange ):
92
+ return ''
103
93
104
94
# Negative positive handling
105
- if not self ._negpos or x == 0 :
106
- tail = ''
107
- elif x > 0 :
108
- tail = self ._negpos [1 ]
109
- else :
110
- x *= - 1
111
- tail = self ._negpos [0 ]
95
+ x , tail = self ._neg_pos_format (x , self ._negpos )
112
96
113
97
# Default string formatting
114
98
string = super ().__call__ (x , pos )
115
- string = _sanitize_label (string , zerotrim = self ._zerotrim )
116
99
100
+ # Fix issue where non-zero string is formatted as zero
101
+ string = self ._fix_small_number (x , string )
102
+
103
+ # Custom string formatting
104
+ string = self ._minus_format (string )
105
+ if self ._zerotrim :
106
+ string = self ._trim_trailing_zeros (string , self .get_useLocale ())
107
+
108
+ # Prefix and suffix
109
+ string = self ._add_prefix_suffix (string , self ._prefix , self ._suffix )
110
+ string = string + tail # add negative-positive indicator
111
+ return string
112
+
113
+ @staticmethod
114
+ def _add_prefix_suffix (string , prefix = None , suffix = None ):
115
+ """
116
+ Add prefix and suffix to string.
117
+ """
118
+ sign = ''
119
+ prefix = prefix or ''
120
+ suffix = suffix or ''
121
+ if string and REGEX_MINUS .match (string [0 ]):
122
+ sign , string = string [0 ], string [1 :]
123
+ return sign + prefix + string + suffix
124
+
125
+ @staticmethod
126
+ def _fix_small_number (x , string , offset = 2 ):
127
+ """
128
+ Fix formatting for non-zero number that gets formatted as zero. The `offset`
129
+ controls the offset from the true floating point precision at which we want
130
+ to limit maximum precision of the string.
131
+ """
117
132
# Add just enough precision for small numbers. Default formatter is
118
133
# only meant to be used for linear scales and cannot handle the wide
119
134
# range of magnitudes in e.g. log scales. To correct this, we only
120
135
# truncate if value is within one order of magnitude of the float
121
136
# precision. Common issue is e.g. levels=plot.arange(-1, 1, 0.1).
122
137
# This choice satisfies even 1000 additions of 0.1 to -100.
123
- # Example code:
124
- # def add(x, decimals=1, type_=np.float64):
125
- # step = type_(10 ** -decimals)
126
- # y = type_(x) + step
127
- # if np.round(y, decimals) == 0:
128
- # return y
129
- # else:
130
- # return add(y, decimals)
131
- # num = abs(add(-200, 1, float))
132
- # precision = abs(np.log10(num) // 1) - 1
133
- # ('{:.%df}' % precision).format(num)
134
- if string == '0' and x != 0 :
135
- string = (
136
- '{:.%df}' % min (
137
- int (abs (np .log10 (abs (x )) // 1 )),
138
- np .finfo (type (x )).precision - 1
139
- )
140
- ).format (x )
141
- string = _sanitize_label (string , zerotrim = self ._zerotrim )
138
+ match = REGEX_ZERO .match (string )
139
+ decimal_point = AutoFormatter ._get_decimal_point ()
142
140
143
- # Prefix and suffix
144
- sign = ''
145
- if string and string [0 ] == '\N{MINUS SIGN} ' :
146
- sign , string = string [0 ], string [1 :]
147
- return sign + self ._prefix + string + self ._suffix + tail
141
+ if match and x != 0 :
142
+ # Get initial precision spit out by algorithm
143
+ decimals , = match .groups ()
144
+ if decimals :
145
+ precision_init = len (decimals .lstrip (decimal_point ))
146
+ else :
147
+ precision_init = 0
148
+
149
+ # Format with precision below floating point error
150
+ precision_true = int (abs (np .log10 (abs (x )) // 1 ))
151
+ precision_max = np .finfo (type (x )).precision - offset
152
+ precision = min (precision_true , precision_max )
153
+ string = ('{:.%df}' % precision ).format (x )
154
+
155
+ # If number is zero after ignoring floating point error, generate
156
+ # zero with precision matching original string.
157
+ if REGEX_ZERO .match (string ):
158
+ string = ('{:.%df}' % precision_init ).format (0 )
159
+
160
+ # Fix decimal point
161
+ string = string .replace ('.' , decimal_point )
162
+
163
+ return string
164
+
165
+ @staticmethod
166
+ def _get_decimal_point (use_locale = None ):
167
+ """
168
+ Get decimal point symbol for current locale (e.g. in Europe will be comma).
169
+ """
170
+ from .config import rc
171
+ use_locale = _not_none (use_locale , rc ['axes.formatter.use_locale' ])
172
+ if use_locale :
173
+ return locale .localeconv ()['decimal_point' ]
174
+ else :
175
+ return '.'
176
+
177
+ @staticmethod
178
+ def _minus_format (string ):
179
+ """
180
+ Format the minus sign and avoid "negative zero," e.g. ``-0.000``.
181
+ """
182
+ from .config import rc
183
+ if rc ['axes.unicode_minus' ] and not rc ['text.usetex' ]:
184
+ string = string .replace ('-' , '\N{MINUS SIGN} ' )
185
+ if REGEX_MINUS_ZERO .match (string ):
186
+ string = string [1 :]
187
+ return string
188
+
189
+ @staticmethod
190
+ def _neg_pos_format (x , negpos ):
191
+ """
192
+ Permit suffixes indicators for "negative" and "positive" numbers.
193
+ """
194
+ if not negpos or x == 0 :
195
+ tail = ''
196
+ elif x > 0 :
197
+ tail = negpos [1 ]
198
+ else :
199
+ x *= - 1
200
+ tail = negpos [0 ]
201
+ return x , tail
202
+
203
+ @staticmethod
204
+ def _outside_tick_range (x , tickrange ):
205
+ """
206
+ Return whether point is outside tick range up to some precision.
207
+ """
208
+ eps = abs (x ) / 1000
209
+ return (x + eps ) < tickrange [0 ] or (x - eps ) > tickrange [1 ]
210
+
211
+ @staticmethod
212
+ def _trim_trailing_zeros (string , use_locale = None ):
213
+ """
214
+ Sanitize tick label strings.
215
+ """
216
+ decimal_point = AutoFormatter ._get_decimal_point ()
217
+ if decimal_point in string :
218
+ string = string .rstrip ('0' ).rstrip (decimal_point )
219
+ return string
148
220
149
221
150
222
def SigFigFormatter (sigfig = 1 , zerotrim = False ):
0 commit comments