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: adamhajari.com
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 [25]:
from spyre import server
class SimpleApp(server.App):
pass
app = SimpleApp()
# app.launch() # this won't launch from ipython notebook, uncomment to actually launch
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 and don't try to run this within ipython notebook. Things will break.
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 type, controls, is also often useful, but not absolutely neccessary). 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 [3]:
inputs = [{ "input_type":"text",
"variable_name":"freq",
"label": "frequency",
"value":5,
"action_id":"plot_sine_wave"}]
An input variable's value can be used by any of the app's outputs by referencing the variable_name can. The action_id is a reference to either an output_id of an output element or a control_id of a control element. The output referenced by the above input can be defined by overriding the App's outputs variable:
In [4]:
outputs = [{"output_type":"plot",
"output_id":"plot_sine_wave",
"on_page_load":True }]
Notice that the action_id from the text input matches the output_id above. As a result, an update to the text field will trigger an update to the output. Finally, we must override the method that generates the plot. Let's make a sine wave:
In [5]:
import numpy as np
from matplotlib import pyplot as plt
def getPlot(self,params):
f = int(params['freq'])
x = np.arange(1,6,0.01)
y = np.sin(f*x)
plt.plot(x,y)
return plt.gcf()
The getPlot method should return a pyplot figure.
If we put it all together and add a title we get
In [32]:
from spyre import server
import numpy as np
from matplotlib import pyplot as plt
class SimpleSineApp(server.App):
title = "Simple App"
inputs = [{ "input_type":"text",
"variable_name":"freq",
"label": "frequency",
"value":5,
"action_id":"plot_sine_wave"}]
outputs = [{"output_type":"plot",
"output_id":"plot_sine_wave",
"on_page_load":True }]
def getPlot(self,params):
f = float(params['freq'])
x = np.arange(1,6,0.01)
y = np.sin(f*x)
plt.plot(x,y)
return plt.gcf()
app = SimpleSineApp()
# app.launch()
Notice that the inputs and outputs variables not only specify what type of elements to include in an app's interface, but also how they're connected to eachother. The connection in this example is simple: a change in the input state updates the output.
Spyre uses the jinja2 templating library to turn the dictionary inputs and outputs into HTML and javascript. Here's a snippet from Spyre's HTML template that generates the text inputs:
This (and the rest of the templated page) gets rendered using the app's inputs and outputs attributes
If the above code looks foreign to you or HTML/javascript just isn't your thing, that's fine. No knowledge of anything other than python is required to create and launch a Spyre app.
Let's suppose you've written a function to grab historical stock price data from the web. Your function returns a pandas dataframe from which you can easily generate graphs of stock prices over time.
In [24]:
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
params = {'ticker':'AAPL'}
df = getData(params) # get data
plt_obj = df.set_index('Date').drop(['volume'],axis=1).plot()
plt_obj.set_ylabel("Price")
print df.set_index('Date').head()
To turn this into a Spyre app we just need to put the code that creates the plot into a getPlot() method and define the inputs and outputs. Let's use a dropdown menu input this time.
In [ ]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "input_type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"variable_name": 'ticker',
"action_id": "plot" }]
outputs = [{ "output_type" : "plot",
"output_id" : "plot",
"on_page_load" : True }]
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
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.set_index('Date').drop(['volume'],axis=1).plot()
plt_obj.set_ylabel("Price")
plt_obj.set_title(self.company_name)
fig = plt_obj.get_figure()
return fig
app = StockExample()
# app.launch(port=9093)
Let's also add a second output: a data table. To generate this table we just need to override the getData method (which we've conveniently already done). This method should return a pandas DataFrame.
Since our inputs can only have one action_id and there are two outputs that need to be updated, we'll also add an update button to our app. The button is a type of control. All controls have a control_id which can be referenced by our outputs, such that a control action (clicking the button in this case) results in an update to those outputs.
In [ ]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "input_type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"variable_name": 'ticker' }] # the input no longer has an action_id
# outputs will be updated by clicking the button control
controls = [{ "control_type" : "button",
"label" : "get historical stock prices",
"control_id" : "update_data"}]
outputs = [{ "output_type" : "plot",
"output_id" : "plot",
"control_id" : "update_data", # this is a reference to the button control
"on_page_load" : True },
# table outputs display the results returned from the getData()
# method (which should be a DataFrame)
{ "output_type" : "table",
"output_id" : "table_id",
"control_id" : "update_data", # multiple outputs can share the same control
"on_page_load" : True }]
def getData(self, params):
ticker = params['ticker']
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])
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
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.set_index('Date').drop(['volume'],axis=1).plot()
plt_obj.set_ylabel("Price")
plt_obj.set_title(self.company_name)
fig = plt_obj.get_figure()
return fig
app = StockExample()
# app.launch(port=9093)
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 [27]:
from spyre import server
import pandas as pd
import urllib2
import json
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{ "input_type":'dropdown',
"label": 'Company',
"options" : [ {"label": "Google", "value":"GOOG"},
{"label": "Yahoo", "value":"YHOO"},
{"label": "Apple", "value":"AAPL"}],
"variable_name": 'ticker',
"action_id": "update_data" }] # action_ids can point to controls
# changing the input state now activates this control so we no longer need a button
controls = [{ "control_type" : "hidden",
"label" : "get historical stock prices",
"control_id" : "update_data"}]
tabs = ["Plot", "Table"] # add tabs
outputs = [{ "output_type" : "plot",
"output_id" : "plot",
"control_id" : "update_data",
"tab" : "Plot", # must specify which tab each output should live in
"on_page_load" : True },
{ "output_type" : "table",
"output_id" : "table_id",
"control_id" : "update_data",
"tab" : "Table",
"on_page_load" : True }]
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
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.set_index('Date').drop(['volume'],axis=1).plot()
plt_obj.set_ylabel("Price")
plt_obj.set_title(self.company_name)
fig = plt_obj.get_figure()
return fig
app = StockExample()
# app.launch(port=9093)
There are five input types
and 4 output types:
Examples of all are available on github in the examples directory.
Adam Hajari
Data Scientist at Next Big Sound
adam@nextbigsound.com
@adamhajari
In [ ]: