-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
step-between as drawstyle [Alternative approach to #15019] #15065
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
Conversation
Alright, so I took a shot at fixing fill_between as well. So we have
Step
Fill_between
You can even match both at once:
|
So the problem as I advertised when I opened this is lies here https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/lines.py#L676 which shows up in tests and transforms. It looks like a lot of the code is just based on the assumption |
Mhh, I guess one could rewrite that code to use x and y seperately throughout. Or is there anything forbidding this? |
Perhaps a new method that returns a subclass of Line2D? |
"use x and y seperately" i am not sure if all the transforms would still work for uneven sized x,y. In a sense I would say biting the bullet on returning an extra datapoint would be the least invasive way to go. |
@ImportanceOfBeingErnest @tacaswell Can you check this implementation and advise on what's failing the docs? |
@ImportanceOfBeingErnest @anntzer @tacaswell Could you review and tell me if anything is missing? |
2838448
to
1790bd1
Compare
bump |
I would expect the following to give two identical curves
but what I get is this: Is there a reason "edges" is ignored for the second one? |
Whoops, good catch. Forgot to parse kwargs to the edge lines. |
I think it's a bit suboptimal that those edges are individual lines. I'm not sure in how far |
59a3067
to
17edc41
Compare
Based on the previous discussions it seemed the 'edges' functionality was requested by a large pool of people. I didn't make it into it's own drawstyle (trying to keep as close as possible to the
That's what I tried to do originally, but I didn't find a way to do a mixed coordinate transform for a point. Such line would need two points with [data_coord, data_coord,], [data_coord, axes_coord,], which did not seem feasible. The return can be fixed. |
I think I would try to find out how the vertical line in logscales works. And yes, fill_between might need a different approach, but it is very different anyways. |
Fill_between() is using the same drawStyle function and actually it's quite convenient that these two are tied together, as we need this behaviour there as well. The log scale seems to me to work as expected, cutting off at ~0. I don't think it would affect the functionality much to not have the option in order to prevent kwarg bloat, but imo it's a nice helper. For the histogram use case in the current implementation the edges always go to the plot edge. If it was some non-hist shape like described in #6669, the bottom kwarg allows to extend the shape at least outside of logscale. Log scale test
If nothing else, the default edges behaviour corresponds to analogous
The bottom kwarg is different by design as there's no point parsing a *(value) kwarg to |
Now it would return |
By logscale I meant to see how the line in It doesn't look like |
I am not sure what you mean, or rather what kind of modification you're after? The current implementation effectively forwards the info to plot in the same way as |
I am trying to evaluate if it is possible to get rid of the extra lines on the sides by making |
Yeah, that can be done if we ditch the bottom kwarg functionality, because that would need to get parsed quite deep. I suppose we can just not allow edges as a key to be parsed in fillbetween. |
@ImportanceOfBeingErnest Is this what you had in mind? |
Maybe I'm missing something, but I don't think that works because it looks like it replaces all |
@ImportanceOfBeingErnest Ok, I didn't think anybody would be doing that, but at least it allows for the Any idea what's tripping up sphinx? The error message is not very illuminating. |
The underlying implementation is my major concern: I don't think |
Ok, thanks for the clarification. I thought that was the case, but wasn't sure since your last post was mostly dealing with the public api. |
@tacaswell I actually think that trying to cram this into (Hist)step lines are conceptually different from plain lines. While you can appoximate stepwise with a drawstyle, the underlying data model of steps is different (unequal number of step positions and plateau values, orientation, baseline). IMHO a new I agree with @jklymak here:
I did not think this through in detail, but I anticipate that a proper class relation to Line2D is feasible (either subclass or pull out a common base class). |
We already support mixed length inputs to plot ax.plot([0, 1, 2], 5) broadcasts up "as you expect". I think it would be a mistake if we said "ah, these are different lengths there for we will pick a different draw style", but if the user gives us the miss-matched length and asks for the draw style it is not ambiguous. It would be better to special case the handler generation to change the draw style extracted from the We either can infer the "direction" from the length miss-match and need an extra parameter of the "zero-point" or we could have a parameter for the "direction" and, if the dependent data is longer, use the first and last values as the "zero point(s)". On a bit more consideration, the second option is worse in several ways: there is still a hard-coded default (0), having different zero points seems cute but is probably not something that is worth the complexity to get, and the API no longer directly matches the returns of I'm not sure if we were starting from scratch we would put I argue that this is adding "flat" to On a bit more consideration of what could be done in a sub-class, we could have a none of this state in So I think there are a couple of mostly lightly coupled decisions to make. I'm focusing on the Line2D case, but expect it will propagate to the fill_between case. On the public API side:
On the Artist side:
A more drastic step if we want to go with (Yes, No, No, SubClass) is that we should then push I have been seeing a bunch of complaints about Matplotlib being inconsistent recently so maybe that is nagging at the back of my mind, but I think it is better to remain consistent with a slightly dodgy design choice [1] than for parts of the library to have inconsistent design choices [2]. [1] the fallout and true trade offs are always clearer in hindsight :( |
I see your arguments, but still come to the different conclusion that dedicated methods and classes and no interference with step draw styles would be cleaner. Let the slightly odd step draw styles and functions just live on as they are. Once there‘s a clean histogram line-plotting functionality, there‘s one less reason to use them. I don‘t have the time to follow this discussion in detail. But for what it‘s worth, I‘d like to have noted that I support @jklymak ‘s point-of-view. |
@timhoffm Despite your comment going up about an hour before mine, I had not read it yet while writing my (too long) comment, sorry about that. |
Just to be concrete about my proposal. In class HistLine(Line2D):
"""
Subclass of `.Line2D` that implements stepped drawing for histograms.
"""
def __init__(self, bins, vals, *,
orientation='horizontal', baseline=0, **kwargs):
self.baseline = baseline
self.orientation = orientation
self._bins = bins
self._vals = vals
x, y = self._update_data()
print(x, y)
super(HistLine, self).__init__(x, y , **kwargs)
def _update_data(self):
if self._bins.size - 1 != self._vals.size:
raise ValueError('the length of the bins is wrong')
x = np.vstack((self._bins, self._bins)).T.flatten()
y = np.vstack((self._vals, self._vals)).T.flatten()
y = np.hstack((self.baseline, y, self.baseline))
if self.orientation == 'horizontal':
return x, y
else:
return y, x
def set_bins(self, bins):
self._bins = bins
self.set_data(self._update_data())
def set_vals(self, vals):
self._vals = vals
self.set_data(self._update_data())
def set_bins_vals(self, bins, vals):
self._vals = vals
self._bins = bins
self.set_data(self._update_data()) and in def hist_draw(self, bins, vals, *,
orientation='horizontal', baseline=0, **kwargs):
# convert stuff in here...
# ...
line = mlines.HistLine(bins, vals, orientation=orientation, baseline=baseline, **kwargs)
self.add_artist(line)
return line This works, but is obviously not finished, with proper error checking and unit conversion, but just to give an idea of the structure. Obviously if we also did |
I disagree with the "boat has sailed" argument in this case. Doubling down on a bad choice doesn't make it better, it makes it worse. Long-term, we can, and do, use deprecations to move towards better design. |
@jklymak Nice, few questions on the rest of the implementation though.
|
We did discuss this yesterday on the call, for quite a while. @tacaswell was still mulling the approaches. Just to be clear, the above was a working prototype for thought. I didn't look into detail at all the things that would need to be customized. Presumably the duplication between line- and filled-histograms would be factored out in a helper that makes the x/y. I guess some of the I think the balance here is between code and API complexity and some cut and paste of straight-forward helper methods on some sub classes. |
I'm pretty sold on needing new API and sub-classes for this. I think the implementation is still up in the air (using I previously said that I would make executive decisions today, but I think the discussion is still moving along OK so going to not do that until y'all ask me to ;) |
I am going to move this to 3.4 as it still needs some work and discussion. Sorry this has gotten bogged down. |
@tacaswell Been a bit busy lately, when do you close 3.3? |
The original deadline for closing 3.3 was the beginning of May (which we obviously missed), but as soon as possible. |
I've moved this to draft. I actually think it should be closed, not because its not nice work, but because I think the underlying premise of making this a draw style is flawed. Thanks for your eagerness to work on this @andrzejnovak and sorry that it got stalled out. I think the underlying issue does require a solution. |
¯_(ツ)_/¯ if merging a drawstyle based implementation is a no go, then might as well, since the alternative is likely to need entirely different code. Maybe it's my limited FOSS experience, but I still think was a strange call. The PR solved a couple of long-standing issues, the magnitude of at least one of them being illustrated by how popular https://github.com/scikit-hep/mplhep got in the hep community, not doing that much more than passing a I would say that in the absence of a clear spec for a new API the choices made were quite logical at least in that they were fixing current pain points (Someone trying to plot a prebinned step hist is most likely hacking Given all that I would expect this to pass the "question isn't if it's the perfect solution, but if it's a solution you can live with" test. Moreover so, if the API is acceptable, the underlying implementation could be changed at any later point. I would still like to see this functionality, but since I don't see a clear way to do this based on the draft above and the fact that there still isn't clear agreed-upon spec, I am not likely to come back to this any time soon. |
@andrzejnovak Sorry this got discussed to death. I believe it was a fruitful discussion nevertheless because we have realized what we want and need to make histogram-like line plots.
IMHO the API is faulty: This is trying to cram something into
I agree that a completely new approach is necessary. But the way seems pretty clear to me:
|
PR Summary
Alternative approach to #15019 where a new
where='between'
kwarg option is implemented as aLine2D
drawstyle, based on comments from @ImportanceOfBeingErnest. This in the end requires a bit of hackery as well, because it seems matplotlib was always expectslen(x) == len(y)
Notably I am not sure how to properly modify therecache()
fcn.If this is preferable, we can pursue this instead.
PR Checklist