#
# Copyright (c) 2016-2017 - Adjacent Link LLC, Bridgewater, New Jersey
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Adjacent Link LLC nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import pandas as pd
import traceback
import time
class Line(object):
[docs] class Plot(object):
def __init__(self,*columns,**kwargs):
"""Creates a Line Plot.
A line plot is a strip chart of one or more time-series
measurments.
Args:
columns ([str]): One or more DataFrame column names
to plot.
Keyword Args:
title (str): Plot title. Default: ''.
ylim ((min,max)): Plot y-axis limit plot
limit. Default: (0,50000).
ylabel (str): Plot y-axis label. Default: ''.
markers ([str]): One or more DataFrame columns to use
as markers. A marker is a vertical line that
indicates an event occurred. A marker is drawn any
time the indicated DataFrame column value is
non-zero and non-nan. Default: [].
indicators ([str]): One or more DataFrame columns to
use as indicators. An indicator is a horizontal
bar that indicates a condition is met. An
indicator is continuously drawn for time stamps
where the indicated DataFrame column value is
non-zero and non-nan. Default: [].
legend (bool): Flag indicating whether a plot legend
should be displayed. Default: True.
Raises:
KeyError: If an invalid keyword is found.
"""
self.columns = columns
self.title = kwargs.pop('title','')
self.ylim = kwargs.pop('ylim',(0,50000))
self.yticks = kwargs.pop('yticks',None)
self.ylabel = kwargs.pop('ylabel','')
self.markers = kwargs.pop('markers',[])
self.indicators = kwargs.pop('indicators',[])
self.legend = kwargs.pop('legend',True)
if not hasattr(self.markers ,'__iter__'):
self.markers = [self.markers]
if not hasattr(self.indicators ,'__iter__'):
self.indicators = [self.indicators]
if kwargs:
raise KeyError("%s unknown key(s): %s" % (self.__class__.__name__,
", ".join(kwargs.keys())))
def __init__(self,model,**kwargs):
self._model = model
self._last_events = 0
self._title = kwargs.pop('title','')
self._interval = kwargs.pop('interval',1000)
self._model_data_kwargs = {}
self._last_update_time = None
for kwarg in kwargs.keys():
if kwarg.startswith('model_'):
self._model_data_kwargs[kwarg[6:]] = kwargs.pop(kwarg)
if kwargs:
raise KeyError("%s unknown key(s): %s" % (self.__class__.__name__,
", ".join(kwargs.keys())))
def show(self,*plots,**kwargs):
self.plot(*plots,**kwargs)
ani = self.animate()
plt.show()
def plot(self,*plots,**kwargs):
plots = [Line.Plot(*p.columns,**p.kwargs) for p in plots]
self._fig = kwargs.pop('figure',None)
tight = kwargs.pop('tight',False)
self._stale_timeout = kwargs.pop('stale_timeout',10)
if kwargs:
raise KeyError("%s unknown key(s): %s" % (self.__class__.__name__,
", ".join(kwargs.keys())))
if self._fig == None:
self._fig = plt.gcf()
self._fig.canvas.set_window_title(self._title)
self._fig.patch.set_facecolor('white')
plt.style.use('ggplot')
self._fig_face_color = self._fig.patch.get_facecolor()
# indicate no updates yet
self._fig.patch.set_facecolor('#ff8080')
self._axes = []
for i in range(1,len(plots)+1):
self._axes.append(self._fig.add_subplot(len(plots),1,i))
self._lines = []
self._columns = []
self._mark_columns = []
self._marks = []
self._indicator_columns = []
self._indicators = []
self._indicator_locations = []
for plot,ax in zip(plots,self._axes):
ax.set_title(plot.title)
ax.set_xlim(0, self._model.stream().cache() - 1)
ax.set_ylim(*plot.ylim)
if plot.yticks != None:
ax.yaxis.set_ticks(plot.yticks)
ax.set_ylabel(plot.ylabel)
self._mark_columns.append(plot.markers)
for column in plot.columns:
self._columns.append(column)
self._lines.append(ax.plot([],[],label=column,animated=True)[0])
# add markers to the legend using blank plots
for index,mcol in enumerate(plot.markers):
ax.plot([],[],
label=mcol,
ls='--',
color=plt.rcParams['axes.color_cycle'][index],
animated=True)
y1,y2 = plot.ylim
y_quarter = (y2 - y1) / 4.0
y_locs = np.linspace(y_quarter*2, y_quarter * 3,len(plot.indicators))
for index,column in enumerate(plot.indicators):
self._indicator_locations.append( y_locs[index])
self._indicator_columns.append(column)
self._indicators.append(ax.plot([],[],
label=column,
lw=4,
color=plt.rcParams['axes.color_cycle'][index],
animated=True)[0])
ax.set_xticks(range(0, self._model.stream().cache()))
ax.set_xticklabels([""] * self._model.stream().cache())
if plot.legend:
ax.legend()
if tight:
self._fig.tight_layout()
def animate(self):
def _init():
for line in self._lines:
line.set_data([], [])
return self._lines
def _animate(i):
data,events,_ = self._model.data(**self._model_data_kwargs)
if self._last_events != events:
if self._last_update_time == None:
self._fig.patch.set_facecolor(self._fig_face_color)
self._fig.canvas.draw()
self._last_update_time = time.time()
else:
if self._last_update_time != None and \
time.time() - self._last_update_time > self._stale_timeout:
self._fig.patch.set_facecolor('#ff8080')
self._fig.canvas.draw()
self._last_update_time = None
if not data.empty and events != self._last_events:
try:
while self._marks:
self._marks.pop(0).remove()
for line,column in zip(self._lines,self._columns):
y = data[column]
x = range(0,len(y))
line.set_data(x, y)
for ax,mcols in zip(self._axes,self._mark_columns):
for index,mcol in enumerate(mcols):
for x,y in enumerate(data[mcol]):
if pd.notnull(y) and y > 0:
self._marks.append(ax.axvline(x,
ls='--',
animated=True,
label=mcol,
color=plt.rcParams['axes.color_cycle'][index]))
for indicator,column,location in zip(self._indicators,self._indicator_columns,self._indicator_locations):
y = [location if (pd.notnull(val) and val > 0) else np.nan for val in data[column]]
x = range(0,len(y))
indicator.set_data(x, y)
except:
traceback.print_exc()
self._last_events = events
return self._lines + self._marks + self._indicators
return animation.FuncAnimation(self._fig,
_animate,
init_func=_init,
interval=self._interval,
blit=True)