One of the goals I had for this site when starting was the ability to have interactive plots. Making a beautiful figure is nice, but it is so much more fun when you can zoom, have floating tooltips, and take advantage of the fact that you're on a computer, not staring at a piece of printed paper.
Basic plotting and data visualization in Python is not really such a contested topic - just use Matplotlib, or Seaborn if you're feeling fancy. I'm comfortable using Matplotlib, and while I guess you can use it to create interactive HTML, it's not really the right tool for the job if you want more full fledged interactivity. While this post won't show the use of callbacks (the ability to load different data at the press of a button on the same chart), it will show some things with tooltips that (to my knowledge) are not really possible on the web using Matplotlib.
If you're willing to move beyond Matplotlib, there are a host of packages that will help you achieve your goals. The major ones to me seem to be Plotly and Bokeh, with Altair as a third option (see here for a Google Colab with all examples). I've played (briefly) with all three, but for the purposes of this blog, I'll be using Plotly.
Why Plotly over Bokeh or another alternative? Based on the research I did, I liked the structure of a Flask + Plotly project seen here, and the comparison done here seemed to make it clear that Plotly would be better for me, as I'm not building a dashboard, think native 3D support could be nice, and I don't want to (yet) mess with state. For those reasons, I started with Plotly.
There will likely be another post where I make some of the same plots or a dashboard with Bokeh...
Once again - this site's source code.
At first I just kind of followed the tutorial linked in the previous section.
However, that tutorial is focused on making a chart on a specific page, or
route. That is all well and good, but not as flexible as I wanted it to be. It
relies on Flask's
render_template, and passing in the JSON that describes the
chart during the page rendering. I realized this would be a problem, as I wanted
figures to be in my blog posts as well, which are Markdown files that are
Flask-Flatpages. Each post
is rendered with the following code:
1 2 3 4 5 6
As you can see, the only optional variable that gets passed into
post=post, and I didn't see a good way of extending that
to include the plot JSON, in the case where a Plotly figure was in the
I guess I could have an optional parameter, like
chart_data_list, which is a
list, default of
None, and some custom parser of
post right before the
return that checks if there is any chart, and if so, calls the right url and
gets the JSON, appending it to the
chart_data_list... but that idea just came
to me now, after everything is done and I think it wouldn't work in practice
Another option might be to make a route that generates the whole plot, and then just somehow linking that within a blog post? I will have to explore that idea more too.
Anyways, here's what I did instead.
I first started with the example plot in the tutorial (copied below), and it worked very well.
1 2 3 4 5 6 7 8 9 10 11 12
For clarification, if you don't want to click on the tutorial,
json is a Python standard library package.
However, I didn't want to return a rendered template from my route, but rather
just the JSON that could then be requested. In order to do so, I added
from flask import Response, and then replaced the return line with
return Response(graphJSON, mimetype="application/json"). Now, if you
visit the endpoint that I was building, you'll
see that it's just a JSON response. Creating the response was now complete.
Now, how to get the JSON into a Flask-Flatpages-rendered page? I banged my head
the end, I couldn't figure out a good way to do so while avoiding that. What the
site needed to do is render the Markdown into HTML (Flask-Flatpages), and then
when/while the HTML is being served, populate the correct
<div> that has the
plot with the required JSON from Flask. In the end, that meant doing something
My approach can be seen in this page's source in the repository, but I'll show and describe it below. In the end it was relatively simple (but only after lots of trial and error though, and thanks Pablo). First, the implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
So, what is happening here? First, I create a
will be used to tell Plotly where to put the chart (it searches for
first being Plotly, which is great, and the second being jQuery, which is
helpful, but not ideal as I wanted to minimize outside dependencies in order to
keep everything running snappily. In the end, when I profile the code though,
the Plotly library is about 1 Mb, while jQuery is about 80 kb, which I think is
Then comes the script that loads the plot - I get the data using jQuery's
.getJSON function. (
$ stands for jQuery, which has always been confusing to
me.) After not understanding the
documentation of this function really
at all, I kind of hacked something together. In my understanding, the
.done runs upon a successful completion of getting the JSON,
and so I put my plotting code there. If the JSON isn't retrieved, it won't plot.
Edit - I have added more explanation to what jQuery is doing/how it works in the next post.
.done function somehow has an input
data from the
getJSON function -
this I don't understand at all. But I can create a variable
layout for Plotly,
as well as update the
config of the returned JSON
data, (to turn off an
annoying pop up when interacting with the graph, and to turn on responsiveness
in sizing), and then
Plotly.newPlot the returned JSON... and it just works! It
feels like magic so far, I have almost 0 understanding of where the
argument comes from. But we're in business 🎉!
Quickly I can define
/sine_graph/ in my app, which is called above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
And now we've reproduced the static plots above in Plotly, which means it's now interactive!
Note - there is no legend.. it seemed too hard to make when using Plotly express and I just wanted a quick demo before the real plot.
Throughout Covid, Berlin has used a stoplight system in order to determine what restrictions are in place, and their (non-interactive) dashboard can be seen here. It always frustrated me that I couldn't zoom into their graphs, and so I decided to recreate it. Later, I found that they kind-of have an interactive version, but it still wasn't what I wanted.
First was to find the data - it was in German so I had to do some googling around, but eventually found the data source. Initially I downloaded the data myself, but to keep it updated, I automated the download if the file was over a day old. That can be seen below, and note some packages need to be imported.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
I then do some extremely minor data wrangling,
and then generate the plot.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
Success - the end result is the chart below!
Some notes - you can click on the legend to remove one (or both) of the lines. You can also click and drag in the plot area to zoom into a subregion, and double click to reset zoom.
There is a lot to improve here, and a lot more that I want to try. In no particular order:
blog.pymuch too crazy.