Repository: matplotlib/matplotlib

Issue:
[Bug]: Legend kwarg `labelcolor='linecolor'` not working properly when `facecolor` is `'None'` 
### Bug summary

The legend kwarg `labelcolor='linecolor'` does not play nicely with `histtype='step'` histograms or with `fc='None'` scatter plots, resulting in an invisible legend label for the histogram, and in an IndexError for the scatter plot.

### Code for reproduction

```Python
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (5, 5)
plt.rcParams['legend.loc'] = 1
plt.rcParams['legend.framealpha'] = 1

x, y = np.random.randn(2, 1000)

fig, axes = plt.subplots(2, 2, sharex=True, sharey='row')
axes[0, 0].hist(x, histtype='bar', label="spam")
axes[0, 1].hist(x, histtype='step', label="spam")
axes[1, 0].scatter(x, y, label="spam")
axes[1, 1].scatter(x, y, ec='C0', fc='None', label="spam")
for ax in axes.ravel():
    ax.legend(labelcolor='linecolor')
```

### Actual outcome

<img width="443" height="428" alt="Image" src="https://github.com/user-attachments/assets/6f528aac-fa42-456d-ad4a-4aeb3063ac62" />

Note:
* correctly coloured legend labels in both left panels 
* invisible legend label in the top right panel
* no legend for the bottom right panel, instead the following error message:

```
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[1], line 15
     13 axes[1, 1].scatter(x, y, ec='C0', fc='None', label="spam")
     14 for ax in axes.ravel():
---> 15     ax.legend(labelcolor='linecolor')

File [~/.virtualenv/py3121env/lib/python3.12/site-packages/matplotlib/axes/_axes.py:337](http://localhost:8888/home/lukas/.virtualenv/py3121env/lib/python3.12/site-packages/matplotlib/axes/_axes.py#line=336), in Axes.legend(self, *args, **kwargs)
    220 """
    221 Place a legend on the Axes.
    222 
   (...)    334 .. plot:: gallery[/text_labels_and_annotations/legend.py](http://localhost:8888/text_labels_and_annotations/legend.py)
    335 """
    336 handles, labels, kwargs = mlegend._parse_legend_args([self], *args, **kwargs)
--> 337 self.legend_ = mlegend.Legend(self, handles, labels, **kwargs)
    338 self.legend_._remove_method = self._remove_legend
    339 return self.legend_

File [~/.virtualenv/py3121env/lib/python3.12/site-packages/matplotlib/legend.py:601](http://localhost:8888/home/lukas/.virtualenv/py3121env/lib/python3.12/site-packages/matplotlib/legend.py#line=600), in Legend.__init__(self, parent, handles, labels, loc, numpoints, markerscale, markerfirst, reverse, scatterpoints, scatteryoffsets, prop, fontsize, labelcolor, borderpad, labelspacing, handlelength, handleheight, handletextpad, borderaxespad, columnspacing, ncols, mode, fancybox, shadow, title, title_fontsize, framealpha, edgecolor, facecolor, bbox_to_anchor, bbox_transform, frameon, handler_map, title_fontproperties, alignment, ncol, draggable)
    597 color = getattr(handle, getter_name)()
    598 if isinstance(color, np.ndarray):
    599     if (
    600             color.shape[0] == 1
--> 601             or np.isclose(color, color[0]).all()
    602     ):
    603         text.set_color(color[0])
    604     else:

IndexError: index 0 is out of bounds for axis 0 with size 0
```

### Expected outcome

The example plot should look like this:

<img width="443" height="428" alt="Image" src="https://github.com/user-attachments/assets/ef1fd83a-51b2-4cf1-92f9-dfc6c1f1cf41" />

(I managed to create this figure by changing `labelcolor='linecolor'` to `labelcolor='C0'` in the provided example.)

In words:
1) The legend label for the `histtype='step'` histogram (top right) should be the same as the one for the `histtype='bar'` histogram (top left).
2) The scatter plot with empty markers (bottom right) should provide the same legend as the one with filled markers (bottom left), except that the legend handles should be obviously empty for the first and filled for the latter.
3) There should be no error.

### Additional information

