diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py
index 7193d1e7edf6..04dea31e2177 100644
--- a/lib/matplotlib/scale.py
+++ b/lib/matplotlib/scale.py
@@ -8,7 +8,7 @@
from matplotlib.cbook import dedent
from matplotlib.ticker import (NullFormatter, ScalarFormatter,
- LogFormatterMathtext, LogitFormatter)
+ LogFormatterSciNotation, LogitFormatter)
from matplotlib.ticker import (NullLocator, LogLocator, AutoLocator,
SymmetricalLogLocator, LogitLocator)
from matplotlib.transforms import Transform, IdentityTransform
@@ -304,9 +304,9 @@ def set_default_locators_and_formatters(self, axis):
log scaling.
"""
axis.set_major_locator(LogLocator(self.base))
- axis.set_major_formatter(LogFormatterMathtext(self.base))
+ axis.set_major_formatter(LogFormatterSciNotation(self.base))
axis.set_minor_locator(LogLocator(self.base, self.subs))
- axis.set_minor_formatter(NullFormatter())
+ axis.set_minor_formatter(LogFormatterSciNotation(self.base))
def get_transform(self):
"""
@@ -462,7 +462,7 @@ def set_default_locators_and_formatters(self, axis):
symmetrical log scaling.
"""
axis.set_major_locator(SymmetricalLogLocator(self.get_transform()))
- axis.set_major_formatter(LogFormatterMathtext(self.base))
+ axis.set_major_formatter(LogFormatterSciNotation(self.base))
axis.set_minor_locator(SymmetricalLogLocator(self.get_transform(),
self.subs))
axis.set_minor_formatter(NullFormatter())
diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf
index c76b653c33fb..03284a4e4ee0 100644
Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf differ
diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png
index 876b47caedb6..92d8776d6cc4 100644
Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png and b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png differ
diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg
index 2e63e7be1870..c6efd3710792 100644
--- a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg
+++ b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg
@@ -27,7 +27,7 @@ z
" style="fill:#ffffff;"/>
-
+" id="m2d4ddba423" style="stroke:#000000;stroke-width:0.5;"/>
-
+
+" id="m4e4fc712fc" style="stroke:#000000;stroke-width:0.5;"/>
-
+
@@ -181,12 +181,12 @@ Q 19.53125 74.21875 31.78125 74.21875
-
+
-
+
@@ -215,12 +215,12 @@ z
-
+
-
+
@@ -261,176 +261,241 @@ Q 31.109375 20.453125 19.1875 8.296875
+" id="m1acb1eab51" style="stroke:#000000;stroke-width:0.5;"/>
-
+
+" id="mb4079269e1" style="stroke:#000000;stroke-width:0.5;"/>
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -441,23 +506,23 @@ L 0 2
+" id="md4f0ea9b24" style="stroke:#000000;stroke-width:0.5;"/>
-
+
+" id="mb109f2c327" style="stroke:#000000;stroke-width:0.5;"/>
-
+
-
+
-
+
-
+
-
+
@@ -530,15 +595,15 @@ z
-
+
-
+
-
+
@@ -553,104 +618,127 @@ z
+" id="m6ccf459a85" style="stroke:#000000;stroke-width:0.5;"/>
-
+
+" id="m4cdb1537b0" style="stroke:#000000;stroke-width:0.5;"/>
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -658,7 +746,7 @@ L -2 0
-
+
diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py
index 59deeaef892a..8a4833d5ee40 100644
--- a/lib/matplotlib/tests/test_ticker.py
+++ b/lib/matplotlib/tests/test_ticker.py
@@ -207,6 +207,48 @@ def check_offset_for(left, right, offset):
yield check_offset_for, right, left, offset
+def _sub_labels(axis, subs=()):
+ "Test whether locator marks subs to be labeled"
+ fmt = axis.get_minor_formatter()
+ minor_tlocs = axis.get_minorticklocs()
+ fmt.set_locs(minor_tlocs)
+ coefs = minor_tlocs / 10**(np.floor(np.log10(minor_tlocs)))
+ label_expected = [np.round(c) in subs for c in coefs]
+ label_test = [fmt(x) != '' for x in minor_tlocs]
+ assert_equal(label_test, label_expected)
+
+
+@cleanup
+def test_LogFormatter_sublabel():
+ # test label locator
+ fig, ax = plt.subplots()
+ ax.set_xscale('log')
+ ax.xaxis.set_major_locator(mticker.LogLocator(base=10, subs=[]))
+ ax.xaxis.set_minor_locator(mticker.LogLocator(base=10,
+ subs=np.arange(2, 10)))
+ ax.xaxis.set_major_formatter(mticker.LogFormatter())
+ ax.xaxis.set_minor_formatter(mticker.LogFormatter(labelOnlyBase=False))
+ # axis range above 3 decades, only bases are labeled
+ ax.set_xlim(1, 1e4)
+ fmt = ax.xaxis.get_major_formatter()
+ fmt.set_locs(ax.xaxis.get_majorticklocs())
+ show_major_labels = [fmt(x) != '' for x in ax.xaxis.get_majorticklocs()]
+ assert np.all(show_major_labels)
+ _sub_labels(ax.xaxis, subs=[])
+
+ # axis range at 2 to 3 decades, label sub 3
+ ax.set_xlim(1, 800)
+ _sub_labels(ax.xaxis, subs=[3])
+
+ # axis range at 1 to 2 decades, label subs 2 and 5
+ ax.set_xlim(1, 80)
+ _sub_labels(ax.xaxis, subs=[2, 5])
+
+ # axis range at 0 to 1 decades, label subs 2, 3, 6
+ ax.set_xlim(1, 8)
+ _sub_labels(ax.xaxis, subs=[2, 3, 6])
+
+
def _logfe_helper(formatter, base, locs, i, expected_result):
vals = base**locs
labels = [formatter(x, pos) for (x, pos) in zip(vals, i)]
@@ -252,6 +294,37 @@ def get_view_interval(self):
yield _logfe_helper, formatter, base, locs, i, expected_result
+def test_LogFormatterSciNotation():
+ test_cases = {
+ 10: (
+ (1e-05, '${10^{-5}}$'),
+ (1, '${10^{0}}$'),
+ (100000, '${10^{5}}$'),
+ (2e-05, '${2\\times10^{-5}}$'),
+ (2, '${2\\times10^{0}}$'),
+ (200000, '${2\\times10^{5}}$'),
+ (5e-05, '${5\\times10^{-5}}$'),
+ (5, '${5\\times10^{0}}$'),
+ (500000, '${5\\times10^{5}}$'),
+ ),
+ 2: (
+ (0.03125, '${2^{-5}}$'),
+ (1, '${2^{0}}$'),
+ (32, '${2^{5}}$'),
+ (0.0375, '${1.2\\times2^{-5}}$'),
+ (1.2, '${1.2\\times2^{0}}$'),
+ (38.4, '${1.2\\times2^{5}}$'),
+ )
+ }
+
+ for base in test_cases.keys():
+ formatter = mticker.LogFormatterSciNotation(base=base)
+ formatter.sublabel = set([1, 2, 5, 1.2])
+ for value, expected in test_cases[base]:
+ with matplotlib.rc_context({'text.usetex': False}):
+ nose.tools.assert_equal(formatter(value), expected)
+
+
def _pprint_helper(value, domain, expected):
fmt = mticker.LogFormatter()
label = fmt.pprint_val(value, domain)
diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py
index a2be7cbd805e..4bd1a36148dc 100644
--- a/lib/matplotlib/ticker.py
+++ b/lib/matplotlib/ticker.py
@@ -820,6 +820,7 @@ def __init__(self, base=10.0, labelOnlyBase=True):
"""
self._base = base + 0.0
self.labelOnlyBase = labelOnlyBase
+ self.sublabel = [1, ]
def base(self, base):
"""
@@ -839,12 +840,46 @@ def label_minor(self, labelOnlyBase):
"""
self.labelOnlyBase = labelOnlyBase
+ def set_locs(self, locs):
+ b = self._base
+
+ vmin, vmax = self.axis.get_view_interval()
+ self.d = abs(vmax - vmin)
+
+ if not hasattr(self.axis, 'get_transform'):
+ # This might be a colorbar dummy axis, do not attempt to get
+ # transform
+ numdec = 10
+ elif hasattr(self.axis.get_transform(), 'linthresh'):
+ t = self.axis.get_transform()
+ linthresh = t.linthresh
+ # Only compute the number of decades in the logarithmic part of the
+ # axis
+ numdec = 0
+ if vmin < -linthresh:
+ numdec += math.log(-vmin / linthresh) / math.log(b)
+
+ if vmax > linthresh and vmin < linthresh:
+ numdec += math.log(vmax / linthresh) / math.log(b)
+ elif vmin >= linthresh:
+ numdec += math.log(vmax / vmin) / math.log(b)
+ else:
+ vmin = math.log(vmin) / math.log(b)
+ vmax = math.log(vmax) / math.log(b)
+ numdec = abs(vmax - vmin)
+
+ if numdec > 3:
+ # Label only bases
+ self.sublabel = set((1,))
+ else:
+ # Add labels between bases at log-spaced coefficients
+ c = np.logspace(0, 1, (4 - int(numdec)) + 1, base=b)
+ self.sublabel = set(np.round(c))
+
def __call__(self, x, pos=None):
"""
Return the format for tick val `x` at position `pos`.
"""
- vmin, vmax = self.axis.get_view_interval()
- d = abs(vmax - vmin)
b = self._base
if x == 0.0:
return '0'
@@ -852,16 +887,21 @@ def __call__(self, x, pos=None):
# only label the decades
fx = math.log(abs(x)) / math.log(b)
isDecade = is_close_to_int(fx)
- if not isDecade and self.labelOnlyBase:
- s = ''
- elif x > 10000:
- s = '%1.0e' % x
- elif x < 1:
- s = '%1.0e' % x
+ exponent = np.round(fx) if isDecade else np.floor(fx)
+ coeff = np.round(x / b ** exponent)
+ if coeff in self.sublabel:
+ if not isDecade and self.labelOnlyBase:
+ s = ''
+ elif x > 10000:
+ s = '%1.0e' % x
+ elif x < 1:
+ s = '%1.0e' % x
+ else:
+ s = self.pprint_val(x, self.d)
+ if sign == -1:
+ s = '-%s' % s
else:
- s = self.pprint_val(x, d)
- if sign == -1:
- s = '-%s' % s
+ s = ''
return self.fix_minus(s)
@@ -951,6 +991,14 @@ class LogFormatterMathtext(LogFormatter):
Format values for log axis using ``exponent = log_base(value)``.
"""
+ def _non_decade_format(self, sign_string, base, fx, usetex):
+ 'Return string for non-decade locations'
+ if usetex:
+ return (r'$%s%s^{%.2f}$') % (sign_string, base, fx)
+ else:
+ return ('$%s$' % _mathdefault('%s%s^{%.2f}' %
+ (sign_string, base, fx)))
+
def __call__(self, x, pos=None):
"""
Return the format for tick value `x`.
@@ -969,6 +1017,8 @@ def __call__(self, x, pos=None):
fx = math.log(abs(x)) / math.log(b)
is_decade = is_close_to_int(fx)
+ exponent = np.round(fx) if is_decade else np.floor(fx)
+ coeff = np.round(x / b ** exponent)
sign_string = '-' if x < 0 else ''
@@ -978,25 +1028,46 @@ def __call__(self, x, pos=None):
else:
base = '%s' % b
- if not is_decade and self.labelOnlyBase:
- return ''
- elif not is_decade:
- if usetex:
- return (r'$%s%s^{%.2f}$') % \
- (sign_string, base, fx)
+ if coeff in self.sublabel:
+ if not is_decade and self.labelOnlyBase:
+ return ''
+ elif not is_decade:
+ return self._non_decade_format(sign_string, base, fx, usetex)
else:
- return ('$%s$' % _mathdefault(
- '%s%s^{%.2f}' %
- (sign_string, base, fx)))
+ if usetex:
+ return (r'$%s%s^{%d}$') % (sign_string,
+ base,
+ nearest_long(fx))
+ else:
+ return ('$%s$' % _mathdefault(
+ '%s%s^{%d}' %
+ (sign_string, base, nearest_long(fx))))
else:
- if usetex:
- return (r'$%s%s^{%d}$') % (sign_string,
- base,
- nearest_long(fx))
- else:
- return ('$%s$' % _mathdefault(
- '%s%s^{%d}' %
- (sign_string, base, nearest_long(fx))))
+ return ''
+
+
+class LogFormatterSciNotation(LogFormatterMathtext):
+ """
+ Format values following scientific notation in a logarithmic axis
+ """
+
+ def __init__(self, base=10.0, labelOnlyBase=False):
+ super(LogFormatterSciNotation, self).__init__(base=base,
+ labelOnlyBase=labelOnlyBase)
+
+ def _non_decade_format(self, sign_string, base, fx, usetex):
+ 'Return string for non-decade locations'
+ b = float(base)
+ exponent = math.floor(fx)
+ coeff = b ** fx / b ** exponent
+ if is_close_to_int(coeff):
+ coeff = nearest_long(coeff)
+ if usetex:
+ return (r'$%g\times%s^{%d}$') % \
+ (coeff, base, exponent)
+ else:
+ return ('$%s$' % _mathdefault(r'%g\times%s^{%d}' %
+ (coeff, base, exponent)))
class LogitFormatter(Formatter):