Simple Interactive Web Applications with Spyre

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.

Where does Spyre Live?

GitHub: github.com/adamhajari/spyre

Live example of a spyre app: adamhajari.com

Installing Spyre

Spyre depends on:

  • cherrypy (server and backend)
  • jinja2 (html and javascript templating)
  • matplotlib (displaying plots and images)
  • pandas (for working within tabular data)

Assuming you don't have any issues with the above dependencies, you can install spyre via pip:

$ pip install dataspyre

Launching a Spyre App

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.

A Very Simple Example

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.

Under the Hood: Auto-Generating the Front-End with Jinja

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.

Another Example

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()


             close    high     low    open    volume
Date                                                
2014-08-22  101.32  101.47  100.19  100.29  44184000
2014-08-25  101.54  102.17  101.28  101.79  40270000
2014-08-26  100.89  101.50  100.86  101.42  33152000
2014-08-27  102.13  102.57  100.70  101.02  52369000
2014-08-28  102.25  102.78  101.56  101.59  68460000

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)

More Examples On GitHub

There are five input types

  • text
  • slider
  • dropdown
  • radiobutton
  • checkbox group

and 4 output types:

  • plots
  • images
  • tables
  • html

Examples of all are available on github in the examples directory.

Who Am I?

Adam Hajari
Data Scientist at Next Big Sound
adam@nextbigsound.com
@adamhajari

In [ ]: