added iplotter

This commit is contained in:
Walker Waylon Scott 2017-08-01 18:43:37 -05:00
parent b1a74737b9
commit cf9c0abb7d
8 changed files with 909 additions and 0 deletions

8
src/iplotter/__init__.py Normal file
View file

@ -0,0 +1,8 @@
from .base_plotter import IPlotter
# from .export import VirtualBrowser
from .c3_plotter import C3Plotter
from .plotly_plotter import PlotlyPlotter
from .chartjs_plotter import ChartJSPlotter
from .chartist_plotter import ChartistPlotter
from .google_plotter import GCPlotter
__version__ = '0.4.3'

View file

@ -0,0 +1,55 @@
from abc import ABCMeta, abstractmethod
import re
import time
# from selenium import webdriver
import os
class IPlotter(object):
"""Abstract IPlotter"""
__metaclass__ = ABCMeta
iframe = '<iframe srcdoc="{source}" src="" width="{w}" height="{h}" frameborder="0" sandbox="allow-scripts"></iframe>'
invalid_name_pattern = re.compile(r'[^a-zA-Z0-9_\-\. ]+')
def __init__(self):
super(IPlotter, self).__init__()
@classmethod
def is_valid_name(cls, name):
'''
check whether plot div id or filenname are valid
'''
if (cls.invalid_name_pattern.search(name)):
return False
else:
return True
@abstractmethod
def render(self):
'''
render the data in HTML template
'''
pass
@abstractmethod
def plot(self):
'''
output an iframe containing the plot in the notebook without saving
'''
pass
@abstractmethod
def save(self):
'''
save the rendered html to a file in the same directory as the notebook
'''
pass
@abstractmethod
def plot_and_save(self):
'''
save the rendered html to a file and return an IFrame to display the
plot in the notebook
'''
pass

386
src/iplotter/c3_plotter.py Normal file
View file

