Note, this page will only work with Javascript... sorry.
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
rendered via
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
render_template
is 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 .md
file.
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
anyways.
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, pd
is pandas
,
px
is plotly.express
, and 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
against this problem for a while, as I didn't want to add more Javascript to the
site, or import any more libraries on the Javascript side, like jQuery, but in
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
with either Javascript, or using jQuery as a convenience.
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 <div>
with id='chart'
, which
will be used to tell Plotly where to put the chart (it searches for <div>
s
with the correct name). Then I source two Javascript libraries (sigh), the
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
ok.
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
function/callback .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.
The .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 data
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,
1 2 |
|
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:
covid_graph
path to return a Plotly plot full screen rather than just
the JSON? Or a button to expand the Plotly size? It's a little small right
now (sounds like Javascript 💀).blog.py
much too crazy.