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 [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]:
Open High Low Close
2018-01-29 16:00:00 170.160 170.1600 167.07 167.96
2018-01-30 16:00:00 165.525 167.3700 164.70 166.97
2018-01-31 16:00:00 166.870 168.4417 166.50 167.43
2018-02-01 16:00:00 167.165 168.6200 166.76 167.78
2018-02-02 16:00:00 166.000 166.8000 160.10 160.50

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

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 [ ]: