Skip to content

ENH: Added a new Formatter to mpl.ticker and some example recipes #7482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions doc/users/whats_new/new_formatters.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Two new Formatters added to `matplotlib.ticker`
-----------------------------------------------

Two new formatters have been added for displaying some specialized
tick labels:

- :class:`matplotlib.ticker.PercentFormatter`
- :class:`matplotlib.ticker.TransformFormatter`


:class:`matplotlib.ticker.PercentFormatter`
```````````````````````````````````````````

This new formatter has some nice features like being able to convert
from arbitrary data scales to percents, a customizable percent symbol
and either automatic or manual control over the decimal points.


:class:`matplotlib.ticker.TransformFormatter`
```````````````````````````````````````````````

A more generic version of :class:`matplotlib.ticker.FuncFormatter` that
allows the tick values to be transformed before being passed to an
underlying formatter. The transformation can yield results of arbitrary
type, so for example, using `int` as the transformation will allow
:class:`matplotlib.ticker.StrMethodFormatter` to use integer format
strings. If the underlying formatter is an instance of
:class:`matplotlib.ticker.Formatter`, it will be configured correctly
through this class.
6 changes: 0 additions & 6 deletions doc/users/whats_new/percent_formatter.rst

This file was deleted.

29 changes: 12 additions & 17 deletions examples/ticks_and_spines/tick-formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,23 @@ def setup(ax):
ax.set_xlim(0, 5)
ax.set_ylim(0, 1)
ax.patch.set_alpha(0.0)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))


plt.figure(figsize=(8, 6))
n = 7
n = 8

# Null formatter
ax = plt.subplot(n, 1, 1)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.NullFormatter())
ax.xaxis.set_minor_formatter(ticker.NullFormatter())
ax.text(0.0, 0.1, "NullFormatter()", fontsize=16, transform=ax.transAxes)

# Fixed formatter
ax = plt.subplot(n, 1, 2)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.0))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
majors = ["", "0", "1", "2", "3", "4", "5"]
ax.xaxis.set_major_formatter(ticker.FixedFormatter(majors))
minors = [""] + ["%.2f" % (x-int(x)) if (x-int(x))
Expand All @@ -54,8 +52,6 @@ def major_formatter(x, pos):

ax = plt.subplot(n, 1, 3)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.FuncFormatter(major_formatter))
ax.text(0.0, 0.1, 'FuncFormatter(lambda x, pos: "[%.2f]" % x)',
fontsize=15, transform=ax.transAxes)
Expand All @@ -64,40 +60,39 @@ def major_formatter(x, pos):
# FormatStr formatter
ax = plt.subplot(n, 1, 4)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.FormatStrFormatter(">%d<"))
ax.text(0.0, 0.1, "FormatStrFormatter('>%d<')",
fontsize=15, transform=ax.transAxes)

# Scalar formatter
ax = plt.subplot(n, 1, 5)
setup(ax)
ax.xaxis.set_major_locator(ticker.AutoLocator())
ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
ax.xaxis.set_major_formatter(ticker.ScalarFormatter(useMathText=True))
ax.text(0.0, 0.1, "ScalarFormatter()", fontsize=15, transform=ax.transAxes)

# StrMethod formatter
ax = plt.subplot(n, 1, 6)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.StrMethodFormatter("{x}"))
ax.text(0.0, 0.1, "StrMethodFormatter('{x}')",
fontsize=15, transform=ax.transAxes)

# Percent formatter
ax = plt.subplot(n, 1, 7)
setup(ax)
ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00))
ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25))
ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5))
ax.text(0.0, 0.1, "PercentFormatter(xmax=5)",
fontsize=15, transform=ax.transAxes)

