make sure you have the latest version of spyre
pip install --upgrade dataspyre
there have been recent changes to spyre, so if you installed more than a day ago, go ahead and upgrade
Adam Hajari
Data Scientist on the Next Big Sound team at Pandora
adam@nextbigsound.com
@adamhajari
Spyre is a web application framework for turning static data tables and plots into interactive web apps. Spyre was motivated by Shiny, a similar framework for R created by the developers of Rstudio.
GitHub: github.com/adamhajari/spyre
Live example of a spyre app:
Spyre depends on:
Assuming you don't have any issues with the above dependencies, you can install spyre via pip:
$ pip install dataspyre
Spyre's server module has a App class that every Spyre app will needs to inherit. Use the app's launch() method to deploy your app.
In [5]:
from spyre import server
class SimpleApp(server.App):
title = "Simple App"
app = SimpleApp()
app.launch() # launching from ipython notebook is not recommended
If you put the above code in a file called simple_app.py you can launch the app from the command line with
$ python simple_app.py
Make sure you uncomment the last line first.
There are two variables of the App class that need to be overridden to create the UI for a Spyre app: inputs and outputs (a third optional type called controls that we'll get to later). All three variables are lists of dictionaries which specify each component's properties. For instance, to create a text box input, overide the App's inputs variable:
In [2]:
from spyre import server
class SimpleApp(server.App):
inputs = [{ "type":"text",
"key":"words",
"label": "write here",
"value":"hello world"}]
app = SimpleApp()
app.launch()
Now let's add an output. We first need to list all our out outputs and their attributes in the outputs dictionary.
In [4]:
from spyre import server
class SimpleApp(server.App):
inputs = [{ "type":"text",
"key":"words",
"label": "write here",
"value":"hello world"}]
outputs = [{"type":"html",
"id":"some_html"}]
app = SimpleApp()
app.launch()
To generate the output, we can override a server.App method specific to that output type. In the case of html output, we overide the getHTML method. Each output method should return an object specific to that output type. In the case of html output, we just return a string.
In [10]:
from spyre import server
class SimpleApp(server.App):
title = "Simple App"
inputs = [{ "type":"text",
"key":"words",
"label": "write here",
"value":"hello world"}]
outputs = [{"type":"html",
"id":"some_html"}]
def getHTML(self, params):
words = params['words']
return "here are the words you wrote: <b>%s</b>"%words
app = SimpleApp()
app.launch()
Great. We've got inputs and outputs, but we're not quite finished. As it is, the content of our output is static. That's because the output doesn't know when it needs to get updated. We can fix this in one of two ways:
action_id
to our input that references the output that we want refreshed when the input value changes.Let's see what the first approach looks like.
In [6]:
from spyre import server
class SimpleApp(server.App):
title = "Simple App"
inputs = [{ "type":"text",
"key":"words",
"label": "write here",
"value":"hello world"}]
outputs = [{"type":"html",
"id":"some_html",
"control_id":"button1"}]
controls = [{"type":"button",
"label":"press to update",
"id":"button1"}]
def getHTML(self, params):
words = params['words']
return "here are the words you wrote: <b>%s</b>"%words
app = SimpleApp()
app.launch()
Our app now has a button with id "button1", and our output references our control's id, so that when we press the button we update the output with the most current input values.
Is a button a little overkill for this simple app? Yeah, probably. Let's get rid of it and have the output update just by changing the value in the text box. To do this we'll add an action_id
attribute to our input dictionary that references the output's id.
In [ ]:
from spyre import server
class SimpleApp(server.App):
title = "Simple App"
inputs = [{ "type":"text",
"key":"words",
"label": "write here",
"value":"look ma, no buttons",
"action_id":"some_html"}]
outputs = [{"type":"html",
"id":"some_html"}]
def getHTML(self, params):
words = params['words']
return "here are the words you wrote: <b>%s</b>"%words
app = SimpleApp()
app.launch()
Now the output gets updated with a change to the input.
Let's suppose you've written a function to grab historical stock price data from the web. Your function returns a pandas dataframe.
In [8]:
%pylab inline
import pandas as pd
import urllib2
import json
def getData(params):
ticker = params['ticker']
# make call to yahoo finance api to get historical stock data
api_url = 'https://chartapi.finance.yahoo.com/instrument/1.0/{}/chartdata;type=quote;range=3m/json'.format(ticker)
result = urllib2.urlopen(api_url).read()
data = json.loads(result.replace('finance_charts_json_callback( ','')[:-1]) # strip away the javascript and load json
company_name = data['meta']['Company-Name']
df = pd.DataFrame.from_records(data['series'])
df['Date'] = pd.to_datetime(df['Date'],format='%Y%m%d')
return df.drop('volume',axis=1)
params = {'ticker':'GOOG'}
df = getData(params)
df.head()
Out[8]:
Let's turn this into a spyre app. We'll use a dropdown menu input this time and start by displaying the data in a table. In the previous example we overrode the getHTML
method and had it return a string to generate HTML output. To get a table output we need to override the getData
method and have it return a pandas dataframe (conveniently, we've already done that!)
In [3]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"key": 'ticker',
"action_id": "table_id"}]
outputs = [{ "type" : "table",
"id" : "table_id"}]
def getData(self, params):
ticker = params['ticker']
# make call to yahoo finance api to get historical stock data
api_url = 'https://chartapi.finance.yahoo.com/instrument/1.0/{}/chartdata;type=quote;range=3m/json'.format(ticker)
result = urllib2.urlopen(api_url).read()
data = json.loads(result.replace('finance_charts_json_callback( ','')[:-1]) # strip away the javascript and load json
self.company_name = data['meta']['Company-Name']
df = pd.DataFrame.from_records(data['series'])
df['Date'] = pd.to_datetime(df['Date'],format='%Y%m%d')
return df.drop('volume',axis=1)
app = StockExample()
app.launch()
One really convenient feature of pandas is that you can plot directly from a dataframe using the plot method.
In [9]:
df.plot()
Out[9]:
Let's take advantage of this convenience and add a plot to our app. To generate a plot output, we need to add another dictionary to our list of outputs.
In [4]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"key": 'ticker'}]
controls = [{ "type" : "button",
"label":"get stock data",
"id" : "update_data"}]
outputs = [{ "type" : "plot",
"id" : "plot",
"control_id" : "update_data"},
{ "type" : "table",
"id" : "table_id",
"control_id" : "update_data"}]
def getData(self, params):
ticker = params['ticker']
# make call to yahoo finance api to get historical stock data
api_url = 'https://chartapi.finance.yahoo.com/instrument/1.0/{}/chartdata;type=quote;range=3m/json'.format(ticker)
result = urllib2.urlopen(api_url).read()
data = json.loads(result.replace('finance_charts_json_callback( ','')[:-1]) # strip away the javascript and load json
self.company_name = data['meta']['Company-Name']
df = pd.DataFrame.from_records(data['series'])
df['Date'] = pd.to_datetime(df['Date'],format='%Y%m%d')
return df.drop(['volume'],axis=1)
app = StockExample()
app.launch()
Notice that we didn't have to add a new method for our plot output. getData
is pulling double duty here serving the data for our table and our plot. If you wanted to alter the data or the plot object, you could do that by overriding the getPlot
method. Under the hood, if you don't specify a getPlot
method for your plot output, server.App's built-in getPlot
method will look for a getData
method, and just return the result of calling the plot() method on its dataframe.
In [3]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"key": 'ticker'}]
controls = [{ "type" : "button",
"label":"get stock data",
"id" : "update_data"}]
outputs = [{ "type" : "plot",
"id" : "plot",
"control_id" : "update_data"},
{ "type" : "table",
"id" : "table_id",
"control_id" : "update_data"}]
def getData(self, params):
ticker = params['ticker']
# make call to yahoo finance api to get historical stock data
api_url = 'https://chartapi.finance.yahoo.com/instrument/1.0/{}/chartdata;type=quote;range=3m/json'.format(ticker)
result = urllib2.urlopen(api_url).read()
data = json.loads(result.replace('finance_charts_json_callback( ','')[:-1]) # strip away the javascript and load json
self.company_name = data['meta']['Company-Name']
df = pd.DataFrame.from_records(data['series'])
df['Date'] = pd.to_datetime(df['Date'],format='%Y%m%d')
return df.drop(['volume'],axis=1)
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.set_index('Date').plot()
plt_obj.set_ylabel("Price")
plt_obj.set_title(self.company_name)
return plt_obj
app = StockExample()
app.launch()
Finally we'll put each of the outputs in separate tabs and add an action_id to the dropdown input that references the "update_data" control. Now, a change to the input state triggers the button to be "clicked". This makes the existence of a "button" supurfluous, so we'll change the control type to "hidden"
In [2]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"key": 'ticker',
"action_id": "update_data"}]
controls = [{ "type" : "hidden",
"id" : "update_data"}]
tabs = ["Plot", "Table"]
outputs = [{ "type" : "plot",
"id" : "plot",
"control_id" : "update_data",
"tab" : "Plot"},
{ "type" : "table",
"id" : "table_id",
"control_id" : "update_data",
"tab" : "Table" }]
def getData(self, params):
ticker = params['ticker']
# make call to yahoo finance api to get historical stock data
api_url = 'https://chartapi.finance.yahoo.com/instrument/1.0/{}/chartdata;type=quote;range=3m/json'.format(ticker)
result = urllib2.urlopen(api_url).read()
data = json.loads(result.replace('finance_charts_json_callback( ','')[:-1]) # strip away the javascript and load json
self.company_name = data['meta']['Company-Name']
df = pd.DataFrame.from_records(data['series'])
df['Date'] = pd.to_datetime(df['Date'],format='%Y%m%d')
return df.drop('volume',axis=1)
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.set_index('Date').plot()
plt_obj.set_ylabel("Price")
plt_obj.set_title(self.company_name)
fig = plt_obj.get_figure()
return fig
app = StockExample()
app.launch()