Repository: pandas-dev/pandas
Pull request #55253 (2023-09-22 23:53:31)

TYP: towards matplotlib 3.8
Type changes for matplotlib 3.8. There is more work needed:
- failing CI with 3.8 https://github.com/pandas-dev/pandas/pull/55210#issuecomment-1732096783
- refactor the use of matplotlib to not set non-existing attributes matploblib object and to avoid using private methods (see mypy errors below). This can be done in a separate PR (or we add type ignores for now).

Typing errors left with 3.8:
```
pandas/plotting/_matplotlib/timeseries.py:127: error: "Axes" has no attribute "_plot_data"; maybe "plot_date"?  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:139: error: "Axes" has no attribute "_plot_data"; maybe "plot_date"?  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:156: error: "Axes" has no attribute "_plot_data"; maybe "plot_date"?  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:158: error: "Axes" has no attribute "freq"  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:160: error: "XAxis" has no attribute "freq"  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:162: error: "Axes" has no attribute "legendlabels"  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:165: error: "Axes" has no attribute "view_interval"  [attr-defined]
pandas/plotting/_matplotlib/timeseries.py:166: error: "Axes" has no attribute "date_axis_info"  [attr-defined]
pandas/plotting/_matplotlib/core.py:468: error: "Axes" has no attribute "containers"  [attr-defined]
pandas/plotting/_matplotlib/core.py:485: error: "_AxesBase" has no attribute "_get_lines"; maybe "get_lines"?  [attr-defined]
pandas/plotting/_matplotlib/core.py:485: error: "Axes" has no attribute "_get_lines"  [attr-defined]
pandas/plotting/_matplotlib/core.py:486: error: "_AxesBase" has no attribute "_get_patches_for_fill"  [attr-defined]
pandas/plotting/_matplotlib/core.py:486: error: "Axes" has no attribute "_get_patches_for_fill"  [attr-defined]
pandas/plotting/_matplotlib/core.py:487: error: "Axes" has no attribute "right_ax"  [attr-defined]
pandas/plotting/_matplotlib/core.py:487: error: "_AxesBase" has no attribute "left_ax"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1440: error: "Axes" has no attribute "_plot_data"; maybe "plot_date"?  [attr-defined]
pandas/plotting/_matplotlib/core.py:1444: error: "Axes" has no attribute "freq"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1458: error: "Axes" has no attribute "_stacker_pos_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1460: error: "Axes" has no attribute "_stacker_neg_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1461: error: "Axes" has no attribute "_stacker_pos_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1462: error: "Axes" has no attribute "_stacker_neg_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1473: error: "Axes" has no attribute "_stacker_pos_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1475: error: "Axes" has no attribute "_stacker_neg_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1488: error: "Axes" has no attribute "_stacker_pos_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1490: error: "Axes" has no attribute "_stacker_neg_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1590: error: "Axes" has no attribute "_stacker_pos_prior"  [attr-defined]
pandas/plotting/_matplotlib/core.py:1592: error: "Axes" has no attribute "_stacker_neg_prior"  [attr-defined]
```

This PR will obviously fail as it doesn't update matplotlib itself.

mroeschke: > refactor the use of matplotlib to not set non-existing attributes matploblib objec

xref https://github.com/pandas-dev/pandas/issues/54485
ksunden: I have also opened matplotlib/matplotlib#26883, FYI, which fixes up some of them that we could do better on (not _all_, particularly I may do a followup regarding the `None | Axis | _DummyAxis | _AxisWrapper` stuff, though I at least made `self.set_axis(self.axis)` typecheck as okay.


Not handling the `None` case is something we do ourselves, for the record.
twoertwein: For some reason this line

https://github.com/pandas-dev/pandas/blame/9e1096e8373bc99675fd1b3490cfb7cf26041395/doc/source/user_guide/visualization.rst#L747

breaks the doc-test with matplotlib 3.8

> Sphinx parallel build error:
> RuntimeError: Unexpected exception in `/home/runner/work/pandas/pandas/doc/source/user_guide/visualization.rst` line 747
> Error: Process completed with exit code 2.
github-actions[bot]: This pull request is stale because it has been open for thirty days with no activity. Please [update](https://pandas.pydata.org/pandas-docs/stable/development/contributing.html#updating-your-pull-request) and respond to this comment if you're still interested in working on this.
twoertwein: Thank you @mroeschke for rebasing! I'm very busy until at least 11/29 - if you or someone else wants to take over this PR, feel free to!

I think this PR might not need all changes since matplotlib 3.8.1 released (see failing mypy/pyright tests).

Patch:
diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml
index b22f8cb34c..94652e8586 100644
--- a/ci/deps/actions-310.yaml
+++ b/ci/deps/actions-310.yaml
@@ -33,7 +33,7 @@ dependencies:
   - gcsfs>=2022.11.0
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - odfpy>=1.4.1
diff --git a/ci/deps/actions-311-downstream_compat.yaml b/ci/deps/actions-311-downstream_compat.yaml
index ceea734352..bf47bfe3a8 100644
--- a/ci/deps/actions-311-downstream_compat.yaml
+++ b/ci/deps/actions-311-downstream_compat.yaml
@@ -34,7 +34,7 @@ dependencies:
   - gcsfs>=2022.11.0
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - odfpy>=1.4.1
diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml
index 1c9f349c2e..bd9e2059ef 100644
--- a/ci/deps/actions-311.yaml
+++ b/ci/deps/actions-311.yaml
@@ -33,7 +33,7 @@ dependencies:
   - gcsfs>=2022.11.0
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - odfpy>=1.4.1
diff --git a/ci/deps/actions-39.yaml b/ci/deps/actions-39.yaml
index 92b88a3409..cf4087a3e4 100644
--- a/ci/deps/actions-39.yaml
+++ b/ci/deps/actions-39.yaml
@@ -33,7 +33,7 @@ dependencies:
   - gcsfs>=2022.11.0
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - odfpy>=1.4.1
diff --git a/ci/deps/circle-310-arm64.yaml b/ci/deps/circle-310-arm64.yaml
index f81b91fcba..abe6145d07 100644
--- a/ci/deps/circle-310-arm64.yaml
+++ b/ci/deps/circle-310-arm64.yaml
@@ -33,7 +33,7 @@ dependencies:
   - gcsfs>=2022.11.0
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - odfpy>=1.4.1
diff --git a/environment.yml b/environment.yml
index e41389c7f2..aea71efd72 100644
--- a/environment.yml
+++ b/environment.yml
@@ -35,7 +35,7 @@ dependencies:
   - ipython
   - jinja2>=3.1.2
   - lxml>=4.9.2
-  - matplotlib>=3.6.3, <3.8
+  - matplotlib>=3.6.3
   - numba>=0.56.4
   - numexpr>=2.8.4
   - openpyxl>=3.1.0
diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx
index f073d099d3..c2d2acd95c 100644
--- a/pandas/_libs/tslib.pyx
+++ b/pandas/_libs/tslib.pyx
@@ -31,9 +31,14 @@ import numpy as np
 
 cnp.import_array()
 
+from pandas._libs.tslibs.dtypes cimport (
+    get_supported_reso,
+    npy_unit_to_abbrev,
+)
 from pandas._libs.tslibs.np_datetime cimport (
     NPY_DATETIMEUNIT,
     NPY_FR_ns,
+    get_datetime64_unit,
     import_pandas_datetime,
     npy_datetimestruct,
     npy_datetimestruct_to_datetime,
@@ -441,6 +446,7 @@ cpdef array_to_datetime(
     utc : bool, default False
          indicator whether the dates should be UTC
     creso : NPY_DATETIMEUNIT, default NPY_FR_ns
+        Set to NPY_FR_GENERIC to infer a resolution.
 
     Returns
     -------
@@ -464,14 +470,19 @@ cpdef array_to_datetime(
         set out_tzoffset_vals = set()
         tzinfo tz_out = None
         cnp.flatiter it = cnp.PyArray_IterNew(values)
-        DatetimeParseState state = DatetimeParseState()
-        str reso_str
+        NPY_DATETIMEUNIT item_reso
+        bint infer_reso = creso == NPY_DATETIMEUNIT.NPY_FR_GENERIC
+        DatetimeParseState state = DatetimeParseState(creso)
+        str abbrev
 
     # specify error conditions
     assert is_raise or is_ignore or is_coerce
 
-    reso_str = npy_unit_to_abbrev(creso)
-    result = np.empty((<object>values).shape, dtype=f"M8[{reso_str}]")
+    if infer_reso:
+        abbrev = "ns"
+    else:
+        abbrev = npy_unit_to_abbrev(creso)
+    result = np.empty((<object>values).shape, dtype=f"M8[{abbrev}]")
     iresult = result.view("i8").ravel()
 
     for i in range(n):
@@ -484,19 +495,38 @@ cpdef array_to_datetime(
                 iresult[i] = NPY_NAT
 
             elif PyDateTime_Check(val):
+                if isinstance(val, _Timestamp):
+                    item_reso = val._creso
+                else:
+                    item_reso = NPY_DATETIMEUNIT.NPY_FR_us
+                state.update_creso(item_reso)
+                if infer_reso:
+                    creso = state.creso
                 tz_out = state.process_datetime(val, tz_out, utc_convert)
                 iresult[i] = parse_pydatetime(val, &dts, creso=creso)
 
             elif PyDate_Check(val):
+                item_reso = NPY_DATETIMEUNIT.NPY_FR_s
+                state.update_creso(item_reso)
+                if infer_reso:
+                    creso = state.creso
                 iresult[i] = pydate_to_dt64(val, &dts, reso=creso)
                 state.found_other = True
 
             elif is_datetime64_object(val):
+                item_reso = get_supported_reso(get_datetime64_unit(val))
+                state.update_creso(item_reso)
+                if infer_reso:
+                    creso = state.creso
                 iresult[i] = get_datetime64_nanos(val, creso)
                 state.found_other = True
 
             elif is_integer_object(val) or is_float_object(val):
                 # these must be ns unit by-definition
+                item_reso = NPY_FR_ns
+                state.update_creso(item_reso)
+                if infer_reso:
+                    creso = state.creso
 
                 if val != val or val == NPY_NAT:
                     iresult[i] = NPY_NAT
@@ -514,11 +544,20 @@ cpdef array_to_datetime(
                 if parse_today_now(val, &iresult[i], utc, creso):
                     # We can't _quite_ dispatch this to convert_str_to_tsobject
                     #  bc there isn't a nice way to pass "utc"
+                    item_reso = NPY_DATETIMEUNIT.NPY_FR_us
+                    state.update_creso(item_reso)
+                    if infer_reso:
+                        creso = state.creso
                     continue
 
                 _ts = convert_str_to_tsobject(
                     val, None, unit="ns", dayfirst=dayfirst, yearfirst=yearfirst
                 )
+                item_reso = _ts.creso
+                state.update_creso(item_reso)
+                if infer_reso:
+                    creso = state.creso
+
                 _ts.ensure_reso(creso, val)
 
                 iresult[i] = _ts.value
@@ -586,6 +625,23 @@ cpdef array_to_datetime(
             # e.g. test_to_datetime_mixed_awareness_mixed_types
             raise ValueError("Cannot mix tz-aware with tz-naive values")
 
+    if infer_reso:
+        if state.creso_ever_changed:
+            # We encountered mismatched resolutions, need to re-parse with
+            #  the correct one.
+            return array_to_datetime(
+                values,
+                errors=errors,
+                yearfirst=yearfirst,
+                dayfirst=dayfirst,
+                utc=utc,
+                creso=state.creso,
+            )
+
+        # Otherwise we can use the single reso that we encountered and avoid
+        #  a second pass.
+        abbrev = npy_unit_to_abbrev(state.creso)
+        result = iresult.view(f"M8[{abbrev}]")
     return result, tz_out
 
 
diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py
index 5a7ceabbf5..848fb77c94 100644
--- a/pandas/plotting/_matplotlib/converter.py
+++ b/pandas/plotting/_matplotlib/converter.py
@@ -64,6 +64,8 @@ import pandas.core.tools.datetimes as tools
 if TYPE_CHECKING:
     from collections.abc import Generator
 
+    from matplotlib.axis import Axis
+
     from pandas._libs.tslibs.offsets import BaseOffset
 
 
@@ -187,7 +189,7 @@ class TimeFormatter(Formatter):
     def __init__(self, locs) -> None:
         self.locs = locs
 
-    def __call__(self, x, pos: int = 0) -> str:
+    def __call__(self, x, pos: int | None = 0) -> str:
         """
         Return the time of day as a formatted string.
 
@@ -364,8 +366,14 @@ class PandasAutoDateLocator(mdates.AutoDateLocator):
             locator = MilliSecondLocator(self.tz)
             locator.set_axis(self.axis)
 
-            locator.axis.set_view_interval(*self.axis.get_view_interval())
-            locator.axis.set_data_interval(*self.axis.get_data_interval())
+            # error: Item "None" of "Axis | _DummyAxis | _AxisWrapper | None"
+            # has no attribute "get_data_interval"
+            locator.axis.set_view_interval(  # type: ignore[union-attr]
+                *self.axis.get_view_interval()  # type: ignore[union-attr]
+            )
+            locator.axis.set_data_interval(  # type: ignore[union-attr]
+                *self.axis.get_data_interval()  # type: ignore[union-attr]
+            )
             return locator
 
         return mdates.AutoDateLocator.get_locator(self, dmin, dmax)
@@ -950,6 +958,8 @@ class TimeSeries_DateLocator(Locator):
     day : {int}, optional
     """
 
+    axis: Axis
+
     def __init__(
         self,
         freq: BaseOffset,
@@ -999,7 +1009,9 @@ class TimeSeries_DateLocator(Locator):
             base = self.base
             (d, m) = divmod(vmin, base)
             vmin = (d + 1) * base
-            locs = list(range(vmin, vmax + 1, base))
+            # error: No overload variant of "range" matches argument types "float",
+            # "float", "int"
+            locs = list(range(vmin, vmax + 1, base))  # type: ignore[call-overload]
         return locs
 
     def autoscale(self):
@@ -1038,6 +1050,8 @@ class TimeSeries_DateFormatter(Formatter):
         Whether the formatter works in dynamic mode or not.
     """
 
+    axis: Axis
+
     def __init__(
         self,
         freq: BaseOffset,
@@ -1084,7 +1098,7 @@ class TimeSeries_DateFormatter(Formatter):
             (vmin, vmax) = (vmax, vmin)
         self._set_default_format(vmin, vmax)
 
-    def __call__(self, x, pos: int = 0) -> str:
+    def __call__(self, x, pos: int | None = 0) -> str:
         if self.formatdict is None:
             return ""
         else:
@@ -1107,6 +1121,8 @@ class TimeSeries_TimedeltaFormatter(Formatter):
     Formats the ticks along an axis controlled by a :class:`TimedeltaIndex`.
     """
 
+    axis: Axis
+
     @staticmethod
     def format_timedelta_ticks(x, pos, n_decimals: int) -> str:
         """
@@ -1124,7 +1140,7 @@ class TimeSeries_TimedeltaFormatter(Formatter):
             s = f"{int(d):d} days {s}"
         return s
 
-    def __call__(self, x, pos: int = 0) -> str:
+    def __call__(self, x, pos: int | None = 0) -> str:
         (vmin, vmax) = tuple(self.axis.get_view_interval())
         n_decimals = min(int(np.ceil(np.log10(100 * 10**9 / abs(vmax - vmin)))), 9)
         return self.format_timedelta_ticks(x, pos, n_decimals)
diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py
index 76b68c6b03..6be8284d2a 100644
--- a/pandas/plotting/_matplotlib/core.py
+++ b/pandas/plotting/_matplotlib/core.py
@@ -529,9 +529,16 @@ class MPLPlot(ABC):
             # otherwise, create twin axes
             orig_ax, new_ax = ax, ax.twinx()
             # TODO: use Matplotlib public API when available
-            new_ax._get_lines = orig_ax._get_lines
-            new_ax._get_patches_for_fill = orig_ax._get_patches_for_fill
-            orig_ax.right_ax, new_ax.left_ax = new_ax, orig_ax
+            new_ax._get_lines = orig_ax._get_lines  # type: ignore[attr-defined]
+            # TODO #54485
+            new_ax._get_patches_for_fill = (  # type: ignore[attr-defined]
+                orig_ax._get_patches_for_fill  # type: ignore[attr-defined]
+            )
+            # TODO #54485
+            orig_ax.right_ax, new_ax.left_ax = (  # type: ignore[attr-defined]
+                new_ax,
+                orig_ax,
+            )
 
             if not self._has_plotted_object(orig_ax):  # no data on left y
                 orig_ax.get_yaxis().set_visible(False)
@@ -540,7 +547,7 @@ class MPLPlot(ABC):
                 new_ax.set_yscale("log")
             elif self.logy == "sym" or self.loglog == "sym":
                 new_ax.set_yscale("symlog")
-            return new_ax
+            return new_ax  # type: ignore[return-value]
 
     @final
     @cache_readonly
@@ -1206,12 +1213,15 @@ class MPLPlot(ABC):
 
     @final
     def _get_subplots(self, fig: Figure):
-        from matplotlib.axes import Subplot
+        if Version(mpl.__version__) < Version("3.8"):
+            from matplotlib.axes import Subplot as Klass
+        else:
+            from matplotlib.axes import Axes as Klass
 
         return [
             ax
             for ax in fig.get_axes()
-            if (isinstance(ax, Subplot) and ax.get_subplotspec() is not None)
+            if (isinstance(ax, Klass) and ax.get_subplotspec() is not None)
         ]
 
     @final
@@ -1255,8 +1265,10 @@ class PlanePlot(MPLPlot, ABC):
         x, y = self.x, self.y
         xlabel = self.xlabel if self.xlabel is not None else pprint_thing(x)
         ylabel = self.ylabel if self.ylabel is not None else pprint_thing(y)
-        ax.set_xlabel(xlabel)
-        ax.set_ylabel(ylabel)
+        # error: Argument 1 to "set_xlabel" of "_AxesBase" has incompatible
+        # type "Hashable"; expected "str"
+        ax.set_xlabel(xlabel)  # type: ignore[arg-type]
+        ax.set_ylabel(ylabel)  # type: ignore[arg-type]
 
     @final
     def _plot_colorbar(self, ax: Axes, *, fig: Figure, **kwds):
@@ -1393,7 +1405,7 @@ class ScatterPlot(PlanePlot):
         else:
             cmap = None
 
-        if color_by_categorical:
+        if color_by_categorical and cmap is not None:
             from matplotlib import colors
 
             n_cats = len(self.data[c].cat.categories)
@@ -1584,13 +1596,13 @@ class LinePlot(MPLPlot):
             decorate_axes(ax.left_ax, freq, kwds)
         if hasattr(ax, "right_ax"):
             decorate_axes(ax.right_ax, freq, kwds)
-        ax._plot_data.append((data, self._kind, kwds))
+        # TODO #54485
+        ax._plot_data.append((data, self._kind, kwds))  # type: ignore[attr-defined]
 
         lines = self._plot(ax, data.index, np.asarray(data.values), style=style, **kwds)
         # set date formatter, locators and rescale limits
-        # error: Argument 3 to "format_dateaxis" has incompatible type "Index";
-        # expected "DatetimeIndex | PeriodIndex"
-        format_dateaxis(ax, ax.freq, data.index)  # type: ignore[arg-type]
+        # TODO #54485
+        format_dateaxis(ax, ax.freq, data.index)  # type: ignore[arg-type, attr-defined]
         return lines
 
     @final
@@ -1606,11 +1618,15 @@ class LinePlot(MPLPlot):
         if stacking_id is None:
             return
         if not hasattr(ax, "_stacker_pos_prior"):
-            ax._stacker_pos_prior = {}
+            # TODO #54485
+            ax._stacker_pos_prior = {}  # type: ignore[attr-defined]
         if not hasattr(ax, "_stacker_neg_prior"):
-            ax._stacker_neg_prior = {}
-        ax._stacker_pos_prior[stacking_id] = np.zeros(n)
-        ax._stacker_neg_prior[stacking_id] = np.zeros(n)
+            # TODO #54485
+            ax._stacker_neg_prior = {}  # type: ignore[attr-defined]
+        # TODO #54485
+        ax._stacker_pos_prior[stacking_id] = np.zeros(n)  # type: ignore[attr-defined]
+        # TODO #54485
+        ax._stacker_neg_prior[stacking_id] = np.zeros(n)  # type: ignore[attr-defined]
 
     @final
     @classmethod
@@ -1624,9 +1640,17 @@ class LinePlot(MPLPlot):
             cls._initialize_stacker(ax, stacking_id, len(values))
 
         if (values >= 0).all():
-            return ax._stacker_pos_prior[stacking_id] + values
+            # TODO #54485
+            return (
+                ax._stacker_pos_prior[stacking_id]  # type: ignore[attr-defined]
+                + values
+            )
         elif (values <= 0).all():
-            return ax._stacker_neg_prior[stacking_id] + values
+            # TODO #54485
+            return (
+                ax._stacker_neg_prior[stacking_id]  # type: ignore[attr-defined]
+                + values
+            )
 
         raise ValueError(
             "When stacked is True, each column must be either "
@@ -1640,9 +1664,11 @@ class LinePlot(MPLPlot):
         if stacking_id is None:
             return
         if (values >= 0).all():
-            ax._stacker_pos_prior[stacking_id] += values
+            # TODO #54485
+            ax._stacker_pos_prior[stacking_id] += values  # type: ignore[attr-defined]
         elif (values <= 0).all():
-            ax._stacker_neg_prior[stacking_id] += values
+            # TODO #54485
+            ax._stacker_neg_prior[stacking_id] += values  # type: ignore[attr-defined]
 
     def _post_plot_logic(self, ax: Axes, data) -> None:
         from matplotlib.ticker import FixedLocator
@@ -1658,7 +1684,9 @@ class LinePlot(MPLPlot):
         if self._need_to_set_index:
             xticks = ax.get_xticks()
             xticklabels = [get_label(x) for x in xticks]
-            ax.xaxis.set_major_locator(FixedLocator(xticks))
+            # error: Argument 1 to "FixedLocator" has incompatible type "ndarray[Any,
+            # Any]"; expected "Sequence[float]"
+            ax.xaxis.set_major_locator(FixedLocator(xticks))  # type: ignore[arg-type]
             ax.set_xticklabels(xticklabels)
 
         # If the index is an irregular time series, then by default
@@ -1737,9 +1765,11 @@ class AreaPlot(LinePlot):
         if stacking_id is None:
             start = np.zeros(len(y))
         elif (y >= 0).all():
-            start = ax._stacker_pos_prior[stacking_id]
+            # TODO #54485
+            start = ax._stacker_pos_prior[stacking_id]  # type: ignore[attr-defined]
         elif (y <= 0).all():
-            start = ax._stacker_neg_prior[stacking_id]
+            # TODO #54485
+            start = ax._stacker_neg_prior[stacking_id]  # type: ignore[attr-defined]
         else:
             start = np.zeros(len(y))
 
@@ -2005,7 +2035,9 @@ class BarhPlot(BarPlot):
         ax.set_yticklabels(ticklabels)
         if name is not None and self.use_index:
             ax.set_ylabel(name)
-        ax.set_xlabel(self.xlabel)
+        # error: Argument 1 to "set_xlabel" of "_AxesBase" has incompatible type
+        # "Hashable | None"; expected "str"
+        ax.set_xlabel(self.xlabel)  # type: ignore[arg-type]
 
 
 class PiePlot(MPLPlot):
diff --git a/pandas/plotting/_matplotlib/hist.py b/pandas/plotting/_matplotlib/hist.py
index f5b415f12f..de4fd91541 100644
--- a/pandas/plotting/_matplotlib/hist.py
+++ b/pandas/plotting/_matplotlib/hist.py
@@ -199,11 +199,21 @@ class HistPlot(LinePlot):
 
     def _post_plot_logic(self, ax: Axes, data) -> None:
         if self.orientation == "horizontal":
-            ax.set_xlabel("Frequency" if self.xlabel is None else self.xlabel)
-            ax.set_ylabel(self.ylabel)
+            # error: Argument 1 to "set_xlabel" of "_AxesBase" has incompatible
+            # type "Hashable"; expected "str"
+            ax.set_xlabel(
+                "Frequency"
+                if self.xlabel is None
+                else self.xlabel  # type: ignore[arg-type]
+            )
+            ax.set_ylabel(self.ylabel)  # type: ignore[arg-type]
         else:
-            ax.set_xlabel(self.xlabel)
-            ax.set_ylabel("Frequency" if self.ylabel is None else self.ylabel)
+            ax.set_xlabel(self.xlabel)  # type: ignore[arg-type]
+            ax.set_ylabel(
+                "Frequency"
+                if self.ylabel is None
+                else self.ylabel  # type: ignore[arg-type]
+            )
 
     @property
     def orientation(self) -> PlottingOrientation:
@@ -447,8 +457,14 @@ def hist_series(
         ax.grid(grid)
         axes = np.array([ax])
 
+        # error: Argument 1 to "set_ticks_props" has incompatible type "ndarray[Any,
+        # dtype[Any]]"; expected "Axes | Sequence[Axes]"
         set_ticks_props(
-            axes, xlabelsize=xlabelsize, xrot=xrot, ylabelsize=ylabelsize, yrot=yrot
+            axes,  # type: ignore[arg-type]
+            xlabelsize=xlabelsize,
+            xrot=xrot,
+            ylabelsize=ylabelsize,
+            yrot=yrot,
         )
 
     else:
diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py
index a5f34e9434..bf4e4be3bf 100644
--- a/pandas/plotting/_matplotlib/style.py
+++ b/pandas/plotting/_matplotlib/style.py
@@ -269,7 +269,9 @@ def _is_single_string_color(color: Color) -> bool:
     """
     conv = matplotlib.colors.ColorConverter()
     try:
-        conv.to_rgba(color)
+        # error: Argument 1 to "to_rgba" of "ColorConverter" has incompatible type
+        # "str | Sequence[float]"; expected "tuple[float, float, float] | ..."
+        conv.to_rgba(color)  # type: ignore[arg-type]
     except ValueError:
         return False
     else:
diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py
index 5471305b03..f0b68e5dde 100644
--- a/pandas/plotting/_matplotlib/timeseries.py
+++ b/pandas/plotting/_matplotlib/timeseries.py
@@ -126,7 +126,7 @@ def _upsample_others(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]) -> None
         labels.extend(rlabels)
 
     if legend is not None and kwargs.get("legend", True) and len(lines) > 0:
-        title = legend.get_title().get_text()
+        title: str | None = legend.get_title().get_text()
         if title == "None":
             title = None
         ax.legend(lines, labels, loc="best", title=title)
@@ -136,7 +136,8 @@ def _replot_ax(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]):
     data = getattr(ax, "_plot_data", None)
 
     # clear current axes and data
-    ax._plot_data = []
+    # TODO #54485
+    ax._plot_data = []  # type: ignore[attr-defined]
     ax.clear()
 
     decorate_axes(ax, freq, kwargs)
@@ -148,7 +149,8 @@ def _replot_ax(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]):
             series = series.copy()
             idx = series.index.asfreq(freq, how="S")
             series.index = idx
-            ax._plot_data.append((series, plotf, kwds))
+            # TODO #54485
+            ax._plot_data.append((series, plotf, kwds))  # type: ignore[attr-defined]
 
             # for tsplot
             if isinstance(plotf, str):
@@ -165,17 +167,23 @@ def _replot_ax(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]):
 def decorate_axes(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]) -> None:
     """Initialize axes for time-series plotting"""
     if not hasattr(ax, "_plot_data"):
-        ax._plot_data = []
+        # TODO #54485
+        ax._plot_data = []  # type: ignore[attr-defined]
 
-    ax.freq = freq
+    # TODO #54485
+    ax.freq = freq  # type: ignore[attr-defined]
     xaxis = ax.get_xaxis()
-    xaxis.freq = freq
+    # TODO #54485
+    xaxis.freq = freq  # type: ignore[attr-defined]
     if not hasattr(ax, "legendlabels"):
-        ax.legendlabels = [kwargs.get("label", None)]
+        # TODO #54485
+        ax.legendlabels = [kwargs.get("label", None)]  # type: ignore[attr-defined]
     else:
         ax.legendlabels.append(kwargs.get("label", None))
-    ax.view_interval = None
-    ax.date_axis_info = None
+    # TODO #54485
+    ax.view_interval = None  # type: ignore[attr-defined]
+    # TODO #54485
+    ax.date_axis_info = None  # type: ignore[attr-defined]
 
 
 def _get_ax_freq(ax: Axes):
diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py
index 8c0e401f99..898b5b25e7 100644
--- a/pandas/plotting/_matplotlib/tools.py
+++ b/pandas/plotting/_matplotlib/tools.py
@@ -52,10 +52,12 @@ def maybe_adjust_figure(fig: Figure, *args, **kwargs) -> None:
 def format_date_labels(ax: Axes, rot) -> None:
     # mini version of autofmt_xdate
     for label in ax.get_xticklabels():
-        label.set_ha("right")
+        label.set_horizontalalignment("right")
         label.set_rotation(rot)
     fig = ax.get_figure()
-    maybe_adjust_figure(fig, bottom=0.2)
+    if fig is not None:
+        # should always be a Figure but can technically be None
+        maybe_adjust_figure(fig, bottom=0.2)
 
 
 def table(
@@ -76,8 +78,14 @@ def table(
 
     cellText = data.values
 
+    # error: Argument "cellText" to "table" has incompatible type "ndarray[Any,
+    # Any]"; expected "Sequence[Sequence[str]] | None"
     return matplotlib.table.table(
-        ax, cellText=cellText, rowLabels=rowLabels, colLabels=colLabels, **kwargs
+        ax,
+        cellText=cellText,  # type: ignore[arg-type]
+        rowLabels=rowLabels,
+        colLabels=colLabels,
+        **kwargs,
     )
 
 
@@ -369,12 +377,12 @@ def _has_externally_shared_axis(ax1: Axes, compare_axis: str) -> bool:
             "_has_externally_shared_axis() needs 'x' or 'y' as a second parameter"
         )
 
-    axes = axes.get_siblings(ax1)
+    axes_siblings = axes.get_siblings(ax1)
 
     # Retain ax1 and any of its siblings which aren't in the same position as it
     ax1_points = ax1.get_position().get_points()
 
-    for ax2 in axes:
+    for ax2 in axes_siblings:
         if not np.array_equal(ax1_points, ax2.get_position().get_points()):
             return True
 
diff --git a/pyright_reportGeneralTypeIssues.json b/pyright_reportGeneralTypeIssues.json
index c059b9c589..e155b34053 100644
--- a/pyright_reportGeneralTypeIssues.json
+++ b/pyright_reportGeneralTypeIssues.json
@@ -99,6 +99,9 @@
         "pandas/io/sql.py",
         "pandas/io/stata.py",
         "pandas/plotting/_matplotlib/boxplot.py",
+        "pandas/plotting/_matplotlib/core.py",
+        "pandas/plotting/_matplotlib/timeseries.py",
+        "pandas/plotting/_matplotlib/tools.py",
         "pandas/tseries/frequencies.py",
         "pandas/tseries/holiday.py",
       ],
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 490e170299..faf915f4b9 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -24,7 +24,7 @@ gcsfs>=2022.11.0
 ipython
 jinja2>=3.1.2
 lxml>=4.9.2
-matplotlib>=3.6.3, <3.8
+matplotlib>=3.6.3
 numba>=0.56.4
 numexpr>=2.8.4
 openpyxl>=3.1.0


Test patch:
diff --git a/pandas/tests/io/test_sql.py b/pandas/tests/io/test_sql.py
index b274866b7c..9ac20774b8 100644
--- a/pandas/tests/io/test_sql.py
+++ b/pandas/tests/io/test_sql.py
@@ -3179,7 +3179,9 @@ def dtype_backend_data() -> DataFrame:
 
 @pytest.fixture
 def dtype_backend_expected():
-    def func(storage, dtype_backend, conn_name):
+    def func(storage, dtype_backend, conn_name) -> DataFrame:
+        string_array: StringArray | ArrowStringArray
+        string_array_na: StringArray | ArrowStringArray
         if storage == "python":
             string_array = StringArray(np.array(["a", "b", "c"], dtype=np.object_))
             string_array_na = StringArray(np.array(["a", "b", pd.NA], dtype=np.object_))
diff --git a/pandas/tests/plotting/common.py b/pandas/tests/plotting/common.py
index fd5a66049b..6912016069 100644
--- a/pandas/tests/plotting/common.py
+++ b/pandas/tests/plotting/common.py
@@ -328,7 +328,7 @@ def _check_axes_shape(axes, axes_num=None, layout=None, figsize=None):
     )
 
 
-def _flatten_visible(axes):
+def _flatten_visible(axes: Axes | Sequence[Axes]) -> Sequence[Axes]:
     """
     Flatten axes, and filter only visible
 
@@ -339,8 +339,8 @@ def _flatten_visible(axes):
     """
     from pandas.plotting._matplotlib.tools import flatten_axes
 
-    axes = flatten_axes(axes)
-    axes = [ax for ax in axes if ax.get_visible()]
+    axes_ndarray = flatten_axes(axes)
+    axes = [ax for ax in axes_ndarray if ax.get_visible()]
     return axes
 
 
diff --git a/pandas/tests/tslibs/test_array_to_datetime.py b/pandas/tests/tslibs/test_array_to_datetime.py
index 3ab214196a..dbf8101366 100644
--- a/pandas/tests/tslibs/test_array_to_datetime.py
+++ b/pandas/tests/tslibs/test_array_to_datetime.py
@@ -23,6 +23,66 @@ import pandas._testing as tm
 creso_infer = NpyDatetimeUnit.NPY_FR_GENERIC.value
 
 
+class TestArrayToDatetimeResolutionInference:
+    # TODO: tests that include tzs, ints
+
+    def test_infer_homogeoneous_datetimes(self):
+        dt = datetime(2023, 10, 27, 18, 3, 5, 678000)
+        arr = np.array([dt, dt, dt], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array([dt, dt, dt], dtype="M8[us]")
+        tm.assert_numpy_array_equal(result, expected)
+
+    def test_infer_homogeoneous_date_objects(self):
+        dt = datetime(2023, 10, 27, 18, 3, 5, 678000)
+        dt2 = dt.date()
+        arr = np.array([None, dt2, dt2, dt2], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array([np.datetime64("NaT"), dt2, dt2, dt2], dtype="M8[s]")
+        tm.assert_numpy_array_equal(result, expected)
+
+    def test_infer_homogeoneous_dt64(self):
+        dt = datetime(2023, 10, 27, 18, 3, 5, 678000)
+        dt64 = np.datetime64(dt, "ms")
+        arr = np.array([None, dt64, dt64, dt64], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array([np.datetime64("NaT"), dt64, dt64, dt64], dtype="M8[ms]")
+        tm.assert_numpy_array_equal(result, expected)
+
+    def test_infer_homogeoneous_timestamps(self):
+        dt = datetime(2023, 10, 27, 18, 3, 5, 678000)
+        ts = Timestamp(dt).as_unit("ns")
+        arr = np.array([None, ts, ts, ts], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array([np.datetime64("NaT")] + [ts.asm8] * 3, dtype="M8[ns]")
+        tm.assert_numpy_array_equal(result, expected)
+
+    def test_infer_homogeoneous_datetimes_strings(self):
+        item = "2023-10-27 18:03:05.678000"
+        arr = np.array([None, item, item, item], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array([np.datetime64("NaT"), item, item, item], dtype="M8[us]")
+        tm.assert_numpy_array_equal(result, expected)
+
+    def test_infer_heterogeneous(self):
+        dtstr = "2023-10-27 18:03:05.678000"
+
+        arr = np.array([dtstr, dtstr[:-3], dtstr[:-7], None], dtype=object)
+        result, tz = tslib.array_to_datetime(arr, creso=creso_infer)
+        assert tz is None
+        expected = np.array(arr, dtype="M8[us]")
+        tm.assert_numpy_array_equal(result, expected)
+
+        result, tz = tslib.array_to_datetime(arr[::-1], creso=creso_infer)
+        assert tz is None
+        tm.assert_numpy_array_equal(result, expected[::-1])
+
+
 class TestArrayToDatetimeWithTZResolutionInference:
     def test_array_to_datetime_with_tz_resolution(self):
         tz = tzoffset("custom", 3600)