spam-classifier
/
venv
/lib
/python3.11
/site-packages
/pandas
/plotting
/_matplotlib
/timeseries.py
| # TODO: Use the fact that axis can have units to simplify the process | |
| from __future__ import annotations | |
| import functools | |
| from typing import ( | |
| TYPE_CHECKING, | |
| Any, | |
| cast, | |
| ) | |
| import warnings | |
| import numpy as np | |
| from pandas._libs.tslibs import ( | |
| BaseOffset, | |
| Period, | |
| to_offset, | |
| ) | |
| from pandas._libs.tslibs.dtypes import ( | |
| OFFSET_TO_PERIOD_FREQSTR, | |
| FreqGroup, | |
| ) | |
| from pandas.core.dtypes.generic import ( | |
| ABCDatetimeIndex, | |
| ABCPeriodIndex, | |
| ABCTimedeltaIndex, | |
| ) | |
| from pandas.io.formats.printing import pprint_thing | |
| from pandas.plotting._matplotlib.converter import ( | |
| TimeSeries_DateFormatter, | |
| TimeSeries_DateLocator, | |
| TimeSeries_TimedeltaFormatter, | |
| ) | |
| from pandas.tseries.frequencies import ( | |
| get_period_alias, | |
| is_subperiod, | |
| is_superperiod, | |
| ) | |
| if TYPE_CHECKING: | |
| from datetime import timedelta | |
| from matplotlib.axes import Axes | |
| from pandas._typing import NDFrameT | |
| from pandas import ( | |
| DataFrame, | |
| DatetimeIndex, | |
| Index, | |
| PeriodIndex, | |
| Series, | |
| ) | |
| # --------------------------------------------------------------------- | |
| # Plotting functions and monkey patches | |
| def maybe_resample(series: Series, ax: Axes, kwargs: dict[str, Any]): | |
| # resample against axes freq if necessary | |
| if "how" in kwargs: | |
| raise ValueError( | |
| "'how' is not a valid keyword for plotting functions. If plotting " | |
| "multiple objects on shared axes, resample manually first." | |
| ) | |
| freq, ax_freq = _get_freq(ax, series) | |
| if freq is None: # pragma: no cover | |
| raise ValueError("Cannot use dynamic axis without frequency info") | |
| # Convert DatetimeIndex to PeriodIndex | |
| if isinstance(series.index, ABCDatetimeIndex): | |
| series = series.to_period(freq=freq) | |
| if ax_freq is not None and freq != ax_freq: | |
| if is_superperiod(freq, ax_freq): # upsample input | |
| series = series.copy() | |
| # error: "Index" has no attribute "asfreq" | |
| series.index = series.index.asfreq( # type: ignore[attr-defined] | |
| ax_freq, how="s" | |
| ) | |
| freq = ax_freq | |
| elif _is_sup(freq, ax_freq): # one is weekly | |
| # Resampling with PeriodDtype is deprecated, so we convert to | |
| # DatetimeIndex, resample, then convert back. | |
| ser_ts = series.to_timestamp() | |
| ser_d = ser_ts.resample("D").last().dropna() | |
| ser_freq = ser_d.resample(ax_freq).last().dropna() | |
| series = ser_freq.to_period(ax_freq) | |
| freq = ax_freq | |
| elif is_subperiod(freq, ax_freq) or _is_sub(freq, ax_freq): | |
| _upsample_others(ax, freq, kwargs) | |
| else: # pragma: no cover | |
| raise ValueError("Incompatible frequency conversion") | |
| return freq, series | |
| def _is_sub(f1: str, f2: str) -> bool: | |
| return (f1.startswith("W") and is_subperiod("D", f2)) or ( | |
| f2.startswith("W") and is_subperiod(f1, "D") | |
| ) | |
| def _is_sup(f1: str, f2: str) -> bool: | |
| return (f1.startswith("W") and is_superperiod("D", f2)) or ( | |
| f2.startswith("W") and is_superperiod(f1, "D") | |
| ) | |
| def _upsample_others(ax: Axes, freq: BaseOffset, kwargs: dict[str, Any]) -> None: | |
| legend = ax.get_legend() | |
| lines, labels = _replot_ax(ax, freq) | |
| _replot_ax(ax, freq) | |
| other_ax = None | |
| if hasattr(ax, "left_ax"): | |
| other_ax = ax.left_ax | |
| if hasattr(ax, "right_ax"): | |
| other_ax = ax.right_ax | |
| if other_ax is not None: | |
| rlines, rlabels = _replot_ax(other_ax, freq) | |
| lines.extend(rlines) | |
| labels.extend(rlabels) | |
| if legend is not None and kwargs.get("legend", True) and len(lines) > 0: | |
| title: str | None = legend.get_title().get_text() | |
| if title == "None": | |
| title = None | |
| ax.legend(lines, labels, loc="best", title=title) | |
| def _replot_ax(ax: Axes, freq: BaseOffset): | |
| data = getattr(ax, "_plot_data", None) | |
| # clear current axes and data | |
| # TODO #54485 | |
| ax._plot_data = [] # type: ignore[attr-defined] | |
| ax.clear() | |
| decorate_axes(ax, freq) | |
| lines = [] | |
| labels = [] | |
| if data is not None: | |
| for series, plotf, kwds in data: | |
| series = series.copy() | |
| idx = series.index.asfreq(freq, how="S") | |
| series.index = idx | |
| # TODO #54485 | |
| ax._plot_data.append((series, plotf, kwds)) # type: ignore[attr-defined] | |
| # for tsplot | |
| if isinstance(plotf, str): | |
| from pandas.plotting._matplotlib import PLOT_CLASSES | |
| plotf = PLOT_CLASSES[plotf]._plot | |
| lines.append(plotf(ax, series.index._mpl_repr(), series.values, **kwds)[0]) | |
| labels.append(pprint_thing(series.name)) | |
| return lines, labels | |
| def decorate_axes(ax: Axes, freq: BaseOffset) -> None: | |
| """Initialize axes for time-series plotting""" | |
| if not hasattr(ax, "_plot_data"): | |
| # TODO #54485 | |
| ax._plot_data = [] # type: ignore[attr-defined] | |
| # TODO #54485 | |
| ax.freq = freq # type: ignore[attr-defined] | |
| xaxis = ax.get_xaxis() | |
| # TODO #54485 | |
| xaxis.freq = freq # type: ignore[attr-defined] | |
| def _get_ax_freq(ax: Axes): | |
| """ | |
| Get the freq attribute of the ax object if set. | |
| Also checks shared axes (eg when using secondary yaxis, sharex=True | |
| or twinx) | |
| """ | |
| ax_freq = getattr(ax, "freq", None) | |
| if ax_freq is None: | |
| # check for left/right ax in case of secondary yaxis | |
| if hasattr(ax, "left_ax"): | |
| ax_freq = getattr(ax.left_ax, "freq", None) | |
| elif hasattr(ax, "right_ax"): | |
| ax_freq = getattr(ax.right_ax, "freq", None) | |
| if ax_freq is None: | |
| # check if a shared ax (sharex/twinx) has already freq set | |
| shared_axes = ax.get_shared_x_axes().get_siblings(ax) | |
| if len(shared_axes) > 1: | |
| for shared_ax in shared_axes: | |
| ax_freq = getattr(shared_ax, "freq", None) | |
| if ax_freq is not None: | |
| break | |
| return ax_freq | |
| def _get_period_alias(freq: timedelta | BaseOffset | str) -> str | None: | |
| if isinstance(freq, BaseOffset): | |
| freqstr = freq.name | |
| else: | |
| freqstr = to_offset(freq, is_period=True).rule_code | |
| return get_period_alias(freqstr) | |
| def _get_freq(ax: Axes, series: Series): | |
| # get frequency from data | |
| freq = getattr(series.index, "freq", None) | |
| if freq is None: | |
| freq = getattr(series.index, "inferred_freq", None) | |
| freq = to_offset(freq, is_period=True) | |
| ax_freq = _get_ax_freq(ax) | |
| # use axes freq if no data freq | |
| if freq is None: | |
| freq = ax_freq | |
| # get the period frequency | |
| freq = _get_period_alias(freq) | |
| return freq, ax_freq | |
| def use_dynamic_x(ax: Axes, data: DataFrame | Series) -> bool: | |
| freq = _get_index_freq(data.index) | |
| ax_freq = _get_ax_freq(ax) | |
| if freq is None: # convert irregular if axes has freq info | |
| freq = ax_freq | |
| # do not use tsplot if irregular was plotted first | |
| elif (ax_freq is None) and (len(ax.get_lines()) > 0): | |
| return False | |
| if freq is None: | |
| return False | |
| freq_str = _get_period_alias(freq) | |
| if freq_str is None: | |
| return False | |
| # FIXME: hack this for 0.10.1, creating more technical debt...sigh | |
| if isinstance(data.index, ABCDatetimeIndex): | |
| # error: "BaseOffset" has no attribute "_period_dtype_code" | |
| freq_str = OFFSET_TO_PERIOD_FREQSTR.get(freq_str, freq_str) | |
| base = to_offset( | |
| freq_str, is_period=True | |
| )._period_dtype_code # type: ignore[attr-defined] | |
| x = data.index | |
| if base <= FreqGroup.FR_DAY.value: | |
| return x[:1].is_normalized | |
| period = Period(x[0], freq_str) | |
| assert isinstance(period, Period) | |
| return period.to_timestamp().tz_localize(x.tz) == x[0] | |
| return True | |
| def _get_index_freq(index: Index) -> BaseOffset | None: | |
| freq = getattr(index, "freq", None) | |
| if freq is None: | |
| freq = getattr(index, "inferred_freq", None) | |
| if freq == "B": | |
| # error: "Index" has no attribute "dayofweek" | |
| weekdays = np.unique(index.dayofweek) # type: ignore[attr-defined] | |
| if (5 in weekdays) or (6 in weekdays): | |
| freq = None | |
| freq = to_offset(freq) | |
| return freq | |
| def maybe_convert_index(ax: Axes, data: NDFrameT) -> NDFrameT: | |
| # tsplot converts automatically, but don't want to convert index | |
| # over and over for DataFrames | |
| if isinstance(data.index, (ABCDatetimeIndex, ABCPeriodIndex)): | |
| freq: str | BaseOffset | None = data.index.freq | |
| if freq is None: | |
| # We only get here for DatetimeIndex | |
| data.index = cast("DatetimeIndex", data.index) | |
| freq = data.index.inferred_freq | |
| freq = to_offset(freq) | |
| if freq is None: | |
| freq = _get_ax_freq(ax) | |
| if freq is None: | |
| raise ValueError("Could not get frequency alias for plotting") | |
| freq_str = _get_period_alias(freq) | |
| with warnings.catch_warnings(): | |
| # suppress Period[B] deprecation warning | |
| # TODO: need to find an alternative to this before the deprecation | |
| # is enforced! | |
| warnings.filterwarnings( | |
| "ignore", | |
| r"PeriodDtype\[B\] is deprecated", | |
| category=FutureWarning, | |
| ) | |
| if isinstance(data.index, ABCDatetimeIndex): | |
| data = data.tz_localize(None).to_period(freq=freq_str) | |
| elif isinstance(data.index, ABCPeriodIndex): | |
| data.index = data.index.asfreq(freq=freq_str) | |
| return data | |
| # Patch methods for subplot. | |
| def _format_coord(freq, t, y) -> str: | |
| time_period = Period(ordinal=int(t), freq=freq) | |
| return f"t = {time_period} y = {y:8f}" | |
| def format_dateaxis( | |
| subplot, freq: BaseOffset, index: DatetimeIndex | PeriodIndex | |
| ) -> None: | |
| """ | |
| Pretty-formats the date axis (x-axis). | |
| Major and minor ticks are automatically set for the frequency of the | |
| current underlying series. As the dynamic mode is activated by | |
| default, changing the limits of the x axis will intelligently change | |
| the positions of the ticks. | |
| """ | |
| from matplotlib import pylab | |
| # handle index specific formatting | |
| # Note: DatetimeIndex does not use this | |
| # interface. DatetimeIndex uses matplotlib.date directly | |
| if isinstance(index, ABCPeriodIndex): | |
| majlocator = TimeSeries_DateLocator( | |
| freq, dynamic_mode=True, minor_locator=False, plot_obj=subplot | |
| ) | |
| minlocator = TimeSeries_DateLocator( | |
| freq, dynamic_mode=True, minor_locator=True, plot_obj=subplot | |
| ) | |
| subplot.xaxis.set_major_locator(majlocator) | |
| subplot.xaxis.set_minor_locator(minlocator) | |
| majformatter = TimeSeries_DateFormatter( | |
| freq, dynamic_mode=True, minor_locator=False, plot_obj=subplot | |
| ) | |
| minformatter = TimeSeries_DateFormatter( | |
| freq, dynamic_mode=True, minor_locator=True, plot_obj=subplot | |
| ) | |
| subplot.xaxis.set_major_formatter(majformatter) | |
| subplot.xaxis.set_minor_formatter(minformatter) | |
| # x and y coord info | |
| subplot.format_coord = functools.partial(_format_coord, freq) | |
| elif isinstance(index, ABCTimedeltaIndex): | |
| subplot.xaxis.set_major_formatter(TimeSeries_TimedeltaFormatter()) | |
| else: | |
| raise TypeError("index type not supported") | |
| pylab.draw_if_interactive() | |