@ -0,0 +1,386 @@
from jinja2 import Template
from IPython.display import IFrame, HTML
import os
import json
from .base_plotter import IPlotter
import math
class C3Plotter(IPlotter):
"""
Class for creating c3.js charts in ipython notebook
"""
head = '''
<!-- Load c3.css -->
<link href='https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.css' rel='stylesheet' type='text/css'/>
<!-- Load d3.js and c3.js -->
<script src='http://d3js.org/d3.v3.min.js' charset='utf-8'></script>
<script src='http://cdnjs.cloudflare.com/ajax/libs/c3/0.4.10/c3.min.js'></script>
<style>{{custom_css}}</style>
'''
template = '''
<h1>{{title}}</h1>
<div id={{div_id}} style='width: 100%; height: 100%'></div>
<script>
var {{div_id}} = document.getElementById('{{div_id}}');
var data = {{data}};
data['axis']['y']['tick']['format'] = d3.format('{{y_axis_tick_format}}')
data['axis']['y2']['tick']['format'] = d3.format('{{secondary_y_axis_tick_format}}')
data['bindto']='#{{div_id}}'
var {{div_id}} = c3.generate(data);
</script>
'''
def __init__(self):
super(C3Plotter, self).__init__()
def render(self,
data,
div_id="chart",
custom_css='',
title="",
head="",
y_axis_tick_format='',
secondary_y_axis_tick_format=''
,
**kwargs):
'''
render the data in HTML template
'''
try:
data = self.pandas_data(data, **kwargs)
except AttributeError:
pass
if not self.is_valid_name(div_id):
raise ValueError(
"Name {} is invalid. Only letters, numbers, '_', and '-' are permitted ".format(
div_id))
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
custom_css=custom_css,
title=title,
y_axis_tick_format=y_axis_tick_format,
secondary_y_axis_tick_format=secondary_y_axis_tick_format,
data=json.dumps(
data, indent=4).replace("'", "\\'").replace('"', "'"))
def plot_and_save(self,
data,
w=800,
h=430,
filename='chart',
subplots=False,
subplot_groups=False,
title=False,
overwrite=True):
'''
save the rendered html to a file and returns an IFrame to display the plot in the notebook
'''
self.save(data, filename, overwrite,)
return IFrame(filename + '.html', w, h)
def plot(self,
data,
w=800,
h=430,
div_id='chart',
subplots=False,
subplot_groups=False,
title=False,
**kwargs):
'''
output an iframe containing the plot in the notebook without saving
'''
if subplots:
if title:
if len(title) > 0:
title = title + '<br>'
body = ''
if not subplot_groups:
subplot_groups = {col: [col] for col in data.columns}
for group in subplot_groups:
body = body + (self.render(data=data[subplot_groups[group]],
div_id=str(div_id) + str(group),
head=self.head,
title=str(title) + str(group),
**kwargs
)
)
title=''
else:
body = self.render(
data=data,
div_id=div_id,
head=self.head,
**kwargs)
return HTML(self.iframe.format(source=body, w=w, h=h*len(subplot_groups)))
def update():
pass
def save(self, data, filename='chart', overwrite=True):
'''
save the rendered html to a file in the same directory as the notebook
'''
try:
data = self.pandas_data(data, **kwargs)
except AttributeError:
pass
html = self.render(data=data, div_id=filename, head=self.head)
if overwrite:
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
if not os.path.exists(filename.replace(" ", "_") + '.html'):
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
raise IOError('File Already Exists!')
def pandas_data(self,
df,
colors=False,
data_label_formats=False,
data_labels=False,
grid=False,
group=False,
height=300,
hue=False,
kind='line',
kinds=None,
legend=True,
mark_right=False,
point=False,
secondary_y=list(),
stacked=False,
subchart=False,
subplots=False,
tick_count=10,
value=False,
value_labels=False,
x_axis_tick_culling=False,
x_axis_type='auto',
x_tick_values=False,
xlabels=False,
xlim=False,
xregions=False,
xy_rotated=False,
ylabels=False,
ylim=False,
yregions=False,
zoom=False,
):
'''
create data dictionary from pandas DataFrame
TODO:
## Pandas Features
* proper docstring
* subplots
* layout
* height -> figsize
* use_index
* legend
* xlim (axis.x.min, axis.x.max or axis.x.extent)
* ylim
* colorbar
* table
* axis-rotation
## Seaborn-esque features
* hue - ability to provide long form data
## C3 Features
* interaction: {enabled: false}
* transition: {duration: 500}
* onrendered: function() {...}
* onmouseover/out
* data.empty.label.text
* data.selection.enabled
* data.selection.grouped
* data.selection.multiple
* data.selection.draggable
* axis.x.tick.fit
* axis.x.tick.values
* axis.x.tick.rotate
* axis.x.label
* axis.x.show
* legend.hide
* legend.position
* tooltip.show
* tooltip.grouped
* point.focus.expand.enabled
* subchart.size.height
* point.focus.expand.r
* point.select.r
* line.connectNull
param kind: str
* line
* spline
* step
* areas
* area-spline
* area-step
* bar
* scatter
* pie
* donut
* gauge
param x_axis_type: str
* timeseries
* category
* numeric
'''
# kinds = ['line', 'spline', 'step', 'area','area-spline', 'area-step',
# 'bar', 'scatter', 'pie', 'donut', 'gauge']
data = {
'size': {
'height': height,
},
"data": {
'x': 'x',
'axes': dict()
},
'subchart': {
'show': subchart
},
'point': {
'show': point
},
'grid': {
'x': {
'show': grid
},
'y': {
'show': grid
}
},
'axis': {
'rotated': xy_rotated,
'x': {'tick': {'count': tick_count,
'values': x_tick_values,
'culling': dict(),
},
},
'y': {'tick': {'format': ''}},
'y2': {'tick': {}},
},
'zoom': {}
}
if kind:
data['data']['type'] = kind
if kinds:
data['data']['types'] = kinds
if mark_right:
df = df.rename(
columns={col: col + '(right)' for col in secondary_y})
secondary_y = [y + '(right)' for y in secondary_y]
if hue and value:
df = df.groupby([df.index.name, hue])[value].sum().unstack()
df = df.copy()
df['x'] = df.index
df['x'] = df['x'].astype('str').values.tolist()
data['data']['columns'] = [[col] + df[col].values.tolist()
for col in df.columns]
# data['data']['columns'].extend([['x'] + df.index.astype('str').values.tolist()])
for col in df.columns:
if col in secondary_y:
data['data']['axes'][col] = 'y2'
else:
data['data']['axes'][col] = 'y'
if len(secondary_y) > 0:
data['axis']['y2']['show'] = True
if colors:
# repeat color palette if not long enough
colors = colors*math.ceil(len(df.columns)/len(colors))
color_data = {}
for col, color in zip(df.columns, colors):
color_data[col] = color
data['data']['colors'] = color_data
if x_axis_type == 'auto':
index_type = str(df.index.dtype)
if 'date' in index_type:
data['axis']['x']['type'] = 'timeseries'
data['axis']['x']['tick']['format'] = '%Y-%m-%d'
if 'object' in index_type or 'category' in index_type:
data['axis']['x']['type'] = 'category'
data['axis']['x']['tick']['culling'][
'max'] = x_axis_tick_culling
else:
if 'date' in x_axis_type or 'time' in x_axis_type:
data['axis']['x']['type'] = 'timeseries'
data['axis']['x']['tick']['format'] = '%Y-%m-%d'
if 'categor' in x_axis_type or 'str' in x_axis_type:
data['axis']['x']['type'] = 'category'
data['axis']['x']['tick']['culling'][
'max'] = x_axis_tick_culling
if xlim:
data['axis']['x']['min'] = xlim[0]
data['axis']['x']['max'] = xlim[1]
if ylim:
data['axis']['y']['min'] = ylim[0]
data['axis']['y']['max'] = ylim[1]
if stacked:
group = df.columns.values.tolist()
group.pop(-1)
group = [group]
if group:
data['data']['groups'] = group
if zoom:
data['zoom']['enabled'] = True
data['zoom']['rescale'] = True
if xregions:
data['regions'] = [{'axis': 'x', 'start': region[
0], 'end':region[1]} for region in xregions]
if yregions:
data['regions'] = [{'axis': 'y', 'start': region[
0], 'end':region[1]} for region in yregions]
if xlabels:
data['grid']['x']['lines'] = [
{'value': label[0], 'text': label[1]} for label in xlabels]
if ylabels:
data['grid']['y']['lines'] = [
{'value': label[0], 'text': label[1]} for label in ylabels]
if data_labels:
if data_labels == True:
data_labels = df.drop('x', axis=1).columns
if data_label_formats:
data['data']['labels'] = {}
for column in data_label_formats:
data['data']['labels'][column] = data_label_formats[column]
else:
data['data']['labels'] = True
return data

View file

@ -0,0 +1,98 @@
from jinja2 import Template
from IPython.display import IFrame, HTML
import os
import json
from .base_plotter import IPlotter
class ChartistPlotter(IPlotter):
"""
Class for creating chartist.js charts in ipython notebook
"""
head = '''
<!-- Load Chartist.js -->
<link rel='stylesheet' href='https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.css'>
<script src='https://cdn.jsdelivr.net/chartist.js/latest/chartist.min.js'></script>
'''
template = '''
<div id={{div_id}} class='ct-chart' style='width: 100%; height: 100%' ></div>
<script>
new Chartist.{{chart_type}}('#{{div_id}}', {{data}}, {{options}});
</script>
'''
def __init__(self):
super(ChartistPlotter, self).__init__()
def render(self, data, chart_type, options=None, div_id="chart", head=""):
'''
render the data in HTML template
'''
if not self.is_valid_name(div_id):
raise ValueError(
"Name {} is invalid. Only letters, numbers, '_', and '-' are permitted ".format(
div_id))
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=json.dumps(
data, indent=4).replace("'", "\\'").replace('"', "'"),
chart_type=chart_type,
options=json.dumps(
options, indent=4).replace("'", "\\'").replace('"', "'"))
def plot_and_save(self,
data,
chart_type,
options=None,
w=800,
h=420,
filename='chart',
overwrite=True):
'''
save the rendered html to a file and return an IFrame to display the plot in the notebook
'''
self.save(data, chart_type, options, filename, overwrite)
return IFrame(filename + '.html', w, h)
def plot(self, data, chart_type, options=None, w=800, h=420):
'''
output an iframe containing the plot in the notebook without saving
'''
return HTML(
self.iframe.format(
source=self.render(
data=data,
options=options,
chart_type=chart_type,
head=self.head),
w=w,
h=h))
def save(self,
data,
chart_type,
options=None,
filename='chart',
overwrite=True):
'''
save the rendered html to a file in the same directory as the notebook
'''
html = self.render(
data=data,
chart_type=chart_type,
options=options,
div_id=filename,
head=self.head)
if overwrite:
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
if not os.path.exists(filename.replace(" ", "_") + '.html'):
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
raise IOError('File Already Exists!')

View file

@ -0,0 +1,115 @@
from jinja2 import Template
from IPython.display import IFrame, HTML
import os
import json
from .base_plotter import IPlotter
class ChartJSPlotter(IPlotter):
"""
Class for creating charts.js charts in ipython notebook
"""
head = '''
<!-- Load Charts.js -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.bundle.min.js'></script>
'''
template = '''
<canvas id='{{div_id}}'></canvas>
<script>
var ctx = document.getElementById('{{div_id}}').getContext('2d');
ctx.canvas.width = {{w}} - (.1 * {{w}});
ctx.canvas.height = {{h}} - (.15 * {{h}});
var myNewChart = new Chart(ctx,{ type: '{{chart_type}}', data: {{data}}, options: {{options}} });
</script>
'''
def __init__(self):
super(ChartJSPlotter, self).__init__()
def render(self,
data,
chart_type,
options=None,
div_id="chart",
head="",
w=800,
h=420):
'''
render the data in HTML template
'''
if not self.is_valid_name(div_id):
raise ValueError(
"Name {} is invalid. Only letters, numbers, '_', and '-' are permitted ".format(
div_id))
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=json.dumps(
data, indent=4).replace("'", "\\'").replace('"', "'"),
chart_type=chart_type,
options=json.dumps(
options, indent=4).replace("'", "\\'").replace('"', "'"),
w=w,
h=h)
def plot_and_save(self,
data,
chart_type,
options=None,
w=800,
h=420,
filename='chart',
overwrite=True):
'''
save the rendered html to a file and return an IFrame to display the plot in the notebook
'''
self.save(data, chart_type, options, filename, w, h, overwrite)
return IFrame(filename + '.html', w, h)
def plot(self, data, chart_type, options=None, w=800, h=420):
'''
output an iframe containing the plot in the notebook without saving
'''
return HTML(
self.iframe.format(
source=self.render(
data=data,
chart_type=chart_type,
options=options,
head=self.head,
w=w,
h=h),
w=w,
h=h))
def save(self,
data,
chart_type,
options=None,
filename='chart',
w=800,
h=420,
overwrite=True):
'''
save the rendered html to a file in the same directory as the notebook
'''
html = self.render(
data=data,
chart_type=chart_type,
options=options,
div_id=filename,
head=self.head,
w=w,
h=h)
if overwrite:
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
if not os.path.exists(filename.replace(" ", "_") + '.html'):
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
raise IOError('File Already Exists!')

35
src/iplotter/export.py Normal file
View file

@ -0,0 +1,35 @@
import time
from selenium import webdriver
import os
class VirtualBrowser(object):
"""Helper class for converting html charts to png"""
def __init__(self, driver=webdriver.Chrome):
super(VirtualBrowser, self).__init__()
self.driver = driver()
def __enter__(self):
return self
def save_as_png(self, filename, width=300, height=250, render_time=1):
'''
open saved html file in an virtual browser and save a screen shot to PNG format
'''
self.driver.set_window_size(width, height)
self.driver.get('file://{path}/{filename}'.format(
path=os.getcwd(), filename=filename + ".html"))
time.sleep(render_time)
self.driver.save_screenshot(filename + ".png")
def __exit__(self, type, value, traceback):
self.driver.quit()
return True
def quit(self):
'''
shutdown virtual browser when finished
'''
self.driver.quit()
return True

View file

