Cracking Full Control Over Plot.ly Dash

Build apps with Plot.ly Dash on your own terms

Plot.ly; Finally typing those letters into a post headline tastes like a strange cocktail of pride and embarrassment. Plot.ly sits at the core of some of the most influential products I’ve had the chance to work on, which remain serving a hodgepodge of Fintech and humanitarian clients well to this day. This begs the question: what the hell took us so long to write our first post about it? We've been operating Hackers and Slackers for over a full year now. Did I seriously write a post about JQuery in that time before writing about Plot.ly?

Much has changed in a single year for Plotly. Number 1 in my book right now is a price reduction of their core product from 300 dollars to zero. I paid the 300 dollars. We really need to get a “donate” button around here. A close second on the list is undoubtedly the introduction of Plot.ly Dash. Dash tickles a sentiment which has danced through many young and helplessly naïve Pythonistas' minds: what if we could write only in Python, like, forever? As awful of an idea it is to start Googling Python-to-frontend code interpreters (they exist; I checked), Plot.ly's Dash does a shockingly good job of breathing life into that same romantic fantasy of committing to Python forever.

But we're not here to deliver a recycled what is Plot.ly? synopsis this evening. We're not even interested in the obligatory how to get started using this already-well-documented-technology post. Plot.ly deserves better than that. Instead, we're coming hot out of the gate swinging. We're going to show you how to beat Plot.ly down, break it, and make it bend to your will. Welcome to a magical edition of Hacking Plot.ly. It must be Christmas, folks.

Let's Make a Plotly + Flask Lovechild from Hell

Like almost every single advancement to come out of Python-geared architecture this year, Dash has a little secret: it's gotten here with a little help from Flask. Alright, perhaps more than a little: Dash actually extends Flask. Sounds sensible, and perhaps even exciting at first; its almost as though every crush you've ever had decided it be best to simply put their differences aside to start a group chat with you in the interest of making your sexual well-being an equal team effort out of sheer love. As you've already guessed, life doesn't work like that.

Dash hijacks Flask from the beginning, starting with the way we instantiate the app. Any code monkey who has laid eyes upon a wsgi.py file can tell you something is up before you can even say app = dash.Dash(__name__). Check out the recommended startup boilerplate:

from dash import Dash
import dash_core_components as dcc
import dash_html_components as html

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div(
        id='example-div-element'
        )

if __name__ == '__main__':
    app.run_server(debug=True)

If you were to attempt to take this boilerplate and attempt to add core Flask logic, such as authentication with Flask-Login, generating assets with Flask-Assets, or just creating a global database, where would you start? Plot.ly cleverly suggests reserving the app namespace for your app- the very same that we would do with Flask. Yet if we attempt to modify the app object the same as we would with Flask, nothing will work: Dash has declared an ecosystem, and nowhere in that ecosystem are you invited to add custom Flask application logic out of the box.

Dash does what it was intended to do very well: building dashboard-based applications. The issue is that applications which can only display data aren't entirely useful as end products. What if we wanted to create a fully-featured app, where data visualization was simply a feature of said app?

A common "workaround" you'll find in the community is passing Flask to Dash as the underlying "server", something like this:

from flask import Flask
from dash import Dash
import dash_core_components as dcc
import dash_html_components as html

server = Flask(__name__)
app = dash.Dash(__name__, server=server, url_base_pathname='/path')
app.layout = html.Div(id='example-div-element')

@server.route("/dash")
def MyDashApp():
    return app.index()

Make no mistake: this method sucks. Sure, you've regained the ability to create routes here and there, but let's not forget:

  • Your app will always start on a Dash-served page: if anything, we'd want our start page to be something we have full control over to then dive into the Dash components.
  • Access to globally available Flask plugins are still unavailable in this method. Notice how we never set an application context?
  • Your ability to style your application with static assets and styles is completely out of your hands.
  • Container architecture built on Flask, such as Google App Engine, won't play nicely when we start something that isn't Flask. So there's a good chance that playing by the rules means losing the ability to deploy.

If we want to do these things, we cannot start our app as an instance of Dash and attempt to work around it. Instead, we must create a Flask app, and put Dash in its place as an app embedded in our app. This gives us full control over when users can enter the Dash interface, and even within that interface, we can still manage database connections or user sessions as we see fit. Welcome to the big leagues.

Turning the Tables: Dash Inside Flask

First things first, let's get our wsgi.py file back. Pretty much any hosted Python application expects this, so enough with the app.py nonsense.

from application import create_app

app = create_app()

if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True)

Look familiar? Not only do we get Flask back, but we get our entire application factory and all that it includes. Take a look at application/__init__.py:

from flask import Flask, g
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from . import dashview


def create_app():
    """Construct the core application."""
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object('config.Config')
    dash_app = dashview.Add_Dash(app)
    
    # Global Database and Session Cache
    db = SQLAlchemy()
    redis_store = FlaskRedis()

    with app.app_context():
        # Create the Application Context
        redis_store.uri = app.config['SQLALCHEMY_DATABASE_URI']
        redis_store.endpoint = app.config['ENDPOINT']
        db = SQLAlchemy(app)

        # Construct the data set
        from . import routes
        app.register_blueprint(routes.dashboard_blueprint)

        return app

It's almost as though nothing changed! In fact, the only line we have regarding Dash here is dash_app = data.Add_Dash(app).

We import 'dashview' at the start of __init.py__. What is this, you might ask? It's actually a file named dashview.py living in our application folder. Dash apps like to have a single .py file per view, which turns out to work great for us. Let's look at why this works by checking out dashview.py:

import glob
from dash import Dash
import dash_table
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd


def Add_Dash(server):
    """Populates page with previews of datasets."""
    dash_app = Dash(server=server, url_base_pathname='/dataview/')
    dash_app.css.append_css({
        "external_url": "https://derp.sfo2.digitaloceanspaces.com/style.css"
        })

    # Create layout
    dash_app.layout = html.Div(
        id='flex-container'
      )

    return dash_app.server

We pass our Flask instance to Add_Dash as a parameter called server. Unlike the previous examples, it's actually server running the show this time, with Dash piggybacking as a module. This is our most important line of code:

dash_app = Dash(server=server, url_base_pathname='/dataview/')

Dash doesn't handle routes like Flask does (or at all, really). That's fine! We start dash_app with URL prefix, which means the Dash logic here is confined to that single page. This means we can build a sprawling Flask app with hundreds of features and views, and oh yeah, if we want a Dash view, we can just create a file for that to chill on its own, not touching anything else.

Now you're thinking with portals™.

Just for Funsies

Here's a fun little thing I did Dash in this context. In our file dashview, I have the app look at a folder of extracted datasets (called /datasets). For each dataset, I use Pandas and Flask's data tables to create a preview of each dataset import to our app. This lets us quickly cruise through the data an app depends on with a cool interface:

A bit rough around the edges, but you get the point.

Here's the source I wrote to create this:

import glob
from pathlib import Path, PurePath
from dash import Dash
import dash_table
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd

p = Path('.')

def Add_Dash(server):
    """Populates page with previews of datasets."""
    dash_app = Dash(server=server, url_base_pathname='/dataview/')
    dash_app.css.append_css({
        "external_url": "https://derp.sfo2.digitaloceanspaces.com/style.css"
        })

    # Create layout
    dash_app.layout = html.Div(
        id='flex-container',
        get_datasets(),
      )

    return dash_app.server


def get_datasets():
    """Gets all CSVS in datasets directory."""
    data_filepath = list(p.glob('application/datasets/*.csv'))
    arr = []
    for index, csv in enumerate(data_filepath):
        print(PurePath(csv))
        df = pd.read_csv(data_filepath[index]).head(10)
        table_preview = dash_table.DataTable(
            id='table' + str(index),
            columns=[{"name": i, "id": i} for i in df.columns],
            data=df.to_dict("rows"),
            sorting=True,
        )
        arr.append(table_preview)
    return arr

Needless to say, there's way more cool shit we can accomplish with Plot.ly Dash. Stick around long enough, and chances are we'll cover all of them.

Todd Birchard Author image
New York City Website
Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.

Product manager turned engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.