diff --git a/pandas/_typing.py b/pandas/_typing.py index 85ed2a3b636de..a1bb119c32ba6 100644 --- a/pandas/_typing.py +++ b/pandas/_typing.py @@ -326,3 +326,6 @@ def closed(self) -> bool: # quantile interpolation QuantileInterpolation = Literal["linear", "lower", "higher", "midpoint", "nearest"] + +# plotting +PlottingOrientation = Literal["horizontal", "vertical"] diff --git a/pandas/core/generic.py b/pandas/core/generic.py index b3fb9635b4801..f32b347de32c3 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -5727,7 +5727,7 @@ def _check_inplace_setting(self, value) -> bool_t: return True @final - def _get_numeric_data(self): + def _get_numeric_data(self: NDFrameT) -> NDFrameT: return self._constructor(self._mgr.get_numeric_data()).__finalize__(self) @final @@ -10954,7 +10954,8 @@ def mad( data = self._get_numeric_data() if axis == 0: - demeaned = data - data.mean(axis=0) + # error: Unsupported operand types for - ("NDFrame" and "float") + demeaned = data - data.mean(axis=0) # type: ignore[operator] else: demeaned = data.sub(data.mean(axis=1), axis=0) return np.abs(demeaned).mean(axis=axis, skipna=skipna) diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py index 929ddb52aea6d..bc39d1f619f49 100644 --- a/pandas/plotting/_core.py +++ b/pandas/plotting/_core.py @@ -1879,11 +1879,11 @@ def _get_plot_backend(backend: str | None = None): ----- Modifies `_backends` with imported backend as a side effect. """ - backend = backend or get_option("plotting.backend") + backend_str: str = backend or get_option("plotting.backend") - if backend in _backends: - return _backends[backend] + if backend_str in _backends: + return _backends[backend_str] - module = _load_backend(backend) - _backends[backend] = module + module = _load_backend(backend_str) + _backends[backend_str] = module return module diff --git a/pandas/plotting/_matplotlib/boxplot.py b/pandas/plotting/_matplotlib/boxplot.py index f82889a304dd2..a49b035b1aaf1 100644 --- a/pandas/plotting/_matplotlib/boxplot.py +++ b/pandas/plotting/_matplotlib/boxplot.py @@ -2,6 +2,7 @@ from typing import ( TYPE_CHECKING, + Literal, NamedTuple, ) import warnings @@ -34,7 +35,10 @@ class BoxPlot(LinePlot): - _kind = "box" + @property + def _kind(self) -> Literal["box"]: + return "box" + _layout_type = "horizontal" _valid_return_types = (None, "axes", "dict", "both") diff --git a/pandas/plotting/_matplotlib/converter.py b/pandas/plotting/_matplotlib/converter.py index 19f726009f646..31f014d25d67d 100644 --- a/pandas/plotting/_matplotlib/converter.py +++ b/pandas/plotting/_matplotlib/converter.py @@ -574,6 +574,8 @@ def _daily_finder(vmin, vmax, freq: BaseOffset): Period(ordinal=int(vmin), freq=freq), Period(ordinal=int(vmax), freq=freq), ) + assert isinstance(vmin, Period) + assert isinstance(vmax, Period) span = vmax.ordinal - vmin.ordinal + 1 dates_ = period_range(start=vmin, end=vmax, freq=freq) # Initialize the output @@ -1073,7 +1075,9 @@ def __call__(self, x, pos=0) -> str: fmt = self.formatdict.pop(x, "") if isinstance(fmt, np.bytes_): fmt = fmt.decode("utf-8") - return Period(ordinal=int(x), freq=self.freq).strftime(fmt) + period = Period(ordinal=int(x), freq=self.freq) + assert isinstance(period, Period) + return period.strftime(fmt) class TimeSeries_TimedeltaFormatter(Formatter): diff --git a/pandas/plotting/_matplotlib/core.py b/pandas/plotting/_matplotlib/core.py index 5fceb14b9d1cc..3641cd7213fec 100644 --- a/pandas/plotting/_matplotlib/core.py +++ b/pandas/plotting/_matplotlib/core.py @@ -1,9 +1,14 @@ from __future__ import annotations +from abc import ( + ABC, + abstractmethod, +) from typing import ( TYPE_CHECKING, Hashable, Iterable, + Literal, Sequence, ) import warnings @@ -11,7 +16,10 @@ from matplotlib.artist import Artist import numpy as np -from pandas._typing import IndexLabel +from pandas._typing import ( + IndexLabel, + PlottingOrientation, +) from pandas.errors import AbstractMethodError from pandas.util._decorators import cache_readonly @@ -78,7 +86,7 @@ def _color_in_style(style: str) -> bool: return not set(BASE_COLORS).isdisjoint(style) -class MPLPlot: +class MPLPlot(ABC): """ Base class for assembling a pandas plot using matplotlib @@ -89,13 +97,17 @@ class MPLPlot: """ @property - def _kind(self): + @abstractmethod + def _kind(self) -> str: """Specify kind str. Must be overridden in child class""" raise NotImplementedError _layout_type = "vertical" _default_rot = 0 - orientation: str | None = None + + @property + def orientation(self) -> str | None: + return None axes: np.ndarray # of Axes objects @@ -843,7 +855,9 @@ def _get_xticks(self, convert_period: bool = False): @classmethod @register_pandas_matplotlib_converters - def _plot(cls, ax: Axes, x, y, style=None, is_errorbar: bool = False, **kwds): + def _plot( + cls, ax: Axes, x, y: np.ndarray, style=None, is_errorbar: bool = False, **kwds + ): mask = isna(y) if mask.any(): y = np.ma.array(y) @@ -1101,7 +1115,7 @@ def _get_axes_layout(self) -> tuple[int, int]: return (len(y_set), len(x_set)) -class PlanePlot(MPLPlot): +class PlanePlot(MPLPlot, ABC): """ Abstract class for plotting on plane, currently scatter and hexbin. """ @@ -1159,7 +1173,9 @@ def _plot_colorbar(self, ax: Axes, **kwds): class ScatterPlot(PlanePlot): - _kind = "scatter" + @property + def _kind(self) -> Literal["scatter"]: + return "scatter" def __init__(self, data, x, y, s=None, c=None, **kwargs) -> None: if s is None: @@ -1247,7 +1263,9 @@ def _make_plot(self): class HexBinPlot(PlanePlot): - _kind = "hexbin" + @property + def _kind(self) -> Literal["hexbin"]: + return "hexbin" def __init__(self, data, x, y, C=None, **kwargs) -> None: super().__init__(data, x, y, **kwargs) @@ -1277,9 +1295,15 @@ def _make_legend(self): class LinePlot(MPLPlot): - _kind = "line" _default_rot = 0 - orientation = "vertical" + + @property + def orientation(self) -> PlottingOrientation: + return "vertical" + + @property + def _kind(self) -> Literal["line", "area", "hist", "kde", "box"]: + return "line" def __init__(self, data, **kwargs) -> None: from pandas.plotting import plot_params @@ -1363,8 +1387,7 @@ def _plot( # type: ignore[override] cls._update_stacker(ax, stacking_id, y) return lines - @classmethod - def _ts_plot(cls, ax: Axes, x, data, style=None, **kwds): + def _ts_plot(self, ax: Axes, x, data, style=None, **kwds): # accept x to be consistent with normal plot func, # x is not passed to tsplot as it uses data.index as x coordinate # column_num must be in kwds for stacking purpose @@ -1377,9 +1400,9 @@ def _ts_plot(cls, ax: Axes, x, data, style=None, **kwds): decorate_axes(ax.left_ax, freq, kwds) if hasattr(ax, "right_ax"): decorate_axes(ax.right_ax, freq, kwds) - ax._plot_data.append((data, cls._kind, kwds)) + ax._plot_data.append((data, self._kind, kwds)) - lines = cls._plot(ax, data.index, data.values, style=style, **kwds) + lines = self._plot(ax, data.index, data.values, style=style, **kwds) # set date formatter, locators and rescale limits format_dateaxis(ax, ax.freq, data.index) return lines @@ -1471,7 +1494,9 @@ def get_label(i): class AreaPlot(LinePlot): - _kind = "area" + @property + def _kind(self) -> Literal["area"]: + return "area" def __init__(self, data, **kwargs) -> None: kwargs.setdefault("stacked", True) @@ -1544,9 +1569,15 @@ def _post_plot_logic(self, ax: Axes, data): class BarPlot(MPLPlot): - _kind = "bar" + @property + def _kind(self) -> Literal["bar", "barh"]: + return "bar" + _default_rot = 90 - orientation = "vertical" + + @property + def orientation(self) -> PlottingOrientation: + return "vertical" def __init__(self, data, **kwargs) -> None: # we have to treat a series differently than a @@ -1698,9 +1729,15 @@ def _decorate_ticks(self, ax: Axes, name, ticklabels, start_edge, end_edge): class BarhPlot(BarPlot): - _kind = "barh" + @property + def _kind(self) -> Literal["barh"]: + return "barh" + _default_rot = 0 - orientation = "horizontal" + + @property + def orientation(self) -> Literal["horizontal"]: + return "horizontal" @property def _start_base(self): @@ -1727,7 +1764,10 @@ def _decorate_ticks(self, ax: Axes, name, ticklabels, start_edge, end_edge): class PiePlot(MPLPlot): - _kind = "pie" + @property + def _kind(self) -> Literal["pie"]: + return "pie" + _layout_type = "horizontal" def __init__(self, data, kind=None, **kwargs) -> None: diff --git a/pandas/plotting/_matplotlib/hist.py b/pandas/plotting/_matplotlib/hist.py index 3be168fe159cf..77496cf049f3d 100644 --- a/pandas/plotting/_matplotlib/hist.py +++ b/pandas/plotting/_matplotlib/hist.py @@ -1,9 +1,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import ( + TYPE_CHECKING, + Literal, +) import numpy as np +from pandas._typing import PlottingOrientation + from pandas.core.dtypes.common import ( is_integer, is_list_like, @@ -40,7 +45,9 @@ class HistPlot(LinePlot): - _kind = "hist" + @property + def _kind(self) -> Literal["hist", "kde"]: + return "hist" def __init__(self, data, bins=10, bottom=0, **kwargs) -> None: self.bins = bins # use mpl default @@ -64,8 +71,8 @@ def _args_adjust(self): def _calculate_bins(self, data: DataFrame) -> np.ndarray: """Calculate bins given data""" - values = data._convert(datetime=True)._get_numeric_data() - values = np.ravel(values) + nd_values = data._convert(datetime=True)._get_numeric_data() + values = np.ravel(nd_values) values = values[~isna(values)] hist, bins = np.histogram( @@ -159,7 +166,7 @@ def _post_plot_logic(self, ax: Axes, data): ax.set_ylabel("Frequency") @property - def orientation(self): + def orientation(self) -> PlottingOrientation: if self.kwds.get("orientation", None) == "horizontal": return "horizontal" else: @@ -167,8 +174,13 @@ def orientation(self): class KdePlot(HistPlot): - _kind = "kde" - orientation = "vertical" + @property + def _kind(self) -> Literal["kde"]: + return "kde" + + @property + def orientation(self) -> Literal["vertical"]: + return "vertical" def __init__(self, data, bw_method=None, ind=None, **kwargs) -> None: MPLPlot.__init__(self, data, **kwargs) diff --git a/pandas/plotting/_matplotlib/style.py b/pandas/plotting/_matplotlib/style.py index 597c0dafa8cab..9e459b82fec97 100644 --- a/pandas/plotting/_matplotlib/style.py +++ b/pandas/plotting/_matplotlib/style.py @@ -143,8 +143,8 @@ def _get_colors_from_colormap( num_colors: int, ) -> list[Color]: """Get colors from colormap.""" - colormap = _get_cmap_instance(colormap) - return [colormap(num) for num in np.linspace(0, 1, num=num_colors)] + cmap = _get_cmap_instance(colormap) + return [cmap(num) for num in np.linspace(0, 1, num=num_colors)] def _get_cmap_instance(colormap: str | Colormap) -> Colormap: diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index 303266ae410de..ca6cccb0f98eb 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import timedelta import functools from typing import ( TYPE_CHECKING, @@ -185,11 +186,10 @@ def _get_ax_freq(ax: Axes): return ax_freq -def _get_period_alias(freq) -> str | None: +def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None: freqstr = to_offset(freq).rule_code - freq = get_period_alias(freqstr) - return freq + return get_period_alias(freqstr) def _get_freq(ax: Axes, series: Series): @@ -235,7 +235,9 @@ def use_dynamic_x(ax: Axes, data: DataFrame | Series) -> bool: x = data.index if base <= FreqGroup.FR_DAY.value: return x[:1].is_normalized - return Period(x[0], freq_str).to_timestamp().tz_localize(x.tz) == x[0] + period = Period(x[0], freq_str) + assert isinstance(period, Period) + return period.to_timestamp().tz_localize(x.tz) == x[0] return True diff --git a/pandas/plotting/_matplotlib/tools.py b/pandas/plotting/_matplotlib/tools.py index bfbf77e85afd3..94357e5002ffd 100644 --- a/pandas/plotting/_matplotlib/tools.py +++ b/pandas/plotting/_matplotlib/tools.py @@ -83,7 +83,11 @@ def table( return table -def _get_layout(nplots: int, layout=None, layout_type: str = "box") -> tuple[int, int]: +def _get_layout( + nplots: int, + layout: tuple[int, int] | None = None, + layout_type: str = "box", +) -> tuple[int, int]: if layout is not None: if not isinstance(layout, (tuple, list)) or len(layout) != 2: raise ValueError("Layout must be a tuple of (rows, columns)") diff --git a/pyright_reportGeneralTypeIssues.json b/pyright_reportGeneralTypeIssues.json index b26376461b901..bcbaa5c90f845 100644 --- a/pyright_reportGeneralTypeIssues.json +++ b/pyright_reportGeneralTypeIssues.json @@ -112,14 +112,6 @@ "pandas/io/sql.py", "pandas/io/stata.py", "pandas/io/xml.py", - "pandas/plotting/_core.py", - "pandas/plotting/_matplotlib/converter.py", - "pandas/plotting/_matplotlib/core.py", - "pandas/plotting/_matplotlib/hist.py", - "pandas/plotting/_matplotlib/misc.py", - "pandas/plotting/_matplotlib/style.py", - "pandas/plotting/_matplotlib/timeseries.py", - "pandas/plotting/_matplotlib/tools.py", "pandas/tseries/frequencies.py", ], }