@ -0,0 +1,124 @@
from jinja2 import Template
from IPython.display import IFrame, HTML
import os
import json
from .base_plotter import IPlotter
class GCPlotter(IPlotter):
"""
Class for creating Google Charts in ipython notebook
"""
head = '''
<!-- Load Google Charts -->
<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
'''
template = '''
<div id={{div_id}} style='width: 100%; height: 100%' ></div>
<script type='text/javascript'>
google.charts.load('current', {'packages':['{{ chart_package}}']});
google.charts.setOnLoadCallback(drawChart);
function drawChart() {
var data = google.visualization.arrayToDataTable({{data}}
);
var chart = new google.visualization.{{chart_type}}(document.getElementById('{{div_id}}'));
chart.draw(data, {{options}});
}
</script>
'''
def __init__(self):
super(GCPlotter, self).__init__()
def render(self,
data,
chart_type,
chart_package='corechart',
options=None,
div_id="chart",
head=""):
'''
render the data in HTML template
'''
if not self.is_valid_name(div_id):
raise ValueError(
"Name {} is invalid. Only letters, numbers, '_', and '-' are permitted ".format(
div_id))
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=json.dumps(
data, indent=4).replace("'", "\\'").replace('"', "'"),
chart_type=chart_type,
chart_package=chart_package,
options=json.dumps(
options, indent=4).replace("'", "\\'").replace('"', "'"))
def plot_and_save(self,
data,
chart_type,
chart_package='corechart',
options=None,
w=800,
h=420,
filename='chart',
overwrite=True):
'''
save the rendered html to a file and return an IFrame to display the plot in the notebook
'''
self.save(data, chart_type, chart_package, options, filename,
overwrite)
return IFrame(filename + '.html', w, h)
def plot(self,
data,
chart_type,
chart_package='corechart',
options=None,
w=800,
h=420):
'''
output an iframe containing the plot in the notebook without saving
'''
return HTML(
self.iframe.format(
source=self.render(
data=data,
options=options,
chart_type=chart_type,
chart_package=chart_package,
head=self.head),
w=w,
h=h))
def save(self,
data,
chart_type,
chart_package='corechart',
options=None,
filename='chart',
overwrite=True):
'''
save the rendered html to a file in the same directory as the notebook
'''
html = self.render(
data=data,
chart_type=chart_type,
chart_package=chart_package,
options=options,
div_id=filename,
head=self.head)
if overwrite:
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
if not os.path.exists(filename.replace(" ", "_") + '.html'):
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
raise IOError('File Already Exists!')

View file

@ -0,0 +1,88 @@
from jinja2 import Template
from IPython.display import IFrame, HTML
import os
import json
from .base_plotter import IPlotter
class PlotlyPlotter(IPlotter):
"""
Class for creating plotly.js charts in ipython notebook
"""
head = '''
<!-- Load d3.js and plotly.js -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js'></script>
<script src='https://code.jquery.com/jquery-2.1.4.min.js'></script>
<script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
'''
template = '''
<div id={{div_id}} style='width: 100%; height: 100%' ></div>
<script>
var {{div_id}} = document.getElementById('{{div_id}}');
Plotly.plot({{div_id}}, {{data}}, {{layout}});
</script>
'''
def __init__(self):
super(PlotlyPlotter, self).__init__()
def render(self, data, layout=None, div_id="chart", head=""):
'''
render the data in HTML template
'''
if not self.is_valid_name(div_id):
raise ValueError(
"Name {} is invalid. Only letters, numbers, '_', and '-' are permitted ".format(
div_id))
return Template(head + self.template).render(
div_id=div_id.replace(" ", "_"),
data=json.dumps(
data, indent=4).replace("'", "\\'").replace('"', "'"),
layout=json.dumps(
layout, indent=4).replace("'", "\\'").replace('"', "'"))
def plot_and_save(self,
data,
layout=None,
w=800,
h=420,
filename='chart',
overwrite=True):
'''
save the rendered html to a file and return an IFrame to display the plot in the notebook
'''
self.save(data, layout, filename, overwrite)
return IFrame(filename + '.html', w, h)
def plot(self, data, layout=None, w=800, h=420):
'''
output an iframe containing the plot in the notebook without saving
'''
return HTML(
self.iframe.format(
source=self.render(
data=data,
layout=layout,
head=self.head, ),
w=w,
h=h))
def save(self, data, layout=None, filename='chart', overwrite=True):
'''
save the rendered html to a file in the same directory as the notebook
'''
html = self.render(
data=data, layout=layout, div_id=filename, head=self.head)
if overwrite:
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
if not os.path.exists(filename.replace(" ", "_") + '.html'):
with open(filename.replace(" ", "_") + '.html', 'w') as f:
f.write(html)
else:
raise IOError('File Already Exists!')