| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Sorry, something went wrong.
|
The creation of a secondary axes with a non-linear transform seems quite complicated. I'm wondering if there is an easier way to achieve this? Ideally one would probably desire something like secax = ax.secondary_xaxis('top', conversion=lambda x: 2*np.pi/x)
Would this be possible, internally inverting any function numerically? |
Sorry, something went wrong.
|
2 np.pi / x is already accounted for without transform (secondary_xaxis('top', conversion='inverted', otherargs=2* np.pi)). As are power laws. We could add “arbitrary” and have otherargs be two vectors like the above example. Then the user would be responsible for the numerics. |
Sorry, something went wrong.
Arbitrary transform API:Using an inverse as an example, though that is already supplied by secondary_xaxis('top', conversion='inverted', otherargs=2* np.pi): functionsdef forward_func(x):
return 2 * np.pi / x
def inverse_func(x):
return x / (2 * np.pi)
secondary_xaxis('top', conversion=forward_func, otherargs=inverse_func)Transformsclass LocalInvert(Transform):
input_dims = 1
output_dims = 1
is_separable = True
has_inverse = True
def __init__(self, fac):
Transform.__init__(self)
self._fac = fac
def transform_non_affine(self, values):
return self.fac / values
def inverted(self):
""" we are just our own inverse """
return LocalInvert(1.0 / self.fac)
secondary_xaxis('top', conversion=LocalInvert(2 * np.pi))Arbitraryxin = np.arange(300)
xout = 2 * np.pi / xin
secondary_xaxis('top', conversion='arbitrary', otherargs=(xin, xout))(Note this has pretty bad numerical issues under zooming (will collapse to linear for delta xin < 1, and will be linear extrapolation for xin < 0 or xin > 300)... |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest and others, which of the three is the better arbitrary interface? I guess I like the functions approach the best (not implemented yet). Its basically the same as the Transform, but with a bit less overhead. Internally, it would just be a transform. Of course we should still support a Transform... |
Sorry, something went wrong.
|
I would vote for the functions. If I were to write this for my own purposes I'd do ax.secondary_xaxis('top', conv=lambda x: .... , inv=lambda k: ...)
but if you think there is a reason to name the arguments differently, that's fine (otherargs sounds very general though). |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest I just didn't want there to be a plethora of kwargs, but maybe otherargs is too mysterious. I do need it for 'linear' and ='inverted' though... |
Sorry, something went wrong.
|
@ImportanceOfBeingErnest, @efiring
I could easily put the string options back, but I think it makes the docs a lot cleaner, and is more powerful for the user just to supply the function. It works very easily with lambda functions: sec = ax.secondary_xaxis('top', conversion=(lambda x: 3 / x, lambda x: 1 / (x * 3))) |
Sorry, something went wrong.
|
|
||
|
|
||
| def inverse(x): | ||
| return 1 / x |
There was a problem hiding this comment.
It might be a bit confusing that the example uses the one and only case where forward and inverse are actually the same, although it probably makes sense to you a/x to show how a log scale behaves. Would something like 2./np.sqrt(x) also work here?
Sorry, something went wrong.
| plt.show() | ||
|
|
||
| ########################################################################### | ||
| # The conversion can be a linear slope and an offset as a 2-tuple. It can |
There was a problem hiding this comment.
Would it make sense to explicitely show the use of this (slope,offset)? I think this is somehow the most common case, or at least the one where there is already a Celsius->Fahrenheit example in the gallery (...which could then be replaced?).
Sorry, something went wrong.
There was a problem hiding this comment.
The conversion=(forward,inverse) is really useful. 👍
The examples could actually be made much more vivid and explicit, using real world applications like Celsius-Fahrenheit, Pascal-Torr, wavelength-wavevector. But that could also be done independently of this PR.
Sorry, something went wrong.
|
I would prefer simplifying the API further: accept only a pair of functions, and allow only one of the obvious ways of specifying them: as conversions=(forward,inverse) or using 2 required keyword-only arguments. This simplifies everything: the mpl code base, the example, and the documentation, and as far as I can see, it imposes no significant extra work on the user under any circumstances. The resulting user code is more readable. There is no chance of getting the order of slope and intercept mixed up. Everyone is happier, right? Am I missing something? Is there any use case this doesn't handle cleanly? (I haven't spent time on this yet, so certainly I might be missing all sorts of things.) |
Sorry, something went wrong.
|
Between the two options--single kwarg vs two kwargs--I am inclined to prefer the latter as the most readable. |
Sorry, something went wrong.
| self.apply_aspect() | ||
|
|
||
| # loop over self and child axes... | ||
| for ax in [self]: |
There was a problem hiding this comment.
This doesn't look like it is looping over anything other than self.
Sorry, something went wrong.
| else: | ||
| warnings.warn("location must be '{}', '{}', or a " | ||
| "float, not '{}'.".format(location, | ||
| self._locstrings[0], self._locstrings[1])) |
There was a problem hiding this comment.
It looks like you have the format arguments out of order; location should be last, not first, shouldn't it?
Sorry, something went wrong.
|
Two senior devs and @ImportanceOfBeingErnest for two-keyword, function-only interface ax.secondary_xaxis('top', conversion=lamda x: 2/x, inverse=lambda x: 1 / (2 * x)) @timhoffm , @tacaswell? Anyone else object to this? |
Sorry, something went wrong.
|
As I understand it, @efiring prefers the two-argument notation conversion=lambda x: 2*x, inverse=lambda x: x/2. while I personnally prefer the single argument tuple conversion=(lambda x: 2*x, lambda x: x/2.). I would not abbandon at least the single factor solution for the following reason: I think part of the motivation for the secondary axes was to get a better way of creating an identical scale on the other side of the plot without abusing twinx/twiny in order to simply format this scale differently or use a different locator. In such case specifying the secondary_*axis as e.g. ax2 = ax.secondary_yaxis('right', conversion=lamda x: x, inverse=lambda x: x) instead of ax2 = ax.secondary_yaxis('right') feels quite cumbersome. |
Sorry, something went wrong.
|
The no-conversion case can be handled concisely by making it the default, conversion=None, inverse=None. In documentation and examples, I suggest avoiding the mystical-looking lambda, and instead define named functions as needed: def reciprocal(x):
return 1/x
ax2 = ax.secondary_yaxis('right', conversion=reciprocal, inverse=reciprocal)I'm not adamantly opposed to the tuple alternative, conversion=(reciprocal, reciprocal), with a default of conversion=None, but I think the two-kw form is a bit more readable and easier to document. I am adamant that we need to choose one or the other. This feature--the axis with a different but functionally-related scale--is a wonderful addition to mpl, and one that even the newest mpl user and the least programming-oriented user should be comfortable with. It is the kind of API for which we should put heavy weight on simplicity, explicitness, and readability. |
Sorry, something went wrong.
|
I think my point was that a signature which looks like function(location, conversion=None, inverse=None) does not make it clear that you need to specify conversion and inverse simultaneously, while with function(location, conversion, inverse) that would be obvious. However if deciding for that latter case, function(location), meaning "no conversion" would be impossible. A signature like function(location, conversion=None) would overcome this, allowing for "no conversion" as None or a custom conversion in form of a tuple. If we expect this feature to be widely used, I'm pretty sure that sooner or later people would demand for conversion also taking a constant, or a slope + offset, essentially what the current implementation already allows for. So it may be beneficial to implement this now and do it the way it can be agreed upon than having a user trying to implement it later and going through all the details of implementation in their PR when it comes in. |
Sorry, something went wrong.
|
Oh, I think I forgot to say that I would certainly agree that one would not want to have a mixture like conversion=(forward,backward), inverse=None - that would sure be confusing and hard to document. |
Sorry, something went wrong.
|
The problem with giving in to demand (if it were to arise) for multiple input forms is that it makes everything more complicated, for what I think is negligible gain in convenience. I'm really worried about code bloat in mpl, and excessive complexity in API and in documentation. |
Sorry, something went wrong.
|
To me it looks like not allowing for the constant factor or slope + offset would save you some 20 lines of the 580 lines long code (from line 248 to 271), which is 4%. |
Sorry, something went wrong.
|
|
||
| fig, ax = plt.subplots() | ||
| ax.loglog(range(1, 360, 5), range(1, 360, 5)) | ||
| ax.set_xlabel('wavenumber [cpkm]') |
There was a problem hiding this comment.
cpkm looks a bit domain-specific? perhaps "/km" (or "period (s)"/"frequency (Hz)")
Sorry, something went wrong.
There was a problem hiding this comment.
Sure...
Sorry, something went wrong.
There was a problem hiding this comment.
Looks good; I hadn't realized how much work would be required for this functionality. My comments are quite minor.
Sorry, something went wrong.
| axes with only one axis visible via `.Axes.axes.secondary_xaxis` and | ||
| `.Axes.axes.secondary_yaxis`. | ||
|
|
||
| If we want to label the top of the axes: |
There was a problem hiding this comment.
I suggest eliminating this first part of the example, since it really is not what the new functionality is for, and as illustrated here it duplicates existing functionality. Instead, go straight to the motivating use case, so that will be what people will see first, without distraction.
Sorry, something went wrong.
There was a problem hiding this comment.
OK
Sorry, something went wrong.
| plt.show() | ||
|
|
||
| ########################################################################### | ||
| # However, its often useful to label the secondary axis with something |
There was a problem hiding this comment.
Having eliminated the example above, this comment section can be reduced to something like its last sentence.
Sorry, something went wrong.
|
|
||
|
|
||
| def rad2deg(x): | ||
| return x * 180 / np.pi |
There was a problem hiding this comment.
Also in the interest of compactness and getting to the point, you could eliminate the function definitions and use np.deg2rad and np.rad2deg.
Sorry, something went wrong.
There was a problem hiding this comment.
Well, I wanted to have these as templates to the user. Otherwise, I find it a bit mysterious.
Sorry, something went wrong.
|
|
||
| xold = np.arange(0, 11, 0.2) | ||
| # fake data set relating x co-ordinate to another data-derived co-ordinate. | ||
| xnew = np.sort(10 * np.exp(-xold / 4) + np.random.randn(len(xold)) / 3) |
There was a problem hiding this comment.
Perhaps it is obvious, but you might add, "Note: we must ensure that both coordinates are monotonic."
Sorry, something went wrong.
| """ | ||
| Add a second x-axis to this axes. | ||
|
|
||
| For example if we want to have a second scale for the data plotted on |
There was a problem hiding this comment.
You can eliminate this sentence and go straight to the example.
Sorry, something went wrong.
There was a problem hiding this comment.
Did you intend to do this, but forget?
Sorry, something went wrong.
There was a problem hiding this comment.
Ooops, I somehow screwed up a transfer between machines.
OTOH, not sure about this one. Note there is a long docstring interp between here and the example, so maybe its good to have the extra line?
Sorry, something went wrong.
There was a problem hiding this comment.
I agree that the entra sentence doesn't add much.
Sorry, something went wrong.
| Examples | ||
| -------- | ||
|
|
||
| Add a secondary axes that shows both wavelength for the main |
There was a problem hiding this comment.
Delete "both".
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed to read:
The main axis shows frequency, and the secondary axis shows period:
Sorry, something went wrong.
There was a problem hiding this comment.
But I don't see this change on github; am I using github incorrectly, or did you not push the change?
Sorry, something went wrong.
There was a problem hiding this comment.
Sorry about that. Fixed now...
Sorry, something went wrong.
| ax.loglog(range(1, 360, 5), range(1, 360, 5)) | ||
| ax.set_xlabel('frequency [Hz]') | ||
|
|
||
|
|
There was a problem hiding this comment.
Out of curiosity: does the matplotlib plot directive require 2 blank lines before functions? It looks like you are using this spacing consistently.
Sorry, something went wrong.
There was a problem hiding this comment.
flake8 requires it for the actual examples, but not here I don;'t think... Fixed
Sorry, something went wrong.
|
|
||
| functions : (callable, callable) | ||
| two-tuple of the forward and inverse functions for the scale. | ||
| The forward function must have an inverse and, for best behavior, |
There was a problem hiding this comment.
Maybe just say it must be monotonic; actually, if it is not monotonic, it doesn't have an inverse function.
Sorry, something went wrong.
| def set_zscale(self, value, **kwargs): | ||
| """ | ||
| Set the scaling of the z-axis: %(scale)s | ||
| Set the x-axis scale. |
There was a problem hiding this comment.
Don't you mean the z-axis? And aren't the changes in this file completely unrelated to this PR?
Sorry, something went wrong.
There was a problem hiding this comment.
Goops, sorry, you are right.
This was necessary because of the doctoring interpolation - It led to two Parameters sections in this docstring, and doc build failure.
Sorry, something went wrong.
| """ | ||
| See `.secondary_xaxis` and `.secondary_yaxis` for the doc string. | ||
| While there is no need for this to be private, it should really be | ||
| called by those higher level functions. |
There was a problem hiding this comment.
Then maybe it should at least start out as private. Is there any advantage to making it public at this point?
Sorry, something went wrong.
There was a problem hiding this comment.
Well, there are some methods the user may want to manipulate?
Sorry, something went wrong.
There was a problem hiding this comment.
Plus this is the returned object, so users may want to see the documentation for it.
Sorry, something went wrong.
Sorry, something went wrong.
|
I see a total of only 2 commits, the original, and one from a couple days ago which has only some of the indicated changes in it. Either something is going wrong with the way I am interpreting or using this web page, or you didn't push something you thought you pushed. |
Sorry, something went wrong.
|
|
||
|
|
||
| _secax_docstring = ''' | ||
| Warnings |
There was a problem hiding this comment.
warning could use a rst directive, as in https://numpydoc.readthedocs.io/en/latest/format.html#other-points-to-keep-in-mind.
Sorry, something went wrong.
There was a problem hiding this comment.
Huh, the docstrings aren't rendering the interpolation at all...
Sorry, something went wrong.
There was a problem hiding this comment.
So not sure why this should be an rst directive versus the numpy doc heading. Is there a difference?
Sorry, something went wrong.
There was a problem hiding this comment.
Looking at numpydoc's implementation they just internally translate warnings sections to .. warnings:, but also put the sections in their own (consistent) order (as seen in the rendered docs); I mildly prefer the latter as you can have them wherever you want or nested under other constructs (e.g. a bullet list) but it doesn't matter too much.
Sorry, something went wrong.
There was a problem hiding this comment.
ummm, so which do you prefer? I dont' really care - easier to remember the numpydoc way than the rst way
Sorry, something went wrong.
There was a problem hiding this comment.
Strictly personally I prefer the rst directive, but again anything works here (though you will need to use the directive if you want the warning to show up above the parameters in the rendered html).
Sorry, something went wrong.
There was a problem hiding this comment.
OK, I'll leave as is - I can see some argument for the warning at the top, but don't think its a big deal.
Sorry, something went wrong.
|
|
||
| If a 2-tuple of functions, the user specifies the transform | ||
| function and its inverse. i.e. | ||
| `functions=(lamda x: 2 / x, lambda x: 2 / x)` would be an |
There was a problem hiding this comment.
lamda -> lambda.
Sorry, something went wrong.
| two-tuple of the forward and inverse functions for the scale. | ||
| The forward function must have an inverse and, for best behavior, | ||
| be monotonic. | ||
| The forward function must be monotonic |
There was a problem hiding this comment.
dot
Sorry, something went wrong.
| class FuncScaleLog(LogScale): | ||
| """ | ||
| Provide an arbitrary scale with user-supplied function for the axis and | ||
| then put on a logarithmic axes |
There was a problem hiding this comment.
dot
Sorry, something went wrong.
|
... merge? |
Sorry, something went wrong.
|
I just ran the first example from this PR and the result looks like this on screen: If resizing the figure window with the mouse this can become as bad as The saved figure is fine though. Let me know if you need further details or want me to test anything particular with my configuration. |
Sorry, something went wrong.
|
Well, huh - never tried resizing the window! |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
PR Summary
(This is #11589, but I closed that PR and force-pushed so I can't re-open; ping @ImportanceOfBeingErnest who had commented and @efiring who promised to take a look)
New methods ax.secondary_xaxis and ax.secondary_yaxis; here the work is all in _secondary_axes.py, and a new method in _axes.py.
Update 7 Sep:
Now called as
Please see the example and tests...
PR Checklist