Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Updating a Bokeh Graph in a Django Website
Bernd Wechner
Bernd Wechner

Posted on

     

Updating a Bokeh Graph in a Django Website

With ourfirst simple graph on an existing Django page, the challenge is to update it when the data on the page updates.

To recap: I have a Django page, on it is a table of data, and I have added a Bokeh histogram that summarises the frequency distribution of one of that table's columns. To get there we:

  1. Added some Bokeh JavaScript and css links to our Django template
  2. Installed Bokeh on the webserver, added the BokehApp to Django's INSTALLED_APPS.
  3. In the Django view (which loads and renders the template) created the graph (first a figure, then vbars, then the div and script components to add to template,
  4. Added the div and script components to our Django template.

And we have a simple histogram on the page. Nice.

But this page, allows users to specify a number of filters, and click a refresh button, which requests new data (with these filters applied, using an AJAX request) and updates the table.

It now needs to update the graph as well!
Updating Graph

Updating the data in a Bokeh graph (the options)

Turns out, there is no easy, or obvious way to do that. Bokeh is full of features, and can update all right, any number of ways, but not this way, not easily, not in any way I could find in the documentation. What options does it provide?At least these:

  1. Run aBokeh server. This is a server you can run which can run python code of your wanting. It's easy to run:bokeh serve --show myapp.py (though on a live webserver would need to be installed as a service and whatever port(s) - port 5006 by default - it uses to communicate with Bokeh in the client must be opened on the firewall, the gateway and any reverse proxy etc.). In short, this is a very nice prospect and feature as it communicates over awebsocket which has a number of advantages overAJAX. But, all the same, it's overkill for this trial. And parked. What I want is a way for my client side JavaScript, when the AJAX call completes and a response is received, to update the Bokeh Graph. I don't want a whole new piece of infrastructure (just yet, not on the first date).Parked!
  2. UsingBokehJS. Looks very promising. Basically a JavaScript interface to Bokeh (✔️). But the documentation is discouraging for this trial. The documentation boldly warns:The BokehJS APIs is still in development and may undergo changes in future releases. and all the examples it provides basically see the three key Python steps we used (creating a figure, vbars and components) moving entirely client side into JavaScript. That has some appeal too. Alas it totally does not explain if and how one might, using JavaScript, provide new data to a Bokeh graph created and provided by server side Bokeh. So this too we park for now, and bookmark the idea for later. For this trial, I am seeking a way to update the graph we created inpart 1.ParkedAgain!
  3. Configuring the graph to use anAjaxDataSource. Rather than providing two lists of numbers as a data source for the graph, we can provide it with a URL and a polling interval. And it will pool back checking for new data to that URL. But no, oh so close, but not what I want. I don't want polled AJAX calls, I already have a user-requested AJAX call (filter criteria changed, new data requested). So this too gets parked. Not a bad feature for live updating of data (though the Bokeh Server is probably better) but not good for this trial.AndParkedAgain!
  4. Using aCustomJS callback. This is another brilliant feature. Server side we can write some custom JS and attach it to a client side element to call back to, on given events, like for example data changing! Yes. And no .... Alas It can be attached to a whole range of Bokeh events, but not to our event (the click-handler for my button), the one that triggers an AJAX call to get new data and update the table. That button! I can't attach custom JS to that button and its click event, only to Bokeh widgets that have thejs_on_change method (with which the callback is registered with that widget). Again, ever so close, but no prize. That said, we will come to this one later in this trial, it does have a use, even in this trial (clue: we can use it to register a JavaScript callback that adjusts the axes, ticks and labels, which is called when the data on the vbars is changed - but we still have to find a way to change that data, client-side).ParkedForTheLastTime

And with that (four exhaustively research options parked), we throw our hands up in the air, file aStack Overflow plea for help, and hope for the best. But, no answers arriving in any hurry and wanting to soldier on, we are left with no options but to reverse engineer something (which is technical jargon for working out how to do something in lieu of documentation or mentorship)

Reverse engineering Bokeh (client-side)

Scratching my head, I was wondering "if, client-side, I have new data from AJAX call to my server, and I have a Bokeh histogram, how can I tell that histogram that I have new data and give it that data?" That was the crux of the problem that, in all my reading, and research, to that point, I had not found an answer to.

The first, baby-step, in reverse engineering, of course, is to take a look at the code. Fortunately, in nay modern browser, I can now press F12 and up propose the source code for the page and a wonderful suite of debugging tools. My, how easy things have become since the 1980s when I first started reverse engineering games by disassembling them to strip the copy protection from them (looks furtively around the room for any disapproving glares, and hopes thestatute of limitations is in full swing).
Reverse Engineering

Now if you recall, a web page with a Bokeh graph on it has adiv andscript provided bybokeh.embed.components and injected into the page using our Django template.

Step 1: That div

Here's what the div looks like:

<divclass="bk-root"id="591aa428-0ac3-4085-938e-481caf339309"data-root-id="1233">
Enter fullscreen modeExit fullscreen mode

wherein, we see our first two clues. Clearly anid that the Bokeh JavaScript library will use to find the div (and I'm betting at this point thescript contains a reference to it) and adata-root-id that sounds a lot like a lead in our hunt for a way to furbish thisdiv with new data!

Searching that page for thatid surprise, surprise it crops in the script, under:

constrender_items=[{"docid":"e71600f9-95b3-4b84-9210-eff6b3036e17","root_ids":["1233"],"roots":{"1233":"591aa428-0ac3-4085-938e-481caf339309"}}];
Enter fullscreen modeExit fullscreen mode

Where we see thedata-root-id andid bound once again.

Step 2: That script

Thescript itself yields none more clue. It starts with:

<scripttype="text/javascript">(function(){constfn=function(){Bokeh.safely(function(){
Enter fullscreen modeExit fullscreen mode

which tells us, there's a JavaScript object called Bokeh, that we might interface with. And what better way to explore that object than to use the browsers debugger.

Step 3: That Bokeh object

Perhaps the most straightforward way to catch JavaScript in action, after the whole page has loaded and everything has rendered and, it's in a clear run state is to set anEvent Listener Breakpoint on the mouse-click event.

Doing that, and then clicking on the Bokeh graph (I elected to click on the Refresh Tool button that the graph provides) breaks in the debugger, and there's a wonderful list of the variables, and under Global we see the Bokeh object.

By expanding it and looking at all its many many properties with patience a small clue might be found. But we might find faster clues by looking at and following the code.

We broke in an event handler:

functiondocumentClick(e){
Enter fullscreen modeExit fullscreen mode

to be exact. F9 is the debugger's Step key, so pressing F9 a few times and watching where we go may yield fruit. In fact, 11 F9 presses and we find outselves here:

this.throttled_paint=(0,throttle_1.throttle)(()=>this.repaint(),1000/60);
Enter fullscreen modeExit fullscreen mode

And that, is why I thought clicking the refresh Tool button might be a fine idea. There it is.this.repaint. But what isthis?

Easy, we just typethis into the console and see that it's:

PlotView{removed:Signal0,_ready:Promise,_slots:WeakMap,_idle_notified:true,model:Plot,}
Enter fullscreen modeExit fullscreen mode

Bingo!

Step 4: That PlotView

Now PlotView sounds good. In the console in fact I typethis. and after teh. the debugger intellisense predictor throws up a list of all the attributes of that object. I flick down them, and there's one that catches my eyemodel.

Sothis.model. and voila it has an id,this.model.id and it is ... 1233! yes, that is thedata-root-id of our div.

Getting warm now.

Looking at the Bokeh object I play hte same game.Bokeh. throws up a long list of attributes again, and I page down, and one catches my eye again. It isdocuments. Bokeh.documents seems to be an Array with one entry soBokeh.documents[0]. lets us walk through the attributes of a document and voila, there isBokeh.documents[0]._all_models and it has an interesting value:

Map(32) {"1233"=>Plot,"1256"=>Toolbar,"1250"=>PanTool,"1251"=>WheelZoomTool,"1252"=>BoxZoomTool, …}
Enter fullscreen modeExit fullscreen mode

32 entries and the first one is our plot. So let's try:

Bokeh.documents[0].get_model_by_id(1233)

no luck. How about:

Bokeh.documents[0].get_model_by_id("1233")

and that looks good, it's a Plot object so lets try:

Bokeh.documents[0].get_model_by_id(1233)==this.model
Enter fullscreen modeExit fullscreen mode

which istrue! So we have a way of getting models in Javascript!

Step 5: Finding the data source

Alas, the Plot object has no eaisly found data source. Poking around inside of it I do find it hasrenderers again a list with one entry, and so exploringthis.renderers[0] I see it has adata_source property, which in turn has adata property and, you get the idea ...this.model.data_source.data is on the money:

this.model.renderers[0].data_source.data{top:Array(12),x:Array(12)}
Enter fullscreen modeExit fullscreen mode

Andtop andx are the very thinhgs I provided tofigure.vbar at the server end and each of these 12 value Arrays are the very lists I provided. Found!

Step 6: That data-root-id, again

Recall, that in my Django view I built the graph as follows:

frombokeh.plottingimportfigurefrombokeh.embedimportcomponentsdefview_Events(request):# Collect the categories and values(players,frequency)=Event.frequency("players",events)# Create the figureplot=figure(height=350,x_axis_label="Count of Players",y_axis_label="Number of Events",background_fill_alpha=0,border_fill_alpha=0,tools="pan,wheel_zoom,box_zoom,save,reset")# And the barsbars=plot.vbar(x=players,top=frequency,width=0.9)# Build the context variablesgraph_script,graph_div=components(plot)# Add them to contextcontext={"graph_script":graph_script,"graph_div":graph_div}# Render the viewreturnrender(request,'events.html',context=context)
Enter fullscreen modeExit fullscreen mode

I checkedplot and sure enough it has anid. Andplot.id here is exaclty the same value that appears in the data-root-id of thediv and is the id of the Plot object that was repainting itself when we walked through the click handler. The threads come together.

Changing the data source

Sure enough if I passplot.id into context of the template and use client side it can find the Plot using that id.

And so on the client side I have:

constplot=Bokeh.documents[0].get_model_by_id(plotid)constsource=plot.renderers[0].data_sourcesource.data.x=players;source.data.top=frequency;
Enter fullscreen modeExit fullscreen mode

whereplotid came in during page load via the Django template context, andplayers andfrequency are delivered by the AJAX call.

And that sort of works. The values are set.

But the graph does not change. Hmmmm

Step 7: What's wrong?

I can see the data source is changed. Why hasn't the graph changed? I'm missing something.

Scratching my head a little, I think, the missing link must be that repaint command. That is, we have given the Plot new data, but we need to tell it that it has new data, tell it that it needs to redraw itself somehow.

So I break in the code above and inspect thesource variable for clues. And the very first attribute on its list ischange, which rings a loud bell. It has the value:

Signal0{sender:ColumnDataSource,name:"change"}
Enter fullscreen modeExit fullscreen mode

And so is a singal of some sort, that we might send to the Plot. Inspecting the attributes of the signal we find it has (4th on its list), anemit method.

So I add:

source.change.emit();
Enter fullscreen modeExit fullscreen mode

to code above to get:

constplot=Bokeh.documents[0].get_model_by_id(plotid)constsource=plot.renderers[0].data_sourcesource.data.x=players;source.data.top=frequency;source.change.emit();
Enter fullscreen modeExit fullscreen mode

And voila, now the graph updates. We hit paydirt!

Step 8: Do we really need renderers?

Not real comfortable with the use ofrenderers and at this stage seeing that there are a lot of models ...:

Bokeh.documents[0]._all_modelsMap(32){"1233"=>Plot,"1256"=>Toolbar,"1250"=>PanTool,"1251"=>WheelZoomTool,"1252"=>BoxZoomTool,}
Enter fullscreen modeExit fullscreen mode

32 models and among them when I expand the list are Vbars!

So I try passing not plot.id into the context, butbars.id and the AJAX response code becomes:

constbars=Bokeh.documents[0].get_model_by_id(barsid)constsource=bars.data_sourcesource.data.x=players;source.data.top=frequency;source.change.emit();
Enter fullscreen modeExit fullscreen mode

And it works beautifully. No renderers needed. The connection is clear now too. At the python side I pass the x/y data to vbar() and at thye JavaScript side I get the vbar and change it's data source and emit a change signal ...all works wonderfully.

The only thing I remain mildly uncomfortable about is thedocuments[0]. I'm stil not clear what documents are, when there might be more than one and so on.


Car vectors graciously created and made available by macrovector - www.freepik.com

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Busy, busy, busy .... see https://cutt.ly/busybusybusy.
  • Location
    Hobart, Tasmania
  • Education
    BE (Mech) UoW, ME (IT&T) FU among other things (if you're into acronyms)
  • Work
    Quality Assurance Engineer
  • Joined

More fromBernd Wechner

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp