-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
This file was deleted.
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 toStrMethodFormatter
, which does not work. But now you can doand everything works out just fine. More details starting with this comment.
There was a problem hiding this comment.
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 whatTransformFormatter
does.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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