Ahh, Plotly. Typing that name into a post headline triggers an emotional cocktail of both pride and embarrassment. Plotly has been at the core of some of the most influential products I’ve personally worked on over the years: a jumble of Fintech and humanitarian clients, all of which are still proudly waving their charts and dashboards around the world. Yet, my mind is boggled by a simple question: what the hell took us so long to write our first post about Plotly? 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 reaching this point?

Much has changed in the last year or so for our friends in Montreal. Number 1 in my book is the 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 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), Plotly's Dash does a shockingly good job of breathing life into that romantic fantasy of committing to Python forever.

But we're not here to deliver a recycled 'What is Plotly?' synopsis. We're not even interested in the obligatory 'How to Get Started Using This Already-Well-Documented-Technology' post. Plotly deserves better than that. Instead, we're coming hot out of the gate swinging: we're going to show you how to beat Plotly down, break it, and make it bend to your will. Welcome to a magical edition of Hacking Plotly. It must be Christmas, folks.

Let's Make a Plotly + Flask Lovechild from Hell

Like most advancements in Python-related architecture this year, Dash has a little secret: it's gotten here with a little help from Flask. In fact, Dash actually extends Flask: every time we make a Dash app, we're actually creating a Flask app with extra bells and whistles. It sounds sensible, and perhaps even exciting: if you love Flask as I do, your mouth may be watering right now. The prospect of combing the power of Plotly with Flask is the equivalent to 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.

The moment Dash is initialized with app = Dash(__name__), it spins up a Flask app to piggyback off of. In retrospect, this shouldn't be surprising because the syntax for starting a Dash app is precisely the same as starting a Flask app. Check out the recommended startup boilerplate:

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

external_stylesheets = ['/static/dist/css/style.css']
external_scripts = ['/static/dist/js/includes/jquery.min.js', 
                    '/static/dist/js/main.js']


app = Dash(__name__,
          external_stylesheets=external_stylesheets,
          external_scripts=external_scripts,
          routes_pathname_prefix='/dash/')

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 try 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? Plotly 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. Plotly has (perhaps intentionally) created a sandbox for you with specific constraints. It's understandable: Plotly is a for-profit company, and this is a no-profit product. If it were too easy to bend Plotly Dash, would companies still need an enterprise license?

Dash excels at what it was intended to do: building dashboard-based applications. The issue is that applications which can only display data aren't always useful 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='/dash')
               
app.layout = html.Div(id='dash-container')

@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:

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 please: enough with the app.py nonsense.

from app 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 comes with it. Take a look at application/__init__.py:

"""Initialize app."""
from flask import Flask


def create_app():
    """Construct the core application."""
    app = Flask(__name__,
                instance_relative_config=False)
    app.config.from_object('config.Config')

    with app.app_context():

        # Import main Blueprint
        from . import routes
        app.register_blueprint(routes.main_bp)

        # Import Dash application
        from .dash_application import dash_example
        app = dash_example.Add_Dash(app)

        return app

It's almost as though nothing changed! In fact, we only have two lines related to Dash: the first imports a Python module, and the second registers our isolated Dash app with our parent Flask app:

# Import Dash application
from .dash_application import dash_example
app = dash_example.Add_Dash(app)

Let's turn our focus to from .dash_application import dash_example for a moment. What is this, you might ask? dash_example is actually a Python file ( ./dash_application/dash_example.py ) which contains our Dash app! Dash typically likes 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 dash_example.py:

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):
    """Create Dash app."""
    external_stylesheets = ['/static/dist/css/style.css']
    external_scripts = ['/static/dist/js/includes/jquery.min.js', 
                    '/static/dist/js/main.js']
                    
    dash_app = Dash(server=server,
                    external_stylesheets=external_stylesheets,
                    external_scripts=external_scripts,
                    routes_pathname_prefix='/commands/')

    # Create Dash Layout
    dash_app.layout = html.Div(id='dash-container')

    return dash_app.server

We pass our Flask instance to Add_Dash as a parameter called server. Unlike the previous examples, its 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,
                external_stylesheets=external_stylesheets,
                external_scripts=external_scripts,
                routes_pathname_prefix='/commands/')

Instead of creating our dash_app object as a global variable (as is suggested), we stuck in a function called Add_Dash(). This allows us to pass our top-level Flask app into Dash as server, hence dash_app = Dash(server=server). This effectively spins up a Dash instance using our Flask app at its core, as opposed to its own!

Take note of how we pass a value to routes_pathname_prefix when creating dash_app. This is effectively our workaround for creating a route for Dash within a larger app: everything we build in this app will be preceded with the prefix we pass (of course, we could always pass / as our prefix). Dash has full control over anything we build beneath the hierarchy of our prefix, and our parent Flask app can control pretty much anything else. This means we can build a sprawling Flask app with hundreds of features and views, and if we want a Dash view, we can just create a module or subdirectory for that to chill in. It's the best collab since jeans and pockets.

Now you're thinking with portals™.

Subtle Differences

Because we create Dash in a function, we should be aware of how this will change the way we interact with the core dash object. The bad news is copy + pasting other people's code will almost certainly not work, because almost every keeps the Dash() object as a global variable named app. The good news is, it doesn't matter! We just need to structure things a bit more logically.

For example, consider callbacks. Dash enables us to create callback functions with a nifty callback decorator. The docs structure this as such:

import dash
from dash.dependencies import Input, Output
import dash_table
import dash_html_components as html

app = dash.Dash(__name__)

app.layout = html.Div([
    # ... Layout stuff
])


@app.callback(
    # ... Callback input/output
    )
def update_graph(rows):
    # ... Callback logic

Notice how everything is global; app is global, we set app.layout globally, and callbacks are defined globally. This won't work for us for a number of reasons. Namely, we don't create Dash() upon file load; we create it when our parent Flask app is ready. We need to structure our Dash file a bit more logically by using functions to ensure our app is loaded before defining things like callbacks:

import dash
from dash.dependencies import Input, Output
import dash_table
import dash_html_components as html

def Add_Dash(server):
    app = dash.Dash(__name__)
    app.layout = html.Div([
        # ... Layout stuff
    ])

    # Initialize callbacks after our app is loaded
    # Pass dash_app as a parameter
    init_callbacks(dash_app)

    return dash_app.server

def init_callbacks(dash_app):
    @app.callback(
        # ... Callback input/output
        )
    def update_graph():
        # ... Insert callback stuff here

See, not so bad!

What Our App Looks Like

If you're following along, it would probably help to have a top-level view of what's going on so far:

plotlydash-flask-tutorial
├── /application
│   ├── __init__.py
│   ├── routes.py
│   ├── /static
│   ├── /templates
│   └── /dash_application
│       └── dash_example.py
├── /data
├── Pipfile
├── Pipfile.lock
├── README.md
├── config.py
├── requirements.txt
├── setup.py
├── start.sh
└── wsgi.py

We're storing our core application within a directory called /application. This is where we create our Flask app and handle anything related to core app logic.

Inside that directory, we have a module named /dash_application. This is the module for our separate Dash application. If we wanted, we could even isolate /dash_application to a directory living alongside /application, depending on which structure makes sense for you.

routes.py

Because the entry point to our app now comes through Flask, routes.py has the flexibility to serve up anything we want. We now have the freedom to build an app without restriction, jumping in or out of Plotly Dash on the views we see fit. I added a simple landing page to demonstrate this:

"""Routes for core Flask app."""
import os
from flask import Blueprint, render_template
from flask import current_app as app

main_bp = Blueprint('main_bp', __name__,
                    template_folder='templates',
                    static_folder='static')


@main_bp.route('/')
def home():
    """Landing page."""
    return render_template('index.html',
                           title='Plotly Flask Tutorial.',
                           template='home-template',
                           body="This is an example homepage served with Flask.")

dash_example.py

dash_example.py is the Dash app we have living within our Flask app. But how does Flask know which route is associated to Dash? Wasn't it missing from routes.py? Indeed it was, good fellow! Because we set routes_pathname_prefix while creating the dash_app object, we don't need to create a route for Dash: it will always be served whenever we navigate to 127.0.01/dashapp. Thus, we can create a navigation template like this:

<nav>
  <a href="/"><i class="fas fa-home"></i> Home</a>
  <a href="/dashapp/"><i class="fas fa-chart-line"></i> Embdedded Plotly Dash</a>
</nav>

Creating Something Useful

Here's a fun little thing I was able to do with Dash, while in the context of running under a Flask app. In our file dash_example.py, I have the app look at a folder of extracted datasets (called /data). For each dataset, I use Pandas to generate a preview, and Dash's "data table" component to render said previews in our Dash 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.

If you're hungry for some source code to get started building your own Plotly Dash views, here's the source I used to create the page above:

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):
    """Create a Dash app."""
    external_stylesheets = ['/static/dist/css/plotly-flask-tutorial.css',
                            'https://fonts.googleapis.com/css?family=Lato',
                            'https://use.fontawesome.com/releases/v5.8.1/css/all.css']
    external_scripts = ['/static/dist/js/includes/jquery.min.js',
                        '/static/dist/js/main.js']
    dash_app = Dash(server=server,
                    external_stylesheets=external_stylesheets,
                    external_scripts=external_scripts,
                    routes_pathname_prefix='/dashapp/')

    # Override the underlying HTML template
    dash_app.index_string = '''<!DOCTYPE html>
        <html>
            <head>
                {%metas%}
                <title>{%title%}</title>
                {%favicon%}
                {%css%}
            </head>
            <body>
                <nav>
                  <a href="/"><i class="fas fa-home"></i> Home</a>
                  <a href="/dashapp/"><i class="fas fa-chart-line"></i> Embdedded Plotly Dash</a>
                </nav>
                {%app_entry%}
                <footer>
                    {%config%}
                    {%scripts%}
                    {%renderer%}
                </footer>
            </body>
        </html>'''

    # Create Dash Layout comprised of Data Tables
    dash_app.layout = html.Div(
        children=get_datasets(),
        id='dash-container'
      )

    return dash_app.server


def get_datasets():
    """Return previews of all CSVs saved in /data directory."""
    data_filepath = list(p.glob('data/*.csv'))
    arr = ['This is an example Plot.ly Dash App.']
    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

I've gone ahead and uploaded the source code for this working example up on Github. Please steal it: it's all yours.

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