# Push the top of the top axes outside the figure because we only show the
# bottom spine.
# TransformFormatter
ax = plt.subplot(n, 1, 8)
setup(ax)
ax.xaxis.set_major_formatter(ticker.TransformFormatter(lambda x: 7 - 2 * x))
ax.text(0.0, 0.1, "TransformFormatter(lambda x: 7 - 2 * x)",
Copy link
Member

@story645 story645 Jan 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😄 though I don't really grok the difference between TransformFormatter vs. FuncFormatter...is it a data space vs. axis space thing?

I wonder, could you do a teeny example that maybe does composition of formatters if that's the big advantage TransformForammter offers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original idea stems from trying to use "{x:x}" as an argument to StrMethodFormatter, which does not work. But now you can do

TransformFormatter(transform=lambda x: int(x), formatter=StrMethodFormatter())`

and everything works out just fine. More details starting with this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if you want to just switch physical units in your data, you can set the transform, but still use the formatter you like without having to rewrite the whole thing. ScalarFormatter.__call__ depends on the bounds of the axis. If you switch units, you'd want the bounds it is working with to be in the units of the transformed data, not the actual. That is basically what TransformFormatter does.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think most of this is covered in the docs I wrote, but I can try to contrive an example if you still think that's necessary. It certainly never hurts to have more documentation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually too much documentation can get overwhelming; the goal is the right sorta documentation. I'm half tempted to let it go though as it's probably not worth holding the PR over the mini-example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works for me, but I would really like #7993 to go first (or get rejected) if that's OK with you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an example of where the linear transformation recipe I made would actually be helpful: http://stackoverflow.com/q/41984104/2988730

fontsize=15, transform=ax.transAxes)

# Push the top of the top axes outside the figure because we only show
# the bottom spine.
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05)

plt.show()
108 changes: 108 additions & 0 deletions examples/ticks_and_spines/tick_transform_formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
Demo of the `matplotlib.ticker.TransformFormatter` class.

This code demonstrates two features:

1. A linear transformation of the input values. A callable class for
doing the transformation is presented as a recipe here. The data
type of the inputs does not change.
2. A transformation of the input type. The example here allows
`matplotlib.ticker.StrMethodFormatter` to handle integer formats
('b', 'o', 'd', 'n', 'x', 'X'), which will normally raise an error
if used directly. This transformation is associated with a
`matplotlib.ticker.MaxNLocator` which has `integer` set to True to
ensure that the inputs are indeed integers.

The same histogram is plotted in two sub-plots with a shared x-axis.
Each axis shows a different temperature scale: one in degrees Celsius,
one in degrees Rankine (the Fahrenheit analogue of Kelvins). This is one
of the few examples of recognized scientific units that have both a
scale and an offset relative to each other.
"""

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axis import Ticker
from matplotlib.ticker import (
TransformFormatter, StrMethodFormatter, MaxNLocator
)


class LinearTransform:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I'm in awe of all the documentation, I wonder if in this case it obscures the use case for this? This is fairly complicated, so at the least is there a simpler 10-20 line use case for the intermediate user that you can turn into a secondary example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The origin behind this file is that the original version of the formatter was doing this linear transformation. I was asked to make the formatter more general and provide the linear transformation as a recipe instead. I have no problem addding a simpler example. Did you have something particular in mind?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, my only request is that it be as few LOC as possible while still being clear and accessible to a wider audience. Maybe a temperature convertor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, you are OK with keeping this particular bit of code around (perhaps in a different location), but would like to see something simpler as the basic example of the new formatter?

Copy link
Member

@story645 story645 Jan 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly!

The simple one could maybe even go in the great big formatter example

"""
A callable class that transforms input values to output according to
a linear transformation.
"""

def __init__(self, in_start=0.0, in_end=None, out_start=0.0, out_end=None):
"""
Sets up the transformation such that `in_start` gets mapped to
`out_start` and `in_end` gets mapped to `out_end`.

Configuration arguments set up the mapping and do not impose
any restriction on the subsequent arguments to `__call__`. None
of the configuration arguments are required.

A missing `in_start` or `out_start` defaults to zero. A missing
`in_end` and `out_end` default to `in_start + 1.0` and
`out_start + 1.0`, respectively.

A simple scaling transformation can be created by only
supplying the end arguments. A translation can be obtained by
only supplying the start arguments.
"""
in_scale = 1.0 if in_end is None else in_end - in_start
out_scale = 1.0 if out_end is None else out_end - out_start

self._scale = out_scale / in_scale
self._offset = out_start - self._scale * in_start

def __call__(self, x):
"""
Transforms the input value `x` according to the rule set up in
`__init__`.
"""
return x * self._scale + self._offset

# X-data
temp_C = np.arange(-5.0, 5.1, 0.25)
# Y-data
counts = 15.0 * np.exp(-temp_C**2 / 25)
# Add some noise
counts += np.random.normal(scale=4.0, size=counts.shape)
if counts.min() < 0:
counts += counts.min()

fig, ax1 = plt.subplots()
ax2 = fig.add_subplot(111, sharex=ax1, sharey=ax1, frameon=False)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to have a shared axis to display this new ticker? That seems to be conflating two separate tasks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary, but the conflation was deliberate. I wanted to be able to display both the original and the transformed values as well as to use this as a demo for #7528. There will be some modifications once that PR gets accepted (which I think is very likely once I find the time to write some additional tests for it).


ax1.plot(temp_C, counts, drawstyle='steps-mid')

ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}'))

# This step is necessary to allow the shared x-axes to have different
# Formatter and Locator objects.
ax2.xaxis.major = Ticker()
# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition)
ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator())
ax2.xaxis.set_major_formatter(
TransformFormatter(LinearTransform(in_start=-273.15, in_end=0,
out_end=491.67),
StrMethodFormatter('{x:0.2f}')))

# The y-axes share their locators and formatters, so only one needs to
# be set
ax1.yaxis.set_major_locator(MaxNLocator(integer=True))
# Setting the transfrom to `int` will only alter the type, not the
# actual value of the ticks
ax1.yaxis.set_major_formatter(
TransformFormatter(int, StrMethodFormatter('{x:02X}')))

ax1.set_xlabel('Temperature (\N{DEGREE SIGN}C)')
ax1.set_ylabel('Samples (Hex)')
ax2.set_xlabel('Temperature (\N{DEGREE SIGN}R)')

ax1.xaxis.tick_top()
ax1.xaxis.set_label_position('top')

plt.show()
Loading