Repository: matplotlib/matplotlib

Issue:
[Bug]: `ax.stairs()` creates inaccurate `fill` for the plot 
### Bug summary

I was working on a code to achieve the following look: 

![image](https://github.com/matplotlib/matplotlib/assets/8880777/7aae54ff-ff30-4ace-8f9d-cc40804b640c)

I got there by combining `ax.plot()` and `ax.fill_between()` functions. But then I've learnt about the `ax.stairs()` function which is suppose to plot in a step-like fashion and allows to add fill under the line **in one go**.

However, when I tried `ax.stairs()` I got the following result:

![image](https://github.com/matplotlib/matplotlib/assets/8880777/0c016dfe-486d-47f7-b542-76516545f77f)

The bug (I think):
* the fill is completely wrong - it doesn't stretch between `x-axis` and the line

Secondary issues:
* I don't think there's a way to have a separate colour for line and fill at the moment, when I set a colour, line disappears - this is especially visibile when I set e.g. `alpha=0.1`
* I've got a feeling that somehow the grid ends up on top of the plot when I call `ax.stairs()`

### Code for reproduction

```python
python
import matplotlib.pyplot as plt

x = [1, 4, 10, 12, 15, 20]
y = [2, 4, 1, 3, 1, 4]

fig, axs = plt.subplots(
    2, 1,
    figsize=(16,10),
    facecolor="white",
    sharex=True,
    sharey=True
)
fig.suptitle(
    "ax.stairs()",
    fontsize=20,
)

# plot points
for ax in axs:
    ax.scatter(
        x, y,
        marker="X",
        s=275,
        linewidth=1.5,
        edgecolor="black",
        facecolor="white",
        zorder=100  # crude way to keep the points always on top
    )

axs[0].stairs(
    values=y,  # step height
    edges=[x[0], *x],  # edge width
    baseline=None,
    color="red",
    fill=True,
    label="steps-pre: duplicate 1st element in `edges=[x[0], *x]`"
)
axs[1].stairs(
    values=y,  # step height
    edges=[*x, x[-1]],  # edge
    baseline=None,
    color="green",
    fill=True,
    alpha=0.1,
    label="steps-post: duplicate last element in `edges=[*x, x[-1]]`"
)

# set axis limits
axs[0].set_ylim(0, 5.5)
axs[0].set_yticks(range(1, 6))
axs[0].set_xlim(0, 21)
axs[1].set_xticks(range(1, 21))

# add legend
for ax in axs:
    ax.legend(
        fontsize=16,
        loc="upper center",
        facecolor="whitesmoke",
        edgecolor="none"
    )

    # Update font size of the 'x' and 'y' axis labels
    ax.tick_params(
        axis="both",
        labelsize=16
    )

    ax.grid(
        color="gainsboro",
        linewidth=0.5
    )

# Remove spines
for position in ["top", "right"]:
    for ax in axs:
        ax.spines[position].set_visible(False)

plt.tight_layout()
```
```


### Actual outcome

![image](https://github.com/matplotlib/matplotlib/assets/8880777/f726f74e-5d28-488d-bf66-6536093b3227)


### Expected outcome

![image](https://github.com/matplotlib/matplotlib/assets/8880777/88a1c5e4-f50f-4b30-8f41-59f69aeee169)


### Additional information

_No response_

### Operating system

Windows 

### Matplotlib Version

3.7.2

### Matplotlib Backend

module://matplotlib_inline.backend_inline

### Python version

3.10.2

### Jupyter version

IPython          : 8.14.0 ipykernel        : 6.25.1 ipywidgets       : not installed jupyter_client   : 8.3.0 jupyter_core     : 5.3.1 jupyter_server   : not installed jupyterlab       : not installed nbclient         : not installed nbconvert        : not installed nbformat         : not installed notebook         : not installed qtconsole        : not installed traitlets        : 5.9.0

### Installation

pip


Discussion:
I may be missing something here, but if you pass `baseline=0` (or skip setting it to `None`) it should solve the fill issue.

With that said, I agree that I cannot make it use different colors for the face and the edge. Also, I would suspect that even if it was possible, the edge color probably would be applied to the whole `StepPatch`, but that is pure speculation.
Thanks for the suggestion, I missed that before. 

Indeed, now I can get the fill that goes all the way to the bottom of the plot.

However, I still think it's not right that I can either get a fill _or_ the line, I cannot have both - this is a bit annoying in a corner case like the bottom graph from my original example.

![image](https://github.com/matplotlib/matplotlib/assets/8880777/7aec0323-d080-4874-a945-3e464a6f0cbd)

On top of that, the doc says, `baseline=None` is a valid parameter value:

![image](https://github.com/matplotlib/matplotlib/assets/8880777/d3726912-31e6-4665-8bf7-a1732f0feb3f)

Therefore I'd expect it produces something more sensible than a fill with a slanted line connecting start and end point of the dataset?

![image](https://github.com/matplotlib/matplotlib/assets/8880777/b1d32b49-423d-4137-8667-538b1d40e1a4)




> Therefore I'd expect it produces something more sensible than a fill with a slanted line connecting start and end point of the dataset?

I think joining the first and last points is a reasonable default. I don't think we would change it at this point.  
> > Therefore I'd expect it produces something more sensible than a fill with a slanted line connecting start and end point of the dataset?
> 
> I think joining the first and last points is a reasonable default. I don't think we would change it at this point.

I'm struggling to think of a single use case where an outcome that creates slanted shading like presented above would be a desired outcome.

Perhaps, a better default would be to set the baseline to `min(y)` if `baseline=None`?

This would look as follows:

![image](https://github.com/matplotlib/matplotlib/assets/8880777/70e0cefa-a1ab-4165-aeab-4f5dafe69378)

Again, without the line the plot doesn't look as good, but at least the shading seems aligned with the data.

A desired outcome recreated with `ax.plot()` and `ax.fill_between` would look as follows:

![image](https://github.com/matplotlib/matplotlib/assets/8880777/d4457f3e-f7f3-4d42-bafd-1c212a0bdcfc)

> > > Therefore I'd expect it produces something more sensible than a fill with a slanted line connecting start and end point of the dataset?
> > 
> > 
> > I think joining the first and last points is a reasonable default. I don't think we would change it at this point.
> 
> I'm struggling to think of a single use case where an outcome that creates slanted shading like presented above would be a desired outcome.
> 
> Perhaps, a better default would be to set the baseline to `min(y)` if `baseline=None`?


I agree with that.


Actually, the better approach would be to add a new option `baseline='min'` for that. This is more explicit that `None` and has the advantage that there is no API breakage involved.
Unfortunately we can not, with a single Artist, get the desired effect.  Looking at the combinations of fill and baseline, we see:

```python
from cycler import cycler
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 2, layout="constrained")

cycle = cycler("baseline", [0, None]) * cycler("fill", [False, True])


for kwargs, ax in zip(cycle, axs.ravel()):
    ax.stairs(
        [4, 5, 1, 0, 2],
        [1, 2, 3, 4, 5, 6],
        facecolor="blue",
        lw=5,
        edgecolor="k",
        **kwargs
    )

    ax.set_title("baseline={baseline} fill={fill}".format(**kwargs))
```
![so](https://github.com/matplotlib/matplotlib/assets/199813/ce198f16-29aa-4807-9458-fbf5b1ecafce)

so 3 out of 4 of them make sense!


The issue is that for stairs we draw the output as a single path. A path has a facecolor (the fill) and the edgecolor (the stroke).  There in no way to stroke _part_ of a Path, either we stroke it with a fixed width or color or not (leaving aside some really wild things with paths that are smaller than their line widths).  The one minor escape hatch we have is that if you fill an open path you can get exactly one edge not stroked which will go straight from the last to first point. This behavior is about the only sensible thing to do as the other options are error, silently not fill, or to fill all space.

It is not possible to draw the stairs missing the stroke on 3 edges (bottom and the two "sides") using a single path.  Either you include the baseline points, and then we will stroke the front and back, or you do not include them and then the fill is sensible but not at all what you want.

I think the best you can do is two calls to stairs like

```python
fig, ax = plt.subplots()
ax.stairs(
        [4, 5, 1, 0, 2],
        [1, 2, 3, 4, 5, 6],
        facecolor="blue",
        lw=0,
        fill=True,
        baseline=0
    )
ax.stairs(
        [4, 5, 1, 0, 2],
        [1, 2, 3, 4, 5, 6],
        lw=5,
        edgecolor="k",
        fill=False,
        baseline=None
    )

```

![so](https://github.com/matplotlib/matplotlib/assets/199813/afea74e3-f99c-4a78-9345-f9a1386e6e77)

> Unfortunately we can not, with a single Artist, get the desired effect. Looking at the combinations of fill and baseline, we see:
> 
> ```python
> from cycler import cycler
> import matplotlib.pyplot as plt
> 
> fig, axs = plt.subplots(2, 2, layout="constrained")
> 
> cycle = cycler("baseline", [0, None]) * cycler("fill", [False, True])
> 
> 
> for kwargs, ax in zip(cycle, axs.ravel()):
>     ax.stairs(
>         [4, 5, 1, 0, 2],
>         [1, 2, 3, 4, 5, 6],
>         facecolor="blue",
>         lw=5,
>         edgecolor="k",
>         **kwargs
>     )
> 
>     ax.set_title("baseline={baseline} fill={fill}".format(**kwargs))
> ```
> 
> ![so](https://user-images.githubusercontent.com/199813/270037268-ce198f16-29aa-4807-9458-fbf5b1ecafce.png)
> 
> so 3 out of 4 of them make sense!
> 
> The issue is that for stairs we draw the output as a single path. A path has a facecolor (the fill) and the edgecolor (the stroke). There in no way to stroke _part_ of a Path, either we stroke it with a fixed width or color or not (leaving aside some really wild things with paths that are smaller than their line widths). The one minor escape hatch we have is that if you fill an open path you can get exactly one edge not stroked which will go straight from the last to first point. This behavior is about the only sensible thing to do as the other options are error, silently not fill, or to fill all space.
> 
> It is not possible to draw the stairs missing the stroke on 3 edges (bottom and the two "sides") using a single path. Either you include the baseline points, and then we will stroke the front and back, or you do not include them and then the fill is sensible but not at all what you want.
> 
> I think the best you can do is two calls to stairs like
> 
> ```python
> fig, ax = plt.subplots()
> ax.stairs(
>         [4, 5, 1, 0, 2],
>         [1, 2, 3, 4, 5, 6],
>         facecolor="blue",
>         lw=0,
>         fill=True,
>         baseline=0
>     )
> ax.stairs(
>         [4, 5, 1, 0, 2],
>         [1, 2, 3, 4, 5, 6],
>         lw=5,
>         edgecolor="k",
>         fill=False,
>         baseline=None
>     )
> ```
> 
> ![so](https://user-images.githubusercontent.com/199813/270039277-afea74e3-f99c-4a78-9345-f9a1386e6e77.png)

I accept this explanation and it makes sense it might not be worth the effort to make any changes if the function works in majority of use cases.

And one thing that you showed me is that you actually can set fill and edge colour in one call to `ax.stairs`.

However one thing that bothers me now as a bit of inconsistency is that if you don't explicitly specify the `lw` parameter inside the `ax.stairs()`, then the default values are:
* `lw=1` if `fill=False`
* `lw=0` if `fill=True`

Why can't it always be `lw=1`?

I discovered this by playing with your code:
```python
from cycler import cycler
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 2, layout="constrained")

