Skip to content

Creating_parse_bar_color_args to unify color handling in plt.bar with precedence and sequence support for facecolor and edgecolor #29133

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

Merged
merged 14 commits into from
Nov 30, 2024

Conversation

Lucx33
Copy link
Contributor

@Lucx33 Lucx33 commented Nov 13, 2024

PR summary

This PR introduces the _parse_bar_color_args function, which provides consistency in color bar arguments by establishing a parameter precedence, "similar" to the one used in scatter:

facecolor: facecolor -> color -> 'b' if in classic mode else the result of get_next_color_func()

edgecolor: edgecolor -> color -> None

This ensures that when edgecolors is not explicitly defined, it tries color. Additionally, this change allows facecolor to accept sequences of colors, ensuring consistency in argument handling, as issued in #29090. The tests are consistent with examples in #29072.

Examples:

  • plt.bar([1, 2, 3], [1, 2, 3], color=['none', 'C2', 'none'], edgecolor=['C1', 'C2', 'C3'])
    Figure_1

  • plt.bar([1, 2, 3], [1, 2, 3], color=['none', 'C2', 'none'])
    Figure_2

  • plt.bar([1, 2, 3], [1, 2, 3]) Figure_3

  • plt.bar([1, 2, 3], [1, 2, 3],facecolor=('none', 'C2', 'none'))
    Figure_4

  • plt.bar([1, 2, 3], [1, 2, 3], color=('C1', 'C2', 'C3'), facecolor=('none', 'C2', 'none'))
    Figure_5

PR checklist

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 13, 2024

Although some tests are failing, I can't determine the cause; For example, test_bbox_inches_tight in test_bbox_tight fails, even though old/new images appear equal.

@rcomer
Copy link
Member

rcomer commented Nov 13, 2024

The easiest test to debug might be test_bar_color_cycle in test_axes.py since that one explicitly shows that we are expecting green but getting blue. So, having already plotted a line, bar is not using the same color in the cycle as plot did.

edgecolor = kwargs.pop('edgecolor', color)

facecolor = (facecolor if facecolor is not None
else "b" if mpl.rcParams['_internal.classic_mode']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you made "b" the default in classic mode for consistency with scatter. However this is not the established behaviour for bar and setting it to blue is likely the cause of at least some of your test failures. For scatter it will only be there for consistency with old Matplotlib versions. I think we should just use the get_next_color_func here.

@@ -2320,7 +2320,59 @@ def _convert_dx(dx, x0, xconv, convert):
# we do by default and convert dx by itself.
dx = convert(dx)
return dx

@staticmethod
def _parse_bar_color_args(kwargs, get_next_color_func):
Copy link
Member

@rcomer rcomer Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to #12739 the method for parsing scatter color arguments was made static so that we could have unit tests specifically for that method. Do you plan to write similar tests here? If not, this could be an ordinary method and we would not need to pass the get_next_color_func but just access self._get_patches_for_fill.get_next_color directly.

Edit: looking at the discussion in #12739 I think it is not advisable to write such tests but just test the public bar function.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 14, 2024

@rcomer thx for the review, I'll fix it as soon as possible. prob later today

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 14, 2024

@rcomer test_bar_color_cycle passed as you said. Others still failed.

@rcomer
Copy link
Member

rcomer commented Nov 15, 2024

Here are the before and after of that test_bbox_inches_tight according to the tests. Note that the image tests run in classic mode unless the style keyword is explicitly set.

Before
bbox_inches_tight-expected

After
bbox_inches_tight

Difference
bbox_inches_tight-failed-diff

So it seems the edgecolor has changed from black to blue.

Note you can download all the test images under "Artifacts" on the Test Summary Page. However those zip files are large so it may be more efficient to run the tests locally and inspect the images you produce that way.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 20, 2024

@timhoffm thx for the review, commited it.

All tests in test_axes passed except for test_bar_hatches but the problem seems to be the ref image:
ax_ref.bar(x[i], y[i], color='C0', hatch=hatches[i])
The generated image appears as expected (Older version). Changing color='C0' to facecolor='C0' resolves the issue:
ax_ref.bar(x[i], y[i], facecolor='C0', hatch=hatches[i])

I can commit this to the PR if you think it's okay.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 20, 2024

@rcomer In test_bbox_inches_tight, since our new precedence says that if color is specified, it defines both facecolor and edgecolor ('b' in this case). Changing color to facecolor fixes the issue:
Old: ax.bar(ind, data[row], width, bottom=yoff, align='edge', color='b')
New: ax.bar(ind, data[row], width, bottom=yoff, align='edge', facecolor='b')

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 20, 2024

Other tests fails for the same reason as both tests above.

@rcomer
Copy link
Member

rcomer commented Nov 20, 2024

I do not think we really want edgecolor to match color by default: if I take the grouped bar example and

  • reverse the order of the bars so the shorter ones are drawn later
  • make facecolor and edgecolor the same

we see that the shorter bars now overlap the larger ones, which is not visually appealing. This does not happen if the edgecolor is left off

image

@timhoffm
Copy link
Member

I do not think we really want edgecolor to match color by default

Indeed. Currently, color is equivalent to facecolor, and it's also documented ("The colors of the bar faces."). We should not change this. Fundamentally, we wouldn't even need color here. facecolor and edgecolor would be enough, but color dates back at least to 2009. Let's just keep it as a synonym for facecolor.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 20, 2024

@rcomer @timhoffm Removed edgecolor being color by default as you both said. I think this PR is 100% ok now.

@rcomer
Copy link
Member

rcomer commented Nov 24, 2024

@Lucx33 please add a test for the new behaviour.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 24, 2024

@rcomer Is this test good enought?

@@ -2378,6 +2428,9 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center",
color : :mpltype:`color` or list of :mpltype:`color`, optional
The colors of the bar faces.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The colors of the bar faces.
The colors of the bar faces. This is an alias for *facecolor*.
If both are given, *facecolor* takes precedence.

@@ -2378,6 +2428,9 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center",
color : :mpltype:`color` or list of :mpltype:`color`, optional
The colors of the bar faces.

facecolor : :mpltype:`color` or list of :mpltype:`color`, optional
The colors of the bar faces.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The colors of the bar faces.
The colors of the bar faces.
If both *color* and *facecolor are given, *facecolor* takes precedence.

for bar in bars:
assert to_rgba(bar.get_facecolor()) == to_rgba('red')

ax.cla()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to clear in between, which unfortunately is a relatively expensive action. Instead I'd do

    # case 1: no color specified
    bars = ax.bar([1, 2, 3], [4, 5, 6])
    [...]

    # case 2: Only 'color'
    bars = ax.bar([11, 12, 13], [4, 5, 6], color='red')
    [...]

    # case 3: Only 'color'
    bars = ax.bar([21, 22, 23], [4, 5, 6], color='red')
    [...]

    # case 4: 'facecolor' and 'color'
    bars = ax.bar([1, 2, 3], [4, 5, 6], color='red', facecolor='green')
    [...]

Putting the no color case first ensures noting is messing with the colorcycle.

Optional: You could use https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.same_color.html#matplotlib.colors.same_color for the asserts.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 26, 2024

@timhoffm thx for the review, commited it.

Lucx33 and others added 2 commits November 26, 2024 14:47
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 27, 2024

Is there anything else I can do to help with the merge?

@timhoffm
Copy link
Member

timhoffm commented Nov 27, 2024

No, we just need a second approval from another core developer.

Note for other reviewers: The win10 test failure is the flaky determinism check and not related to this PR.

Copy link
Member

@rcomer rcomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Lucx33, I just have one small comment. Otherwise this looks good to me.

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 30, 2024

Messed up my commit by changing wrong file manually but fixed already

@Lucx33
Copy link
Contributor Author

Lucx33 commented Nov 30, 2024

@rcomer Changed blue as you said

@rcomer rcomer added this to the v3.10.0 milestone Nov 30, 2024
@rcomer rcomer merged commit 7025fb9 into matplotlib:main Nov 30, 2024
39 checks passed
meeseeksmachine pushed a commit to meeseeksmachine/matplotlib that referenced this pull request Nov 30, 2024
…color handling in plt.bar with precedence and sequence support for facecolor and edgecolor
@rcomer
Copy link
Member

rcomer commented Nov 30, 2024

Thanks @Lucx33 and congratulations on your first Matplotlib PR! I hope we hear from you again.

ksunden added a commit that referenced this pull request Dec 4, 2024
…133-on-v3.10.x

Backport PR #29133 on branch v3.10.x (Creating_parse_bar_color_args to unify color handling in plt.bar with precedence and sequence support for facecolor and edgecolor)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Waiting for author
Development

Successfully merging this pull request may close these issues.

[MNT]: More consistent color parameters for bar()
4 participants