Repository: matplotlib/matplotlib
Pull request: FIX: show bars when the first location is nan

## PR Summary

Due to the way we handle units on the bar width having an invalid value in the
first position of the x bar (of y of barh) would effectively poison all of the
widths making all of the bars invisible.

This also renames the cbook function _safe_first_non_none function ->
_safe_first_finite and adjusts the behavior to also drop nans

closes #23687


## PR Checklist

<!-- Please mark any checkboxes that do not apply to this PR as [N/A]. -->
**Tests and Styling**
- [x] Has pytest style unit tests (and `pytest` passes).
- [x] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (install `flake8-docstrings` and run `flake8 --docstring-convention=all`).



Problem statement:
[Bug]: barplot does not show anything when x or bottom start and end with NaN
### Bug summary

I am not sure if this is a bug or an optimization, but in most cases it is fine to plot bars of arrays containing NaN (only the NaN values are not displayed, the rest is ok).

Here I found that when the `x` array or the `bottom` array have NaNs at the extremities, no bar is displayed.

(`barh` has the same behavior).

### Code for reproduction

```python
import numpy as np, matplotlib as mpl, matplotlib.pyplot as plt

barx = np.arange(3, dtype=float)
barheights = np.array([0.5, 1.5, 2.0])
barstarts=np.array([0.77]*3)

barx[[0,2]] = np.NaN
# alternatively:
#barstarts[[0,2]] = np.NaN

fig, ax = plt.subplots()

ax.bar(barx, barheights, bottom=barstarts)
# Not ok, nothing shown!

# Same behavior with ax.barh:
ax.barh(barx, barheights, left=barstarts)

# However, the behavior is correct when NaN only affect the 'heights' argument:

fig, ax = plt.subplots()
ax.bar(np.arange(3), np.array([np.NaN, 1, np.NaN]), bottom=np.array([0.2, 0.3, 0.4]))
# OK, one bar is displayed.
```


### Actual outcome

zero bar displayed

### Expected outcome

Non-NaN elements should be shown

### Additional information

_No response_

### Operating system

Ubuntu 20.04.3

### Matplotlib Version

3.5.3

### Matplotlib Backend

TkAgg, pdf

### Python version

3.8.10

### Jupyter version

ipython

### Installation

pip

Hints from issue discussion:
I can reproduce this, but do not understand it yet.
In 7c4b000c423c9e136da52d75ebecdcd312156a8f / https://github.com/matplotlib/matplotlib/pull/12903 we added some logic to make sure that width would work with interesting units, notable time deltas and and datetime data.

To do this we effective add the width to the first element of the x data, run it through the conversion machinery, and then subtract off the already converted value of the first data point to convert the "width" in (possibly unitful) user data into our internal version of it.  However, if the first point in `nan`, then the width is  poisoned to also be `nan` which means that when we make the `Rectangles` the x positions are `nan` which then (correctly) does not render anything.
I guess we should be using the new safe_first_element excluding NaNs?
The new one only drops `None`, but I just finished adapting it to work with `nan` as well.  PR shortly....

Commit message: FIX: show bars when the first location is nan

Due to the way we handle units on the bar width having an invalid value in the
first position of the x bar (of y of barh) would effectively poison all of the
widths making all of the bars invisible.

This also renames the cbook function _safe_first_non_none function ->
_safe_first_finite and adjusts the behavior to also drop nans

closes #23687

Patch to review:
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index cb1d4c989c23..205b594e7588 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -2179,12 +2179,12 @@ def _convert_dx(dx, x0, xconv, convert):
             # removes the units from unit packages like `pint` that
             # wrap numpy arrays.
             try:
-                x0 = cbook._safe_first_non_none(x0)
+                x0 = cbook._safe_first_finite(x0)
             except (TypeError, IndexError, KeyError):
                 pass
 
             try:
-                x = cbook._safe_first_non_none(xconv)
+                x = cbook._safe_first_finite(xconv)
             except (TypeError, IndexError, KeyError):
                 x = xconv
 
