From cfb27b348139f635325734ff4509e6d67a4b6e54 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 26 Aug 2022 17:47:02 -0400 Subject: [PATCH 1/2] 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 --- lib/matplotlib/axes/_axes.py | 14 +++++++------- lib/matplotlib/cbook/__init__.py | 19 +++++++++++++++---- lib/matplotlib/dates.py | 2 +- lib/matplotlib/tests/test_axes.py | 24 ++++++++++++++++++++++++ lib/matplotlib/tests/test_cbook.py | 6 +++--- lib/matplotlib/units.py | 2 +- 6 files changed, 51 insertions(+), 16 deletions(-) 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: From bbf0cd2610eb253fdfee73276e0a57fb8a28851b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 13 Sep 2022 16:47:57 -0400 Subject: [PATCH 2/2] MNT: shorten logic + docstring --- lib/matplotlib/cbook/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 3c9a48a4dd7c..171c7a6fff1f 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -1708,7 +1708,8 @@ def safe_first_element(obj): def _safe_first_finite(obj, *, skip_nonfinite=True): """ - Return the first non-None element in *obj*. + Return the first non-None (and optionally finite) element in *obj*. + This is a method for internal use. This is an type-independent way of obtaining the first non-None element, @@ -1716,6 +1717,8 @@ def _safe_first_finite(obj, *, skip_nonfinite=True): The first non-None element will be obtained when skip_none is True. """ def safe_isfinite(val): + if val is None: + return False try: return np.isfinite(val) if np.isscalar(val) else True except TypeError: @@ -1743,10 +1746,7 @@ def safe_isfinite(val): raise RuntimeError("matplotlib does not " "support generators as input") else: - return next( - val for val in obj - if val is not None and safe_isfinite(val) - ) + return next(val for val in obj if safe_isfinite(val)) def sanitize_sequence(data):