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 [6]:
from googlefinance.client import get_price_data
def getData(params):
ticker = params['ticker']
xchng = "NASD"
param = {
'q': ticker, # Stock symbol (ex: "AAPL")
'i': "86400", # Interval size in seconds ("86400" = 1 day intervals)
'x': xchng, # Stock exchange symbol on which stock is traded (ex: "NASD")
'p': "3M" # Period (Ex: "1Y" = 1 year)
}
df = get_price_data(param)
return df.drop('Volume', axis=1)
params = {'ticker':'AAPL'}
df = getData(params)
df.head()
Out[6]:
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
from googlefinance.client import get_price_data
server.include_df_index = True
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{
"type": 'dropdown',
"label": 'Company',
"options": [
{"label": "Google", "value": "GOOG"},
{"label": "Amazon", "value": "AMZN"},
{"label": "Apple", "value": "AAPL"}
],
"key": 'ticker',
"action_id": "plot"
}]
outputs = [{
"type": "plot",
"id": "plot",
"control_id": "update_data"
}]
def getData(self, params):
ticker = params['ticker']
xchng = "NASD"
param = {
'q': ticker, # Stock symbol (ex: "AAPL")
'i': "86400", # Interval size in seconds ("86400" = 1 day intervals)
'x': xchng, # Stock exchange symbol on which stock is traded (ex: "NASD")
'p': "3M" # Period (Ex: "1Y" = 1 year)
}
df = get_price_data(param)
return df.drop('Volume', axis=1)
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.plot()
plt_obj.set_ylabel("Price")
plt_obj.set_xlabel("Date")
plt_obj.set_title(params['ticker'])
return plt_obj
app = StockExample()
app.launch()
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
from googlefinance.client import get_price_data
server.include_df_index = True
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{
"type": 'dropdown',
"label": 'Company',
"options": [
{"label": "Google", "value": "GOOG"},
{"label": "Amazon", "value": "AMZN"},
{"label": "Apple", "value": "AAPL"}
],
"key": 'ticker',
}]
outputs = [{
"type": "plot",
"id": "plot",
"control_id": "update_data"
}, {
"type": "table",
"id": "table_id",
"control_id": "update_data"
}]
controls = [{
"type": "button",
"label": "get stock data",
"id": "update_data"
}]
def getData(self, params):
ticker = params['ticker']
xchng = "NASD"
param = {
'q': ticker, # Stock symbol (ex: "AAPL")
'i': "86400", # Interval size in seconds ("86400" = 1 day intervals)
'x': xchng, # Stock exchange symbol on which stock is traded (ex: "NASD")
'p': "3M" # Period (Ex: "1Y" = 1 year)
}
df = get_price_data(param)
return df.drop('Volume', axis=1)
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.plot()
plt_obj.set_ylabel("Price")
plt_obj.set_xlabel("Date")
plt_obj.set_title(params['ticker'])
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 [27]:
from spyre import server
from googlefinance.client import get_price_data
server.include_df_index = True
class StockExample(server.App):
title = "Historical Stock Prices"
inputs = [{
"type": 'dropdown',
"label": 'Company',
"options": [
{"label": "Google", "value": "GOOG"},
{"label": "Amazon", "value": "AMZN"},
{"label": "Apple", "value": "AAPL"}
],
"key": 'ticker',
"action_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"
}]
controls = [{
"type": "hidden",
"label": "get stock data",
"id": "update_data"
}]
def getData(self, params):
ticker = params['ticker']
xchng = "NASD"
param = {
'q': ticker, # Stock symbol (ex: "AAPL")
'i': "86400", # Interval size in seconds ("86400" = 1 day intervals)
'x': xchng, # Stock exchange symbol on which stock is traded (ex: "NASD")
'p': "3M" # Period (Ex: "1Y" = 1 year)
}
df = get_price_data(param)
return df.drop('Volume', axis=1)
def getPlot(self, params):
df = self.getData(params)
plt_obj = df.plot()
plt_obj.set_ylabel("Price")
plt_obj.set_xlabel("Date")
plt_obj.set_title(params['ticker'])
return plt_obj
app = StockExample()
app.launch()
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 [ ]: