Repository: matplotlib/matplotlib
Pull request: Fix RangeSlider for same init values #22686
## PR Summary
Change the way that polygon is initilized to cover edge case of same init values. The method axhspan is no longer used and the polygon is created manually. (closes #22686 )

## PR Checklist

<!-- Please mark any checkboxes that do not apply to this PR as [N/A]. -->
**Tests and Styling**
- [ ] Has pytest style unit tests (and `pytest` passes).
- [ ] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (install `flake8-docstrings` and run `flake8 --docstring-convention=all`).

**Documentation**
- [ ] New features are documented, with examples if plot related.
- [ ] New features have an entry in `doc/users/next_whats_new/` (follow instructions in README.rst there).
- [ ] API changes documented in `doc/api/next_api_changes/` (follow instructions in README.rst there).
- [ ] Documentation is sphinx and numpydoc compliant (the docs should [build](https://matplotlib.org/devel/documenting_mpl.html#building-the-docs) without error).

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at
  https://matplotlib.org/devel/gitwash/development_workflow.html.

- Do not create the PR out of main, but out of a separate branch.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[Bug]: cannot give init value for RangeSlider widget
### Bug summary

I think `xy[4] = .25, val[0]` should be commented in /matplotlib/widgets. py", line 915, in set_val
as it prevents to initialized value for RangeSlider

### Code for reproduction

```python
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import RangeSlider

# generate a fake image
np.random.seed(19680801)
N = 128
img = np.random.randn(N, N)

fig, axs = plt.subplots(1, 2, figsize=(10, 5))
fig.subplots_adjust(bottom=0.25)

im = axs[0].imshow(img)
axs[1].hist(img.flatten(), bins='auto')
axs[1].set_title('Histogram of pixel intensities')

# Create the RangeSlider
slider_ax = fig.add_axes([0.20, 0.1, 0.60, 0.03])
slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max(),valinit=[0.0,0.0])

# Create the Vertical lines on the histogram
lower_limit_line = axs[1].axvline(slider.val[0], color='k')
upper_limit_line = axs[1].axvline(slider.val[1], color='k')


def update(val):
    # The val passed to a callback by the RangeSlider will
    # be a tuple of (min, max)

    # Update the image's colormap
    im.norm.vmin = val[0]
    im.norm.vmax = val[1]

    # Update the position of the vertical lines
    lower_limit_line.set_xdata([val[0], val[0]])
    upper_limit_line.set_xdata([val[1], val[1]])

    # Redraw the figure to ensure it updates
    fig.canvas.draw_idle()


slider.on_changed(update)
plt.show()
```


### Actual outcome

```python
  File "<ipython-input-52-b704c53e18d4>", line 19, in <module>
    slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max(),valinit=[0.0,0.0])

  File "/Users/Vincent/opt/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/widgets.py", line 778, in __init__
    self.set_val(valinit)

  File "/Users/Vincent/opt/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/widgets.py", line 915, in set_val
    xy[4] = val[0], .25

IndexError: index 4 is out of bounds for axis 0 with size 4
```

### Expected outcome

range slider with user initial values

### Additional information

error can be removed by commenting this line
```python

    def set_val(self, val):
        """
        Set slider value to *val*.

        Parameters
        ----------
        val : tuple or array-like of float
        """
        val = np.sort(np.asanyarray(val))
        if val.shape != (2,):
            raise ValueError(
                f"val must have shape (2,) but has shape {val.shape}"
            )
        val[0] = self._min_in_bounds(val[0])
        val[1] = self._max_in_bounds(val[1])
        xy = self.poly.xy
        if self.orientation == "vertical":
            xy[0] = .25, val[0]
            xy[1] = .25, val[1]
            xy[2] = .75, val[1]
            xy[3] = .75, val[0]
            # xy[4] = .25, val[0]
        else:
            xy[0] = val[0], .25
            xy[1] = val[0], .75
            xy[2] = val[1], .75
            xy[3] = val[1], .25
            # xy[4] = val[0], .25
        self.poly.xy = xy
        self.valtext.set_text(self._format(val))
        if self.drawon:
            self.ax.figure.canvas.draw_idle()
        self.val = val
        if self.eventson:
            self._observers.process("changed", val)

```

### Operating system

OSX

### Matplotlib Version

3.5.1

### Matplotlib Backend

_No response_

### Python version

3.8

### Jupyter version

_No response_

### Installation

pip

Hints from issue discussion:
Huh, the polygon object must have changed inadvertently. Usually, you have
to "close" the polygon by repeating the first vertex, but we make it
possible for polygons to auto-close themselves. I wonder how (when?) this
broke?

On Tue, Mar 22, 2022 at 10:29 PM vpicouet ***@***.***> wrote:

> Bug summary
>
> I think xy[4] = .25, val[0] should be commented in /matplotlib/widgets.
> py", line 915, in set_val
> as it prevents to initialized value for RangeSlider
> Code for reproduction
>
> import numpy as npimport matplotlib.pyplot as pltfrom matplotlib.widgets import RangeSlider
> # generate a fake imagenp.random.seed(19680801)N = 128img = np.random.randn(N, N)
> fig, axs = plt.subplots(1, 2, figsize=(10, 5))fig.subplots_adjust(bottom=0.25)
> im = axs[0].imshow(img)axs[1].hist(img.flatten(), bins='auto')axs[1].set_title('Histogram of pixel intensities')
> # Create the RangeSliderslider_ax = fig.add_axes([0.20, 0.1, 0.60, 0.03])slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max(),valinit=[0.0,0.0])
> # Create the Vertical lines on the histogramlower_limit_line = axs[1].axvline(slider.val[0], color='k')upper_limit_line = axs[1].axvline(slider.val[1], color='k')
>
> def update(val):
>     # The val passed to a callback by the RangeSlider will
>     # be a tuple of (min, max)
>
>     # Update the image's colormap
>     im.norm.vmin = val[0]
>     im.norm.vmax = val[1]
>
>     # Update the position of the vertical lines
>     lower_limit_line.set_xdata([val[0], val[0]])
>     upper_limit_line.set_xdata([val[1], val[1]])
>
>     # Redraw the figure to ensure it updates
>     fig.canvas.draw_idle()
>
> slider.on_changed(update)plt.show()
>
> Actual outcome
>
>   File "<ipython-input-52-b704c53e18d4>", line 19, in <module>
>     slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max(),valinit=[0.0,0.0])
>
>   File "/Users/Vincent/opt/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/widgets.py", line 778, in __init__
>     self.set_val(valinit)
>
>   File "/Users/Vincent/opt/anaconda3/envs/py38/lib/python3.8/site-packages/matplotlib/widgets.py", line 915, in set_val
>     xy[4] = val[0], .25
>
> IndexError: index 4 is out of bounds for axis 0 with size 4
>
> Expected outcome
>
> range slider with user initial values
> Additional information
>
> error can be
>
>
>     def set_val(self, val):
>         """
>         Set slider value to *val*.
>
>         Parameters
>         ----------
>         val : tuple or array-like of float
>         """
>         val = np.sort(np.asanyarray(val))
>         if val.shape != (2,):
>             raise ValueError(
>                 f"val must have shape (2,) but has shape {val.shape}"
>             )
>         val[0] = self._min_in_bounds(val[0])
>         val[1] = self._max_in_bounds(val[1])
>         xy = self.poly.xy
>         if self.orientation == "vertical":
>             xy[0] = .25, val[0]
>             xy[1] = .25, val[1]
>             xy[2] = .75, val[1]
>             xy[3] = .75, val[0]
>             # xy[4] = .25, val[0]
>         else:
>             xy[0] = val[0], .25
>             xy[1] = val[0], .75
>             xy[2] = val[1], .75
>             xy[3] = val[1], .25
>             # xy[4] = val[0], .25
>         self.poly.xy = xy
>         self.valtext.set_text(self._format(val))
>         if self.drawon:
>             self.ax.figure.canvas.draw_idle()
>         self.val = val
>         if self.eventson:
>             self._observers.process("changed", val)
>
>
> Operating system
>
> OSX
> Matplotlib Version
>
> 3.5.1
> Matplotlib Backend
>
> *No response*
> Python version
>
> 3.8
> Jupyter version
>
> *No response*
> Installation
>
> pip
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/matplotlib/matplotlib/issues/22686>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AACHF6CW2HVLKT5Q56BVZDLVBJ6X7ANCNFSM5RMUEIDQ>
> .
> You are receiving this because you are subscribed to this thread.Message
> ID: ***@***.***>
>

Yes, i might have been too fast, cause it allows to skip the error but then it seems that the polygon is not right...
Let me know if you know how this should be solved...
![Capture d’écran, le 2022-03-22 à 23 20 23](https://user-images.githubusercontent.com/37241971/159617326-44c69bfc-bf0a-4f79-ab23-925c7066f2c2.jpg)


So I think you found an edge case because your valinit has both values equal. This means that the poly object created by `axhspan` is not as large as the rest of the code expects. 

https://github.com/matplotlib/matplotlib/blob/11737d0694109f71d2603ba67d764aa2fb302761/lib/matplotlib/widgets.py#L722

A quick workaround is to have the valinit contain two different numbers (even if only minuscule difference)
Yes you are right!
Thanks a lot for digging into this!!
Compare:

```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
poly_same_valinit = ax.axvspan(0, 0, 0, 1)
poly_diff_valinit = ax.axvspan(0, .5, 0, 1)
print(poly_same_valinit.xy.shape)
print(poly_diff_valinit.xy.shape)
```

which gives:

```
(4, 2)
(5, 2)
```

Two solutions spring to mind:

1. Easier option
Throw an error in init if `valinit[0] == valinit[1]`

2. Maybe better option?
Don't use axhspan and manually create the poly to ensure it always has the expected number of vertices
Option 2 might be better yes
@vpicouet any interest in opening a PR?
I don't think I am qualified to do so, never opened a PR yet. 
RangeSlider might also contain another issue. 
When I call `RangeSlider.set_val([0.0,0.1])`
It changes only the blue poly object and the range value on the right side of the slider not the dot:
![Capture d’écran, le 2022-03-25 à 15 53 44](https://user-images.githubusercontent.com/37241971/160191943-aef5fbe2-2f54-42ae-9719-23375767b212.jpg)
 
> I don't think I am qualified to do so, never opened a PR yet.

That's always true until you've opened your first one :). But I also understand that it can be intimidating.


>  RangeSlider might also contain another issue.
> When I call RangeSlider.set_val([0.0,0.1])
> It changes only the blue poly object and the range value on the right side of the slider not the dot:


oh hmm - good catch! may be worth opening a separate issue there as these are two distinct bugs and this one may be a bit more comlicated to fix.

Commit message: Create method _fill_verts

Patch to review:
diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py
index cd79e9523a04..8a54bdd1c40f 100644
--- a/lib/matplotlib/widgets.py
+++ b/lib/matplotlib/widgets.py
@@ -19,7 +19,7 @@
 from . import (_api, _docstring, backend_tools, cbook, colors, ticker,
                transforms)
 from .lines import Line2D
-from .patches import Circle, Rectangle, Ellipse
+from .patches import Circle, Rectangle, Ellipse, Polygon
 from .transforms import TransformedPatchPath, Affine2D
 
 
@@ -702,6 +702,8 @@ def __init__(
             f'marker{k}': v for k, v in {**defaults, **handle_style}.items()
         }
 
+        verts = np.zeros([5, 2])
+
         if orientation == "vertical":
             self.track = Rectangle(
                 (.25, 0), .5, 2,
@@ -709,7 +711,9 @@ def __init__(
                 facecolor=track_color
             )
             ax.add_patch(self.track)
-            self.poly = ax.axhspan(valinit[0], valinit[1], 0, 1, **kwargs)
+            verts = self._fill_verts(verts, valinit)
+            poly = Polygon(verts, **kwargs)
+            poly.set_transform(self.ax.get_yaxis_transform(which="grid"))
             handleXY_1 = [.5, valinit[0]]
             handleXY_2 = [.5, valinit[1]]
         else:
@@ -719,9 +723,15 @@ def __init__(
                 facecolor=track_color
             )
             ax.add_patch(self.track)
-            self.poly = ax.axvspan(valinit[0], valinit[1], 0, 1, **kwargs)
+            verts = self._fill_verts(verts, valinit)
+            poly = Polygon(verts, **kwargs)
+            poly.set_transform(self.ax.get_xaxis_transform(which="grid"))
             handleXY_1 = [valinit[0], .5]
             handleXY_2 = [valinit[1], .5]
+        poly.get_path()._interpolation_steps = 100
+        self.ax.add_patch(poly)
+        self.ax._request_autoscale_view()
+        self.poly = poly
         self._handles = [
             ax.plot(
                 *handleXY_1,
@@ -777,6 +787,22 @@ def __init__(
         self._active_handle = None
         self.set_val(valinit)
 
+    def _fill_verts(self, verts, val):
+        """Fills the *verts* Nx2 shaped numpy array."""
+        if self.orientation == "vertical":
+            verts[0] = .25, val[0]
+            verts[1] = .25, val[1]
+            verts[2] = .75, val[1]
+            verts[3] = .75, val[0]
+            verts[4] = .25, val[0]
+        else:
+            verts[0] = val[0], .25
+            verts[1] = val[0], .75
+            verts[2] = val[1], .75
+            verts[3] = val[1], .25
+            verts[4] = val[0], .25
+        return verts
+
     def _min_in_bounds(self, min):
         """Ensure the new min value is between valmin and self.val[1]."""
         if min <= self.valmin:
@@ -906,22 +932,11 @@ def set_val(self, val):
         val[0] = self._min_in_bounds(val[0])
         val[1] = self._max_in_bounds(val[1])
         xy = self.poly.xy
+        xy = self._fill_verts(xy, val)
         if self.orientation == "vertical":
-            xy[0] = .25, val[0]
-            xy[1] = .25, val[1]
-            xy[2] = .75, val[1]
-            xy[3] = .75, val[0]
-            xy[4] = .25, val[0]
-
             self._handles[0].set_ydata([val[0]])
             self._handles[1].set_ydata([val[1]])
         else:
-            xy[0] = val[0], .25
-            xy[1] = val[0], .75
-            xy[2] = val[1], .75
-            xy[3] = val[1], .25
-            xy[4] = val[0], .25
-
             self._handles[0].set_xdata([val[0]])
             self._handles[1].set_xdata([val[1]])
 


Code review comments:
diff_hunk: @@ -719,9 +723,15 @@ def __init__(
                 facecolor=track_color
             )
             ax.add_patch(self.track)
-            self.poly = ax.axvspan(valinit[0], valinit[1], 0, 1, **kwargs)
+            verts = self._fill_verts(verts, valinit)
+            poly = Polygon(verts, **kwargs)
+            poly.set_transform(self.ax.get_xaxis_transform(which="grid"))
             handleXY_1 = [valinit[0], .5]
             handleXY_2 = [valinit[1], .5]
+        poly.get_path()._interpolation_steps = 100
+        self.ax.add_patch(poly)
+        self.ax._request_autoscale_view()
+        self.poly = poly
path: lib/matplotlib/widgets.py
text: @user1:
With above comments you can pull more duplication out of the `if-else` and also can just directly assign to `self.poly`

```suggestion
        self.poly = Polygon(verts, **kwargs)
        self.poly.set_transform(poly_transform)
        self.poly.get_path()._interpolation_steps = 100
        self.ax.add_patch(self.poly)
        self.ax._request_autoscale_view()
```

diff_hunk: @@ -702,14 +702,18 @@ def __init__(
             f'marker{k}': v for k, v in {**defaults, **handle_style}.items()
         }
 
+        verts = np.zeros([5, 2])
path: lib/matplotlib/widgets.py
text: @user1:
You can also do
```suggestion
        verts = self._fill_verts(np.zeros([5,2]), valinit)
```

and then eliminate the usage in the if

diff_hunk: @@ -702,14 +702,18 @@ def __init__(
             f'marker{k}': v for k, v in {**defaults, **handle_style}.items()
         }
 
+        verts = np.zeros([5, 2])
+
         if orientation == "vertical":
             self.track = Rectangle(
                 (.25, 0), .5, 2,
                 transform=ax.transAxes,
                 facecolor=track_color
             )
             ax.add_patch(self.track)
-            self.poly = ax.axhspan(valinit[0], valinit[1], 0, 1, **kwargs)
+            verts = self._fill_verts(verts, valinit)
+            poly = Polygon(verts, **kwargs)
+            poly.set_transform(self.ax.get_yaxis_transform(which="grid"))
path: lib/matplotlib/widgets.py
text: @user1:
with above suggestion you can do:

```suggestion
            poly_transform = self.ax.get_yaxis_transform(which="grid"))
```

and then create the polygon as below