-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Enh arbitrary scale #12818
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
Enh arbitrary scale #12818
Conversation
Note this is also a first-step in solving issues like #12665 and #12808 for colorbars with non-standard norms. Though we will have a bit of an issue getting from a norm to a transform. My guess is that the API here will need to expand to allow a Transform with an inverse as the argument as well as the two-tuple. But I'll let others comment on the basic idea first. |
All reactions
Sorry, something went wrong.
lib/matplotlib/scale.py
Outdated
self._inverse = inverse | ||
else: | ||
raise ValueError('arguments to ArbitraryTransform must ' | ||
'be functions.') |
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.
generally, exception messages have no final dot (applies throughout)
Sorry, something went wrong.
All reactions
-
👍 1 reaction
lib/matplotlib/scale.py
Outdated
'be functions.') | ||
|
||
def transform_non_affine(self, values): | ||
with np.errstate(divide='ignore', invalid='ignore'): |
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 setting the errstate should be the job of the function itself? (aka. let's assume that whoever passes arbitrary functions in knows what they're doing, or document that they should)
Sorry, something went wrong.
All reactions
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.
Sounds good to me. That was leftover from LogScale (I think), and I didn't quite grok what it was doing... I'll need to make an example that does the error checking though....
Sorry, something went wrong.
All reactions
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.
well, in logscale it effectively belongs to the user-defined (meaning by us) function
Sorry, something went wrong.
All reactions
-
👍 1 reaction
lib/matplotlib/scale.py
Outdated
|
||
name = 'arbitrary' | ||
|
||
def __init__(self, axis, functions=None): |
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.
Just don't set a default to functions
, as None isn't a valid value anyways?
Wonder whether the locator and formatter classes should be arguments too, i.e. I'd write something like
def __init__(self, axis, functions, *,
major_locator_class=AutoLocator,
minor_locator_class=NullLocator,
major_formatter_class=ScalarFormatter,
minor_formatter_class=NullFormatter):
I think you're also going to run into "interesting" issues in shared-axis handling in the implementation of cla(), which assumes that you can "copy" a scale by doing
self.xaxis._scale = mscale.scale_factory(
self._sharex.xaxis.get_scale(), self.xaxis)
although it's not clear why the code doesn't just do
self.xaxis._scale = self._sharex.xaxis._scale
...
Sorry, something went wrong.
All reactions
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.
Thats a good idea!
Sorry, something went wrong.
All reactions
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.
So the problem w/ passing the class instead of an instance of the class is that the Locators and Formatters need to be passed arguments, so I think it makes more sense to do
def __init__(self, axis, functions, *,
major_locator=AutoLocator(),
minor_locator=NullLocator(),
major_formatter=ScalarFormatter(),
minor_formatter=NullFormatter()):
Sorry, something went wrong.
All reactions
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.
But that won't work because you can't share a locator/formatter across multiple axes right now (as they keep a reference to their axis -- they probably shouldn't, but that's another in depth refactoring...).
Sorry, something went wrong.
All reactions
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.
Hmmm, well, I guess folks will just have to change the locators and formatters manually
Sorry, something went wrong.
All reactions
lib/matplotlib/scale.py
Outdated
TODO | ||
|
||
""" | ||
if functions is None or len(functions) < 2: |
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'd probably just write forward, inverse = functions
and get whatever error message you have when functions is not an iterable of len 2, rather than supplying your own error message. (After all, as of this commit passing a non-interable will also give you the builtin message.)
Sorry, something went wrong.
All reactions
-
👍 1 reaction
looks like you got an extra commit in there |
All reactions
-
😄 1 reaction
Sorry, something went wrong.
bf1154f
to
d403e65
Compare
d403e65
to
1e2d23e
Compare
lib/matplotlib/scale.py
Outdated
Parameters | ||
---------- | ||
|
||
forward: The forward function for the transform |
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.
Should be something like
forward : callable
The forward function for the transform. It must have the signature::
def forward(values: array-like) -> array-like
Are there any additional constraints? E.g. must the function be monotonic? Do we need more precise type descriptions?
Sorry, something went wrong.
All reactions
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.
Changed to
forward: callable
The forward function for the transform. This function must have
an inverse and, for best behavior, be monotonic.
It must have the signature::
def forward(values: array-like) ->
array-likeThe forward function for the transform
inverse: callable
The inverse of the forward function. Signature as ``forward``.
Sorry, something went wrong.
All reactions
lib/matplotlib/scale.py
Outdated
axis: the axis for the scale | ||
|
||
functions: (forward, inverse) | ||
two-tuple of the forward and inverse functions for the scale. |
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.
See ArbitraryTransform
for details on the functions.
Sorry, something went wrong.
All reactions
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 this is more user facing so I changed this to have the more complete info as well. A bit repetitive, but...
Sorry, something went wrong.
All reactions
lib/matplotlib/scale.py
Outdated
@@ -545,6 +623,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): | |||
'log': LogScale, | |||
'symlog': SymmetricalLogScale, | |||
'logit': LogitScale, | |||
'arbitrary': ArbitraryScale, |
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.
It's a bit misleading that LogTransformBase
can have an ArbitraryScale
.
Sorry, something went wrong.
All reactions
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.
Not following this comment...
Sorry, something went wrong.
All reactions
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.
Thanks for the suggestions @timhoffm This was the only one I didn't understand...
Sorry, something went wrong.
All reactions
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.
Sorry, I misread the code. Never mind.
Sorry, something went wrong.
All reactions
1e2d23e
to
8548e47
Compare
Only did a quick read, but 👍 to the functionality. |
All reactions
Sorry, something went wrong.
Minor preference for naming this |
All reactions
-
👍 1 reaction
Sorry, something went wrong.
OK< name changed to This needs to be squashed before merging because I have an extra image in there, but want to keep the old commit in case someone wants me to go back to |
All reactions
Sorry, something went wrong.
is_separable = True | ||
has_inverse = True | ||
|
||
def __init__(self, forward, inverse): |
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.
Is there a reason why FuncTransform takes the two functions as separate args but FuncScale takes the pair as single arg? (Perhaps there is, didn't think more than that about it.)
Sorry, something went wrong.
All reactions
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.
Just because the kwarg gets passed in as a tuple, whereas to me it makes more sense for the transform to take them separately. But I don't feel very strongly about it.
Sorry, something went wrong.
All reactions
axis.set_minor_formatter(NullFormatter()) | ||
# update the minor locator for x and y axis based on rcParams | ||
if rcParams['xtick.minor.visible']: | ||
axis.set_minor_locator(AutoMinorLocator()) |
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.
Can you incoorporate the fix from #12938 here as well?
Sorry, something went wrong.
All reactions
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.
Sorry for the delay - Done!
Sorry, something went wrong.
All reactions
99055d4
to
c9984fe
Compare
examples/scales/scales.py
Outdated
return np.rad2deg( | ||
np.ma.log(np.abs(np.ma.tan(masked) + 1.0 / np.ma.cos(masked)))) | ||
else: | ||
return np.rad2deg(np.log(np.abs(np.tan(a) + 1.0 / np.cos(a)))) |
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.
Can you just rely on this returning nan for invalid values? (possibly wrap in with np.errstate(invalid="ignore")
to silence warnings)
At least the test below returns nan when square-rooting negative values.
Sorry, something went wrong.
All reactions
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.
This is just an example, right? More sophisticated users (than me) can add any error catching they want if they want to implement this...
Sorry, something went wrong.
All reactions
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.
My point is that
# Function Mercator transform
def forward(a):
a = np.deg2rad(a)
return np.rad2deg(np.log(np.abs(np.tan(a) + 1 / np.cos(a))))
is much shorter, and basically works just as well (AFAICT...)
Sorry, something went wrong.
All reactions
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.
Sure, probably. I just stole the code from the custom_scale.py
example to show that it was directly transferable. But I simplified in the latest commit.
Sorry, something went wrong.
All reactions
5ce3637
to
2ca2865
Compare
lib/matplotlib/tests/test_scale.py
Outdated
good = x >= 0 | ||
y = np.full_like(x, np.NaN) | ||
y[good] = x[good]**(1/2) | ||
return y |
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 guess in the same vein something as simple as
def forward(x): return x**(1/2)
also works (apparently it does in the scales.py example above...)? In which case you can probably even just pass them as lambdas below...
Sorry, something went wrong.
All reactions
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.
Sure, they can be lambdas, but I don't find that easier to read than just defining the functions.... But main point taken...
Sorry, something went wrong.
All reactions
My immediate thought on this is what happens if |
All reactions
Sorry, something went wrong.
Garbage in, garbage out? |
All reactions
Sorry, something went wrong.
2ca2865
to
6220a18
Compare
lib/matplotlib/tests/test_scale.py
Outdated
@@ -1,6 +1,8 @@ | |||
from matplotlib.testing.decorators import image_comparison | |||
import matplotlib.pyplot as plt | |||
from matplotlib.scale import Log10Transform, InvertedLog10Transform | |||
import matplotlib.ticker as mticker |
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 import is not actually needed?
Sorry, something went wrong.
All reactions
-
👍 1 reaction
lib/matplotlib/scale.py
Outdated
Both functions must have the signature:: | ||
|
||
def forward(values: array-like) -> | ||
array-likeThe forward function for the transform |
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.
something wrong with the formatting
Sorry, something went wrong.
All reactions
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.
oooops. Should be
Both functions must have the signature::
def forward(values: array-like) returns array-like
Sorry, something went wrong.
All reactions
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
def forward(values: array-like) -> array-like
looks best to me (it's nearly normal type annotation syntax), but not insisting on that.
Sorry, something went wrong.
All reactions
-
👍 1 reaction
19a2475
to
bb16ae4
Compare
lib/matplotlib/scale.py
Outdated
It must have the signature:: | ||
|
||
def forward(values: array-like) -> | ||
array-likeThe forward function for the transform |
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 too
Sorry, something went wrong.
All reactions
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.
Goops, sorry, should have checked the whole thing
Sorry, something went wrong.
All reactions
bb16ae4
to
bc91f3e
Compare
lib/matplotlib/scale.py
Outdated
def forward(values: array-like) -> array-like | ||
|
||
inverse: callable | ||
The inverse of the forward function. Signature as ``forward``. |
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.
This paragraph is indented one character less than the one for forward.
Also the type annotations actually also use a space before the colon (see rst syntax for definition lists, and numpy/numpydoc#78).
Likewise for the docstring below.
Sorry, something went wrong.
All reactions
API: add ability to set formatter and locators from scale init FIX: rename to FuncScale DOC: add whats new FIX: simplify Mercator transform TST: simplify test
bc91f3e
to
9c74f77
Compare
ping on this one - I think I've addressed all review suggestions.... Thanks! |
All reactions
Sorry, something went wrong.
The 3.7 failure is spurious. |
All reactions
Sorry, something went wrong.
Thanks a lot for reviewing and merging! |
All reactions
Sorry, something went wrong.
anntzer
ImportanceOfBeingErnest
timhoffm
Successfully merging this pull request may close these issues.
None yet
PR Summary
As pointed out by @ImportanceOfBeingErnest in the SecondaryAxes PR (#11859 (comment)), there is probably call for an arbitrary axis scale, so, here you go.
PR Checklist