demo + utils venv
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
# TODO: Use the fact that axis can have units to simplify the process
|
||||
|
||||
import functools
|
||||
|
||||
from matplotlib import pylab
|
||||
import numpy as np
|
||||
|
||||
from pandas._libs.tslibs.frequencies import (
|
||||
FreqGroup, get_base_alias, get_freq, is_subperiod, is_superperiod)
|
||||
from pandas._libs.tslibs.period import Period
|
||||
import pandas.compat as compat
|
||||
|
||||
from pandas.core.dtypes.generic import (
|
||||
ABCDatetimeIndex, ABCPeriodIndex, ABCTimedeltaIndex)
|
||||
|
||||
from pandas.io.formats.printing import pprint_thing
|
||||
from pandas.plotting._converter import (
|
||||
TimeSeries_DateFormatter, TimeSeries_DateLocator,
|
||||
TimeSeries_TimedeltaFormatter)
|
||||
import pandas.tseries.frequencies as frequencies
|
||||
from pandas.tseries.offsets import DateOffset
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Plotting functions and monkey patches
|
||||
|
||||
|
||||
def tsplot(series, plotf, ax=None, **kwargs):
|
||||
import warnings
|
||||
"""
|
||||
Plots a Series on the given Matplotlib axes or the current axes
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axes : Axes
|
||||
series : Series
|
||||
|
||||
Notes
|
||||
_____
|
||||
Supports same kwargs as Axes.plot
|
||||
|
||||
|
||||
.. deprecated:: 0.23.0
|
||||
Use Series.plot() instead
|
||||
"""
|
||||
warnings.warn("'tsplot' is deprecated and will be removed in a "
|
||||
"future version. Please use Series.plot() instead.",
|
||||
FutureWarning, stacklevel=2)
|
||||
|
||||
# Used inferred freq is possible, need a test case for inferred
|
||||
if ax is None:
|
||||
import matplotlib.pyplot as plt
|
||||
ax = plt.gca()
|
||||
|
||||
freq, series = _maybe_resample(series, ax, kwargs)
|
||||
|
||||
# Set ax with freq info
|
||||
_decorate_axes(ax, freq, kwargs)
|
||||
ax._plot_data.append((series, plotf, kwargs))
|
||||
lines = plotf(ax, series.index._mpl_repr(), series.values, **kwargs)
|
||||
|
||||
# set date formatter, locators and rescale limits
|
||||
format_dateaxis(ax, ax.freq, series.index)
|
||||
return lines
|
||||
|
||||
|
||||
def _maybe_resample(series, ax, kwargs):
|
||||
# resample against axes freq if necessary
|
||||
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()
|
||||
series.index = series.index.asfreq(ax_freq, how='s')
|
||||
freq = ax_freq
|
||||
elif _is_sup(freq, ax_freq): # one is weekly
|
||||
how = kwargs.pop('how', 'last')
|
||||
series = getattr(series.resample('D'), how)().dropna()
|
||||
series = getattr(series.resample(ax_freq), how)().dropna()
|
||||
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, f2):
|
||||
return ((f1.startswith('W') and is_subperiod('D', f2)) or
|
||||
(f2.startswith('W') and is_subperiod(f1, 'D')))
|
||||
|
||||
|
||||
def _is_sup(f1, f2):
|
||||
return ((f1.startswith('W') and is_superperiod('D', f2)) or
|
||||
(f2.startswith('W') and is_superperiod(f1, 'D')))
|
||||
|
||||
|
||||
def _upsample_others(ax, freq, kwargs):
|
||||
legend = ax.get_legend()
|
||||
lines, labels = _replot_ax(ax, freq, kwargs)
|
||||
_replot_ax(ax, freq, kwargs)
|
||||
|
||||
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, kwargs)
|
||||
lines.extend(rlines)
|
||||
labels.extend(rlabels)
|
||||
|
||||
if (legend is not None and kwargs.get('legend', True) and
|
||||
len(lines) > 0):
|
||||
title = legend.get_title().get_text()
|
||||
if title == 'None':
|
||||
title = None
|
||||
ax.legend(lines, labels, loc='best', title=title)
|
||||
|
||||
|
||||
def _replot_ax(ax, freq, kwargs):
|
||||
data = getattr(ax, '_plot_data', None)
|
||||
|
||||
# clear current axes and data
|
||||
ax._plot_data = []
|
||||
ax.clear()
|
||||
|
||||
_decorate_axes(ax, freq, kwargs)
|
||||
|
||||
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
|
||||
ax._plot_data.append((series, plotf, kwds))
|
||||
|
||||
# for tsplot
|
||||
if isinstance(plotf, compat.string_types):
|
||||
from pandas.plotting._core import _plot_klass
|
||||
plotf = _plot_klass[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, freq, kwargs):
|
||||
"""Initialize axes for time-series plotting"""
|
||||
if not hasattr(ax, '_plot_data'):
|
||||
ax._plot_data = []
|
||||
|
||||
ax.freq = freq
|
||||
xaxis = ax.get_xaxis()
|
||||
xaxis.freq = freq
|
||||
if not hasattr(ax, 'legendlabels'):
|
||||
ax.legendlabels = [kwargs.get('label', None)]
|
||||
else:
|
||||
ax.legendlabels.append(kwargs.get('label', None))
|
||||
ax.view_interval = None
|
||||
ax.date_axis_info = None
|
||||
|
||||
|
||||
def _get_ax_freq(ax):
|
||||
"""
|
||||
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_freq(ax, series):
|
||||
# get frequency from data
|
||||
freq = getattr(series.index, 'freq', None)
|
||||
if freq is None:
|
||||
freq = getattr(series.index, 'inferred_freq', None)
|
||||
|
||||
ax_freq = _get_ax_freq(ax)
|
||||
|
||||
# use axes freq if no data freq
|
||||
if freq is None:
|
||||
freq = ax_freq
|
||||
|
||||
# get the period frequency
|
||||
if isinstance(freq, DateOffset):
|
||||
freq = freq.rule_code
|
||||
else:
|
||||
freq = get_base_alias(freq)
|
||||
|
||||
freq = frequencies.get_period_alias(freq)
|
||||
return freq, ax_freq
|
||||
|
||||
|
||||
def _use_dynamic_x(ax, data):
|
||||
freq = _get_index_freq(data)
|
||||
ax_freq = _get_ax_freq(ax)
|
||||
|
||||
if freq is None: # convert irregular if axes has freq info
|
||||
freq = ax_freq
|
||||
else: # do not use tsplot if irregular was plotted first
|
||||
if (ax_freq is None) and (len(ax.get_lines()) > 0):
|
||||
return False
|
||||
|
||||
if freq is None:
|
||||
return False
|
||||
|
||||
if isinstance(freq, DateOffset):
|
||||
freq = freq.rule_code
|
||||
else:
|
||||
freq = get_base_alias(freq)
|
||||
freq = frequencies.get_period_alias(freq)
|
||||
|
||||
if freq is None:
|
||||
return False
|
||||
|
||||
# hack this for 0.10.1, creating more technical debt...sigh
|
||||
if isinstance(data.index, ABCDatetimeIndex):
|
||||
base = get_freq(freq)
|
||||
x = data.index
|
||||
if (base <= FreqGroup.FR_DAY):
|
||||
return x[:1].is_normalized
|
||||
return Period(x[0], freq).to_timestamp(tz=x.tz) == x[0]
|
||||
return True
|
||||
|
||||
|
||||
def _get_index_freq(data):
|
||||
freq = getattr(data.index, 'freq', None)
|
||||
if freq is None:
|
||||
freq = getattr(data.index, 'inferred_freq', None)
|
||||
if freq == 'B':
|
||||
weekdays = np.unique(data.index.dayofweek)
|
||||
if (5 in weekdays) or (6 in weekdays):
|
||||
freq = None
|
||||
return freq
|
||||
|
||||
|
||||
def _maybe_convert_index(ax, data):
|
||||
# tsplot converts automatically, but don't want to convert index
|
||||
# over and over for DataFrames
|
||||
if isinstance(data.index, ABCDatetimeIndex):
|
||||
freq = getattr(data.index, 'freq', None)
|
||||
|
||||
if freq is None:
|
||||
freq = getattr(data.index, 'inferred_freq', None)
|
||||
if isinstance(freq, DateOffset):
|
||||
freq = freq.rule_code
|
||||
|
||||
if freq is None:
|
||||
freq = _get_ax_freq(ax)
|
||||
|
||||
if freq is None:
|
||||
raise ValueError('Could not get frequency alias for plotting')
|
||||
|
||||
freq = get_base_alias(freq)
|
||||
freq = frequencies.get_period_alias(freq)
|
||||
|
||||
data = data.to_period(freq=freq)
|
||||
return data
|
||||
|
||||
|
||||
# Patch methods for subplot. Only format_dateaxis is currently used.
|
||||
# Do we need the rest for convenience?
|
||||
|
||||
def format_timedelta_ticks(x, pos, n_decimals):
|
||||
"""
|
||||
Convert seconds to 'D days HH:MM:SS.F'
|
||||
"""
|
||||
s, ns = divmod(x, 1e9)
|
||||
m, s = divmod(s, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
decimals = int(ns * 10**(n_decimals - 9))
|
||||
s = r'{:02d}:{:02d}:{:02d}'.format(int(h), int(m), int(s))
|
||||
if n_decimals > 0:
|
||||
s += '.{{:0{:0d}d}}'.format(n_decimals).format(decimals)
|
||||
if d != 0:
|
||||
s = '{:d} days '.format(int(d)) + s
|
||||
return s
|
||||
|
||||
|
||||
def _format_coord(freq, t, y):
|
||||
return "t = {0} y = {1:8f}".format(Period(ordinal=int(t), freq=freq), y)
|
||||
|
||||
|
||||
def format_dateaxis(subplot, freq, index):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
# 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()
|
||||
Reference in New Issue
Block a user