Might be related to #3670, #28153. 

### Operating system

Manjaro Linux

### Matplotlib Version

3.10.3

### Matplotlib Backend

module://matplotlib_inline.backend_inline

### Python version

3.12.1

### Jupyter version

4.4.2

### Installation

pip


Discussion:
Thanks for the clear report @lukashergt.  I note that the two righthand panels work correctly if I set `labelcolor='markeredgecolor'`, which is intuitive for the scatter plot but a bit mysterious for the histogram.

If we pass 'linecolor' then the code checks the handle for a `get_color` method and then a `get_facecolor` method.
https://github.com/matplotlib/matplotlib/blob/30f4ece3f55244c90bb8fe9a77aef8a615a920a9/lib/matplotlib/legend.py#L579

It seems to me it should check `get_edgecolor` rather than `get_facecolor` (or as well as?).
> I note that the two righthand panels work correctly if I set `labelcolor='markeredgecolor'`, which is intuitive for the scatter plot but a bit mysterious for the histogram.

Good point, and that will help for plots where everything has an edgecolor, but `labelcolor='markeredgecolor'` will still fail in mixed plots, e.g. for:
```
axes[1, 0].scatter(x, y, fc='C0', ec='None', label="spam")
axes[1, 1].scatter(x, y, ec='C0', fc='None', label="spam")
```
In that scenario I would want `'linecolor'` to intelligently determine that it needs to use the `fc` of the first and `ec` of the second, right?

### Scatter plot

> It seems to me it should check `get_edgecolor` rather than `get_facecolor` (or as well as?).

I think if `get_facecolor` returns `[]` as it does here and which then causes the IndexError, then it should rather loop on to also check `get_edgecolor`. So indeed, does anything speak against changing the `color_getter` for `'linecolor'` to the following?
```python
            'linecolor':       ['get_color', 'get_facecolor', 'get_edgecolor'],
```

Then in [lines 598 to 608](https://github.com/matplotlib/matplotlib/blob/30f4ece3f55244c90bb8fe9a77aef8a615a920a9/lib/matplotlib/legend.py#L598-L608), if we `continue` for an empty `color`, then that resolves the scatter plot issue:
```python
                        if isinstance(color, np.ndarray):
                            if color.size == 0:
                                continue
                            elif (
                                    color.shape[0] == 1
                                    or np.isclose(color, color[0]).all()
                            ):
                                text.set_color(color[0])
                            else:
                                pass
                        else:
                            text.set_color(color)
                        break
```

### Histograms

For histograms with `histtype='step'` things are a bit more complicated, because these actually do have a facecolor, only that facecolor has an alpha value of zero rendering the label invisible... I don't think it would be right to generally loop on for `alpha=0`. There might be cases where you'd want an `alpha=0` text label, and if we generally loop on, then if it doesn't have an edgecolor either then it will default to textcolor, i.e. black. So if facecolor has `alpha=0`, then you'd want to loop on only if edgecolor actually exists, right?

Sidenote: These might be bigger issues of their own. 
1) Why does `'step'` have a facecolor at all? It should only have an edgecolor.
2) For that matter, why do we have both `'step'` and `'stepfilled'`, why not just one and then handle the rest via `fc` and `ec` as elsewhere in matplotlib...?



Patch:
diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py
index 2fb14e52c58c..ff1f64664a42 100644
--- a/lib/matplotlib/legend.py
+++ b/lib/matplotlib/legend.py
@@ -576,7 +576,11 @@ def __init__(
         # set the text color
 
         color_getters = {  # getter function depends on line or patch
-            'linecolor':       ['get_color',           'get_facecolor'],
+            'linecolor':       ['get_markerfacecolor',
+                                'get_facecolor',
+                                'get_markeredgecolor',
+                                'get_edgecolor',
+                                'get_color'],
             'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'],
             'mfc':             ['get_markerfacecolor', 'get_facecolor'],
             'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'],
