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() | |