-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Add functionality to label individual bars with Axes.bar() #23525
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
Thanks for the PR. First there is already a colors kwarg for bars, so how does this interact with that? Secondly there is tick_label kwarg that seems to be what this pr is suggesting? Can you clarify how this is different? https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.barh.html |
This PR populates the >>> bar_container = ax.barh(x, y, label=x)
>>> print([bar.get_label() for bar in bar_container])
['_nolegend_', '_nolegend_', '_nolegend_'] When x = ["a", "b", "c"]
y = [10, 20, 15]
fig, ax = plt.subplots()
_ = ax.barh(x, y, labels=["Apple", "Banana", "Cherry"])
ax.legend() When x = ["a", "b", "c"]
y = [10, 20, 15]
fig, ax = plt.subplots()
_ = ax.barh(x, y) If you pass in both: fig, ax = plt.subplots()
bar_container = ax.barh(x, y, labels=["Apple", "Banana", "Cherry"], color=['blue', 'red', 'orange'])
ax.legend() |
Thanks I see. Do people want a legend if the bars are already labeled via the ticks? |
My main use case was actually building animations. I use the |
I'm of two minds on this. On one hand I see how much nicer this is that having to do the loop outside and I can totally see a use case for setting the legend and dropping the ticks / axis all together. I also see the analogy to stack plot (even if it is a bit rough because stackplots takes a sequence of sequences of scalars and bar only takes a sequence of scalars and a better analogy to stack plot would be extending bar to make stacked bar charts). On the other hand I am worried about stacking yet more complexity into the public APIs! I think in addition Even if we do not take this, this is nice work. Thank you for a fully documented and tested PR out of the gate @stefmolin ! ignore the linting error #23527 will fix it. |
Initially, I was trying to match the API of My change to the bar colors was to make the legend in my examples make sense. So if we are more comfortable with just using the |
If we promote import matplotlib.pyplot as plt
x = [1, 2, 3]
y1 = [1, 5, 7]
y2 = [3, 1, 6]
fig, ax = plt.subplots()
ax.bar(x, y1, label='G1', tick_label=['a', 'b', 'c'])
ax.bar(x, y2, bottom=y1, label='G2', tick_label=['a', 'b', 'c'])
ax.legend() Maybe only do it if the overall bar does not have a label? Maybe make it opt-in like |
Good point. Another option would be to prefix the individual bar labels with
so essentially naming everything in the case of stacked bars as That way they don't show up in the legend, and at the same time, they have unique labels. |
I feel that making bars indiviudally configurable was an overreach of the API of The minimal (and possibly reasonable) extension is I'm very sceptical on mixing with |
We guarantee that labels starting with an underscore are not drawn in the legend:
@stefmolin If your only conern is giving unique IDs to bars, you can define any label you want starting with an underscore for this. With the list-of-labels API suggested above, you can easily do that - and decide yourself how your IDs look like. |
@timhoffm - That logic was if we were going to use the |
Here are some examples of the new implementation. Note that colors are no longer altered.
>>> import matplotlib.pyplot as plt
>>>
>>> fig, ax = plt.subplots()
>>> bar_container = ax.barh(
... ["a", "b", "c"],
... [10, 20, 15],
... label=["Apple", "Banana", "Cherry"]
... )
>>> [bar.get_label() for bar in bar_container]
['Apple', 'Banana', 'Cherry']
>>> import matplotlib.pyplot as plt
>>>
>>> fig, ax = plt.subplots()
>>> bar_container = ax.barh(
... "a",
... 10,
... label="Apple"
... )
>>> [bar.get_label() for bar in bar_container]
['Apple']
>>> import matplotlib.pyplot as plt
>>>
>>> fig, ax = plt.subplots()
>>> bar_container = ax.barh(
... ["a", "b", "c"],
... [10, 20, 15]
... )
>>> [bar.get_label() for bar in bar_container]
['_nolegend_', '_nolegend_', '_nolegend_']
>>> import matplotlib.pyplot as plt
>>> import itertools
>>>
>>> x = [1, 2, 3]
>>> y1 = [1, 5, 7]
>>> y2 = [3, 1, 6]
>>>
>>> fig, ax = plt.subplots()
>>> bar_container1 = ax.bar(
... x, y1, label='G1', tick_label=['a', 'b', 'c']
... )
>>> bar_container2 = ax.bar(
... x, y2, bottom=y1, label='G2', tick_label=['a', 'b', 'c']
... )
>>> [
... bar.get_label()
... for bar in itertools.chain(bar_container1, bar_container2)
... ]
['_nolegend_',
'_nolegend_',
'_nolegend_',
'_nolegend_',
'_nolegend_',
'_nolegend_'] |
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.
Since the label
behavior grew more complex now, it deseves explicit mention
in the Other Parameters section of the docstring (preferably right below tick_label
.
I suggest something like:
label : str or list of str, optional
A single label is attached to the resulting BarContainer as a
label for the whole dataset.
If a list is given, it must be the same length as *x* and
labels the individual bars. For example this may used with
lists of *color*.
The fourth element ( I advise against trying to automatically filter duplicates. That's tedious due to normalization. It's also a bit magical, as the entries legend entries are associated with the bars, if you filter duplicates out, technically some bars don't have a label, e.g. 'a' would be associtated with 'A' but 'c' wouldn't - it just looks the same. You could even break that by re-styling 'a' now. Then the legend would follow, but 'c' would not. |
I won't block if labels are repeated in the legend, but I think then this choice has to be clearly documented as I expect it to be a follow up feature request.
I'd be ok w/ this being the example of how to use this keyword to do grouping, but I think it'd be worth either expanding one of the gallery examples or adding a new one discussing this. Also, it seems like at least one image test wouldn't hurt. |
I'm fine with documenting that the behavior for repeated labels is not defined and may change in the future.
For now, the expected behavior is exactly defined by testing the labels of the individual bars and the label of the BarContainer: "Where does the information go?". Every Artistvwith a label shows up in the legend. There's no additional magic here that needs testing as an image. |
I addressed the comments and fixed that bug: import matplotlib.pyplot as plt
fig, ax = plt.subplots()
x = ['a', 'b', 'c']
y = [2, 1, 3]
l = ['A', 'B', 'A']
c = ['tab:orange', 'tab:blue', 'tab:orange']
ax.bar(x, y, label=l, color=c)
ax.legend()
Can you provide some additional information on this? |
Yeah I didn't quite grok how to test the double labeling issue, but I like @stefmolin adding it to the code tests better than an image test.
I think this new keyword argument could be more discoverable with an addition to the gallery in the lines-bars-and-markers section showing 1) the use of this keyword 2) the use of this keyword with a mix of labels and no legend. The latter could also show off the list of colors, which is another keyword we don't have an explicit example for. Granted I can also spin thus request off into a follow up issue so this is another non-blocking request. |
@stefmolin could you rebase this to squash out the adding / removed API change note? |
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.
Minus the remaining comments.
Co-authored-by: Elliott Sales de Andrade <quantum.analyst@gmail.com>
@tacaswell - I rebased to remove those changes. I also incorporated the change to the docstring as suggested. Linting is failing from the latest changes on master after the rebase.
@story645 - Should I move forward with this in a separate PR? |
Yes, that would be awesome! |
I squash-merged, as I don't think we need the history of no-longer-implemented functionality. |
PR Summary
Currently, if you need to label each bar in a plot say for an animation, you have to loop over the bars in the bar container that
Axes.bar()
returns and callset_label()
on each bar. I have an example here in a workshop I deliver. When compared withstackplot()
(which has alabels
argument for this) this can be a gotcha for newcomers. There is alabel
key shown in the docs as available on theRectangle
, but it doesn't have the expected effect of labeling the bars, rather it labels theBarContainer
:This PR adds a
labels
argument toAxes.bar()
, which makes it possible to easily label each bar and color them differently, making it possible to create a legend immediately after calling thebar()
/barh()
method.Default color behavior is preserved when
labels
isn't passed in:PR Checklist
Tests and Styling
pytest
passes).flake8-docstrings
and runflake8 --docstring-convention=all
).Documentation
doc/users/next_whats_new/
(follow instructions in README.rst there).