@@ -595,19 +599,22 @@ def __init__(
                 for getter_name in getter_names:
                     try:
                         color = getattr(handle, getter_name)()
-                        if isinstance(color, np.ndarray):
-                            if (
-                                    color.shape[0] == 1
-                                    or np.isclose(color, color[0]).all()
-                            ):
-                                text.set_color(color[0])
-                            else:
-                                pass
-                        else:
-                            text.set_color(color)
-                        break
                     except AttributeError:
-                        pass
+                        continue
+                    if isinstance(color, np.ndarray):
+                        if color.size == 0:
+                            continue
+                        elif (color.shape[0] == 1 or np.isclose(color, color[0]).all()):
+                            text.set_color(color[0])
+                        else:
+                            pass
+                    elif cbook._str_lower_equal(color, 'none'):
+                        continue
+                    elif mpl.colors.to_rgba(color)[3] == 0:
+                        continue
+                    else:
+                        text.set_color(color)
+                    break
         elif cbook._str_equal(labelcolor, 'none'):
             for text in self.texts:
                 text.set_color(labelcolor)


Test patch:
diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py
index 9b100037cc41..61fae63a298e 100644
--- a/lib/matplotlib/tests/test_legend.py
+++ b/lib/matplotlib/tests/test_legend.py
@@ -1068,6 +1068,201 @@ def test_legend_labelcolor_rcparam_markerfacecolor_short():
         assert mpl.colors.same_color(text.get_color(), color)
 
 
+def assert_last_legend_patch_color(histogram, leg, expected_color,
+                                   facecolor=False, edgecolor=False):
+    """
+    Check that histogram color, legend handle color, and legend label color all
+    match the expected input. Provide facecolor and edgecolor flags to clarify
+    which feature to match.
+    """
+    label_color = leg.texts[-1].get_color()
+    patch = leg.get_patches()[-1]
+    histogram = histogram[-1][0]
+    assert mpl.colors.same_color(label_color, expected_color)
+    if facecolor:
+        assert mpl.colors.same_color(label_color, patch.get_facecolor())
+        assert mpl.colors.same_color(label_color, histogram.get_facecolor())
+    if edgecolor:
+        assert mpl.colors.same_color(label_color, patch.get_edgecolor())
+        assert mpl.colors.same_color(label_color, histogram.get_edgecolor())
+
+
+def test_legend_labelcolor_linecolor_histograms():
+    x = np.arange(10)
+
+    # testing c kwarg for bar, step, and stepfilled histograms
+    fig, ax = plt.subplots()
+    h = ax.hist(x, histtype='bar', color='r', label="red bar hist with a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+    h = ax.hist(x, histtype='step', color='g', label="green step hist, green label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'g', edgecolor=True)
+
+    h = ax.hist(x, histtype='stepfilled', color='b',
+                label="blue stepfilled hist with a blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'b', facecolor=True)
+
+    # testing c, fc, and ec combinations for bar histograms
+    h = ax.hist(x, histtype='bar', color='r', ec='b',
+                label="red bar hist with blue edges and a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+    h = ax.hist(x, histtype='bar', fc='r', ec='b',
+                label="red bar hist with blue edges and a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+    h = ax.hist(x, histtype='bar', fc='none', ec='b',
+                label="unfilled blue bar hist with a blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'b', edgecolor=True)
+
+    # testing c, and ec combinations for step histograms
+    h = ax.hist(x, histtype='step', color='r', ec='b',
+                label="blue step hist with a blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'b', edgecolor=True)
+
+    h = ax.hist(x, histtype='step', ec='b',
+                label="blue step hist with a blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'b', edgecolor=True)
+
+    # testing c, fc, and ec combinations for stepfilled histograms
+    h = ax.hist(x, histtype='stepfilled', color='r', ec='b',
+                label="red stepfilled hist, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+    h = ax.hist(x, histtype='stepfilled', fc='r', ec='b',
+                label="red stepfilled hist, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+    h = ax.hist(x, histtype='stepfilled', fc='none', ec='b',
+                label="unfilled blue stepfilled hist, blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'b', edgecolor=True)
+
+    h = ax.hist(x, histtype='stepfilled', fc='r', ec='none',
+                label="edgeless red stepfilled hist with a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_patch_color(h, leg, 'r', facecolor=True)
+
+
+def assert_last_legend_linemarker_color(line_marker, leg, expected_color, color=False,
+                                        facecolor=False, edgecolor=False):
+    """
+    Check that line marker color, legend handle color, and legend label color all
+    match the expected input. Provide color, facecolor and edgecolor flags to clarify
+    which feature to match.
+    """
+    label_color = leg.texts[-1].get_color()
+    leg_marker = leg.get_lines()[-1]
+    assert mpl.colors.same_color(label_color, expected_color)
+    if color:
+        assert mpl.colors.same_color(label_color, leg_marker.get_color())
+        assert mpl.colors.same_color(label_color, line_marker.get_color())
+    if facecolor:
+        assert mpl.colors.same_color(label_color, leg_marker.get_markerfacecolor())
+        assert mpl.colors.same_color(label_color, line_marker.get_markerfacecolor())
+    if edgecolor:
+        assert mpl.colors.same_color(label_color, leg_marker.get_markeredgecolor())
+        assert mpl.colors.same_color(label_color, line_marker.get_markeredgecolor())
+
+
+def test_legend_labelcolor_linecolor_plot():
+    x = np.arange(5)
+
+    # testing line plot
+    fig, ax = plt.subplots()
+    l, = ax.plot(x, c='r', label="red line with a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'r', color=True)
+
+    # testing c, fc, and ec combinations for maker plots
+    l, = ax.plot(x, 'o', c='r', label="red circles with a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'r', color=True)
+
+    l, = ax.plot(x, 'o', c='r', mec='b', label="red circles, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'r', color=True)
+
+    l, = ax.plot(x, 'o', mfc='r', mec='b', label="red circles, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'r', facecolor=True)
+
+    # 'none' cases
+    l, = ax.plot(x, 'o', mfc='none', mec='b',
+                 label="blue unfilled circles, blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'b', edgecolor=True)
+
+    l, = ax.plot(x, 'o', mfc='r', mec='none', label="red edgeless circles, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'r', facecolor=True)
+
+    l, = ax.plot(x, 'o', c='none', mec='none',
+                 label="black label despite invisible circles for dummy entries")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_linemarker_color(l, leg, 'k')
+
+
+def assert_last_legend_scattermarker_color(scatter_marker, leg, expected_color,
+                                           facecolor=False, edgecolor=False):
+    """
+    Check that scatter marker color, legend handle color, and legend label color all
+    match the expected input. Provide facecolor and edgecolor flags to clarify
+    which feature to match.
+    """
+    label_color = leg.texts[-1].get_color()
+    leg_handle = leg.legend_handles[-1]
+    assert mpl.colors.same_color(label_color, expected_color)
+    if facecolor:
+        assert mpl.colors.same_color(label_color, leg_handle.get_facecolor())
+        assert mpl.colors.same_color(label_color, scatter_marker.get_facecolor())
+    if edgecolor:
+        assert mpl.colors.same_color(label_color, leg_handle.get_edgecolor())
+        assert mpl.colors.same_color(label_color, scatter_marker.get_edgecolor())
+
+
+def test_legend_labelcolor_linecolor_scatter():
+    x = np.arange(5)
+
+    # testing c, fc, and ec combinations for scatter plots
+    fig, ax = plt.subplots()
+    s = ax.scatter(x, x, c='r', label="red circles with a red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True)
+
+    s = ax.scatter(x, x, c='r', ec='b', label="red circles, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True)
+
+    s = ax.scatter(x, x, fc='r', ec='b', label="red circles, blue edges, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True)
+
+    # 'none' cases
+    s = ax.scatter(x, x, fc='none', ec='b', label="blue unfilled circles, blue label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'b', edgecolor=True)
+
+    s = ax.scatter(x, x, fc='r', ec='none', label="red edgeless circles, red label")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True)
+
+    s = ax.scatter(x, x, c='none', ec='none',
+                   label="black label despite invisible circles for dummy entries")
+    leg = ax.legend(labelcolor='linecolor')
+    assert_last_legend_scattermarker_color(s, leg, 'k')
+
+
 @pytest.mark.filterwarnings("ignore:No artists with labels found to put in legend")
 def test_get_set_draggable():
     legend = plt.legend()