cycle = cycler("baseline", [0, None]) * cycler("fill", [False, True])

artists=[]

for kwargs, ax in zip(cycle, axs.ravel()):
    a = ax.stairs(
        [4, 5, 1, 0, 2],
        [1, 2, 3, 4, 5, 6],
        facecolor="blue",
        # lw=5,
        edgecolor="k",
        **kwargs
    )
    artists.append(a)

    ax.set_title("baseline={baseline} fill={fill}".format(**kwargs))
```

It gives you this:

![image](https://github.com/matplotlib/matplotlib/assets/8880777/d07e124a-eee5-4c5f-b0df-e8cb9bad84e1)

At first I thought that perhaps the line is so thin and it somehow get printed behind the fill and that's why you can't see it. But when I checked the actual value, it indicated zero:

```python
for a in artists:
    print(a.get_linewidth())

# Output:
1.0
0.0
1.0
0.0
```

As part of the 2.0 style change we dropped edges on all filled things by default, but if there is no fill then we _have_ to draw the edge (or it is invisible!).



Patch:
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index b65004b8c272..673998defb66 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -7194,9 +7194,16 @@ def stairs(self, values, edges=None, *,
             True or an array is passed to *baseline*, a closed
             path is drawn.
 
+            If None, then drawn as an unclosed Path.
+
         fill : bool, default: False
             Whether the area under the step curve should be filled.
 
+            Passing both ``fill=True` and ``baseline=None`` will likely result in
+            undesired filling: the first and last points will be connected
+            with a straight line and the fill will be between this line and the stairs.
+
+
         Returns
         -------
         StepPatch : `~matplotlib.patches.StepPatch`
@@ -7234,6 +7241,16 @@ def stairs(self, values, edges=None, *,
                                    fill=fill,
                                    **kwargs)
         self.add_patch(patch)
+        if baseline is None and fill:
+            _api.warn_external(
+                f"Both {baseline=} and {fill=} have been passed. "
+                "baseline=None is only intended for unfilled stair plots. "
+                "Because baseline is None, the Path used to draw the stairs will "
+                "not be closed, thus because fill is True the polygon will be closed "
+                "by drawing an (unstroked) edge from the first to last point.  It is "
+                "very likely that the resulting fill patterns is not the desired "
+                "result."
+            )
         if baseline is None:
             baseline = 0
         if orientation == 'vertical':


Test patch:
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 819f4eb3b598..3644f0861d1b 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -2342,6 +2342,18 @@ def test_hist_zorder(histtype, zorder):
         assert patch.get_zorder() == zorder
 
 
+def test_stairs_no_baseline_fill_warns():
+    fig, ax = plt.subplots()
+    with pytest.warns(UserWarning, match="baseline=None and fill=True"):
+        ax.stairs(
+            [4, 5, 1, 0, 2],
+            [1, 2, 3, 4, 5, 6],
+            facecolor="blue",
+            baseline=None,
+            fill=True
+        )
+
+
 @check_figures_equal(extensions=['png'])
 def test_stairs(fig_test, fig_ref):
     import matplotlib.lines as mlines