@@ -2801,11 +2801,11 @@ def broken_barh(self, xranges, yrange, **kwargs):
         """
         # process the unit information
         if len(xranges):
-            xdata = cbook._safe_first_non_none(xranges)
+            xdata = cbook._safe_first_finite(xranges)
         else:
             xdata = None
         if len(yrange):
-            ydata = cbook._safe_first_non_none(yrange)
+            ydata = cbook._safe_first_finite(yrange)
         else:
             ydata = None
         self._process_unit_info(
@@ -3447,10 +3447,10 @@ def _upcast_err(err):
                     # safe_first_element because getitem is index-first not
                     # location first on pandas objects so err[0] almost always
                     # fails.
-                    isinstance(cbook._safe_first_non_none(err), np.ndarray)
+                    isinstance(cbook._safe_first_finite(err), np.ndarray)
             ):
                 # Get the type of the first element
-                atype = type(cbook._safe_first_non_none(err))
+                atype = type(cbook._safe_first_finite(err))
                 # Promote the outer container to match the inner container
                 if atype is np.ndarray:
                     # Converts using np.asarray, because data cannot
@@ -4313,7 +4313,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize,
         c_is_string_or_strings = (
             isinstance(c, str)
             or (np.iterable(c) and len(c) > 0
-                and isinstance(cbook._safe_first_non_none(c), str)))
+                and isinstance(cbook._safe_first_finite(c), str)))
 
         def invalid_shape_exception(csize, xsize):
             return ValueError(
diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py
index 17d1cad3a753..3c9a48a4dd7c 100644
--- a/lib/matplotlib/cbook/__init__.py
+++ b/lib/matplotlib/cbook/__init__.py
@@ -1703,10 +1703,10 @@ def safe_first_element(obj):
     This is an type-independent way of obtaining the first element,
     supporting both index access and the iterator protocol.
     """
-    return _safe_first_non_none(obj, skip_none=False)
+    return _safe_first_finite(obj, skip_nonfinite=False)
 
 
-def _safe_first_non_none(obj, skip_none=True):
+def _safe_first_finite(obj, *, skip_nonfinite=True):
     """
     Return the first non-None element in *obj*.
     This is a method for internal use.
@@ -1715,7 +1715,14 @@ def _safe_first_non_none(obj, skip_none=True):
     supporting both index access and the iterator protocol.
     The first non-None element will be obtained when skip_none is True.
     """
-    if skip_none is False:
+    def safe_isfinite(val):
+        try:
+            return np.isfinite(val) if np.isscalar(val) else True
+        except TypeError:
+            # This is something that numpy can not make heads or tails
+            # of, assume "finite"
+            return True
+    if skip_nonfinite is False:
         if isinstance(obj, collections.abc.Iterator):
             # needed to accept `array.flat` as input.
             # np.flatiter reports as an instance of collections.Iterator
@@ -1730,12 +1737,16 @@ def _safe_first_non_none(obj, skip_none=True):
                                "as input")
         return next(iter(obj))
     elif isinstance(obj, np.flatiter):
+        # TODO do the finite filtering on this
         return obj[0]
     elif isinstance(obj, collections.abc.Iterator):
         raise RuntimeError("matplotlib does not "
                            "support generators as input")
     else:
-        return next(val for val in obj if val is not None)
+        return next(
+            val for val in obj
+            if val is not None and safe_isfinite(val)
+        )
 
 
 def sanitize_sequence(data):
diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py
index 6dfc972f704d..dc01ffded2ec 100644
--- a/lib/matplotlib/dates.py
+++ b/lib/matplotlib/dates.py
@@ -1908,7 +1908,7 @@ def default_units(x, axis):
             x = x.ravel()
 
         try:
-            x = cbook._safe_first_non_none(x)
+            x = cbook._safe_first_finite(x)
         except (TypeError, StopIteration):
             pass
 
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index a230af2ac1e0..628f9542aa42 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -8109,3 +8109,27 @@ def test_get_xticklabel():
     for ind in range(10):
         assert ax.get_xticklabels()[ind].get_text() == f'{ind}'
         assert ax.get_yticklabels()[ind].get_text() == f'{ind}'
+
+
+def test_bar_leading_nan():
+
+    barx = np.arange(3, dtype=float)
+    barheights = np.array([0.5, 1.5, 2.0])
+    barstarts = np.array([0.77]*3)
+
+    barx[0] = np.NaN
+
+    fig, ax = plt.subplots()
+
+    bars = ax.bar(barx, barheights, bottom=barstarts)
+
+    hbars = ax.barh(barx, barheights, left=barstarts)
+
+    for bar_set in (bars, hbars):
+        # the first bar should have a nan in the location
+        nanful, *rest = bar_set
+        assert (~np.isfinite(nanful.xy)).any()
+        assert np.isfinite(nanful.get_width())
+        for b in rest:
+            assert np.isfinite(b.xy).all()
+            assert np.isfinite(b.get_width())
diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py
index 70f2c2499418..26748d1a5798 100644
--- a/lib/matplotlib/tests/test_cbook.py
+++ b/lib/matplotlib/tests/test_cbook.py
@@ -602,7 +602,7 @@ def test_flatiter():
     it = x.flat
     assert 0 == next(it)
     assert 1 == next(it)
-    ret = cbook._safe_first_non_none(it)
+    ret = cbook._safe_first_finite(it)
     assert ret == 0
 
     assert 0 == next(it)
@@ -758,7 +758,7 @@ def test_contiguous_regions():
 def test_safe_first_element_pandas_series(pd):
     # deliberately create a pandas series with index not starting from 0
     s = pd.Series(range(5), index=range(10, 15))
-    actual = cbook._safe_first_non_none(s)
+    actual = cbook._safe_first_finite(s)
     assert actual == 0
 
 
@@ -893,5 +893,5 @@ def test_format_approx():
 def test_safe_first_element_with_none():
     datetime_lst = [date.today() + timedelta(days=i) for i in range(10)]
     datetime_lst[0] = None
-    actual = cbook._safe_first_non_none(datetime_lst)
+    actual = cbook._safe_first_finite(datetime_lst)
     assert actual is not None and actual == datetime_lst[1]
diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py
index cf973034d341..2bcfcaf2eb12 100644
--- a/lib/matplotlib/units.py
+++ b/lib/matplotlib/units.py
@@ -197,7 +197,7 @@ def get_converter(self, x):
             except KeyError:
                 pass
         try:  # If cache lookup fails, look up based on first element...
-            first = cbook._safe_first_non_none(x)
+            first = cbook._safe_first_finite(x)
         except (TypeError, StopIteration):
             pass
         else:


Code review comments:
diff_hunk: @@ -1715,7 +1715,14 @@ def _safe_first_non_none(obj, skip_none=True):
     supporting both index access and the iterator protocol.
     The first non-None element will be obtained when skip_none is True.
     """
-    if skip_none is False:
+    def safe_isfinite(val):
+        try:
+            return np.isfinite(val) if np.isscalar(val) else True
path: lib/matplotlib/cbook/__init__.py
text: @user1:
I guess that's `return np.isscalar(val) and np.isfinite(val)`?

@author:
👍🏻 

@author:
These are not the same.  If `np.isscalar(val)` is False the expression needs to return `True` 

@user1:
Ah, oops.

diff_hunk: @@ -1730,12 +1737,16 @@ def _safe_first_non_none(obj, skip_none=True):
                                "as input")
         return next(iter(obj))
     elif isinstance(obj, np.flatiter):
+        # TODO do the finite filtering on this
         return obj[0]
     elif isinstance(obj, collections.abc.Iterator):
         raise RuntimeError("matplotlib does not "
                            "support generators as input")
     else:
-        return next(val for val in obj if val is not None)
+        return next(
+            val for val in obj
+            if val is not None and safe_isfinite(val)
path: lib/matplotlib/cbook/__init__.py
text: @user1:
Logically, should we absorb the None check in safe_isfinite? -I think None is not finite.

@author:
I think we need to check this here:

```
In [62]: np.isfinite(None)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In [63], line 1
----> 1 np.isfinite(None)

TypeError: ufunc 'isfinite' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
```



@user1:
Yes, we need the check. I was suggesting that it could logically belong inside `safe_isfinite` (by the  argument that `None` is not a finite number, similar to NaN).

i.e.
```
def safe_isfinite(val):
    if val is None:
        return False
    ...
```

and here `next(val for val in obj if safe_isfinite(val))`

I just realized that this is an inner function, so it's not too important, but I'd still have a slight preference. I'll approve and you can decide whether you want to change it or leave it.

@author:
I agree that is nicer.