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.
wsgi.py is always our app's entry point in this app pattern. This is standard practice:
Now let's take a peek at how our Flask app is being created in
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:
Let's turn our focus to
import create_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
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™.
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 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:
<a href="/dashapp/" class="dash-link"> <span>Embdedded Plotly Dash</span> <i class="fas fa-arrow-right"></i> </a>
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: https://plotlydashflask.hackersandslackers.app/
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.