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:
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?
Creating a Fully-Featured App (Where Data Vis is 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:
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 is 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 entirely 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
So what does "Dash inside Flask" look like from a project structure perspective? If you're familiar with the Flask Application Factory pattern, it won't look different at all:
That's right folks: I'm going to shove proper app structure down your throats any chance I get: even in the midst of a tutorial about hacking things together.
Initializing Flask with Dash
As with any respectable Flask app, our entry point resides within wsgi.py as expected. Simply standard Flask thusfar:
The initialization of our app happens within the init_app()
function following the Flask Application Factory pattern. As a refresher, here's what a barebones function to initialize a Flask app looks like:
This example is as simple as it gets. A Flask app is created, a config file is loaded, and some routes are imported. Pretty boring.
Now we're going to embed an app (Dash) within an app. Keep an eye on what changes:
All it takes is two lines! The first line imports a function called init_dashboard()
which initializes a Dash application, not unlike what init_app()
does itself. The second line registers our isolated Dash app onto our parent Flask app:
Let's turn our focus to import init_dashboard
for a moment. We're importing a file called dashboard.py from a directory in our Flask app called /plotlydash. Inside dashboard.py is a single function which contains the entirety of a Plotly Dash app in itself:
We pass our Flask instance to create_dashboard()
as a parameter called server
. Unlike the previous examples, its actually server
running the show this time, with Dash piggybacking as a module. This brings us to our most important line of code:
Instead of creating our dash_app
object as a global variable (as is suggested), we stuck in a function called create_dashboard()
. 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:
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:
See, not so bad!
Creating a Flask Homepage
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:
dashboard.py
dashboard.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 link to our dashboard via a regular Flask template like so:
Creating Something Useful
I threw together a working demo of Dash within Flask to demonstrate this in action. The example below shows the journey of a user navigating our app. The user lands on the homepage of our Flask app which we defined in routes.py. From there, the user is able to click through to the Plotly Dash dashboard we define in dashboard.py seamlessly:
The user is none the wiser that they've jumped from a Flask application to a Dash application, which is what makes this practice so appealing: by combining the ease of Plotly Dash with a legitimate web app, we're empowered to create user experiences which go far beyond the sum of their parts.
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:
A working version of this demo is live here:
I've uploaded the source code for this working example up on Github. Take it, use it, abuse 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 there's a good chance we'll cover all of them.