'Bokeh: Generate graph server side, update graph from JS client side (change data source, axes ...)
I just took Bokeh for a spin on a Django site. Lot of fine tutorials out there and really easy to get a quick example running. For example, I have a Django view that offers something like:
...
# This is part of a broader Django view, simply adding a graph to it.
# Fetch the x and y data for a histogram using a custom function.
# It takes a queryset, a field in that queryset and returns, two lists one
# containing the unique values that some field took on and the second containing
# the count of times it took on that value.
(values, frequency) = FrequencyData(some_queryset, "some field")
p = 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")
p.xaxis.ticker = values
p.yaxis.ticker = list(range(max(frequency) + 1))
p.toolbar.logo = None
p.vbar(x=values, top=frequency, width=0.9)
p.y_range.start = 0
graph_script, graph_div = components(p)
context.update({"graph_script": graph_script,"graph_div": graph_div})
...
Then in the Django template I simply have:
{{ graph_div | safe }}
{{ graph_script| safe }}
And I have a lovely histogram of data that is otherwise presented in a table on that view. I like it.
Now the same view, like many, has a pile of settings for filtering the data primarily and a refresh button. The refresh button triggers an AJAX callback to the view which duly delivers the date in JSON, and the table and everything else on the page are updated in JS to reflect the new data. All fairly standard stuff.
Alas, the graph does not update. And this is not new, I can find a lot of questions and answers about that. BUT, the crunch is none of them have provided me with satisfaction yet, and I've run out of findable answers. I've looked through Bokeh docs and also not found joy yet.
So, to break down the solutions on the table already that don't please me fully:
Run a Bokeh server. That would be in parallel to the Django UWSGI service, and It has some benefits but is overkill here IMHO. I understand it communicates using Tornado (and web sockets) with the client side JS (included with
{{ graph_script| safe }}
and it's dependencies from a CDN of course as per all the docs). But it's my Django app that gets the AJAX request for new data and even if I wanted to add this bit of infrastructure (and I may later of course of other benefits) I'm stuck with the Django app telling the Bokeh server that the graph has new data. So they also need to talk.Using BokehJS. Alas, that looks like a paradigm shift to building the graph client side. More attractive and a fall back option indeed, I just send the data to the client side (via the template rendered on page load and AJAX afterwards when updating) and render the graph client side. Downsides are the warning on the help (that this is in development and may change), and the frustration, in a sense, of not finding a simpler solution that seems in line with standard Bokeh demos and tutorials.
Configuring the graph to use an AjaxDataSource. To be honest I'm not quite sure how to integrate that with the sample above yet, but haven't researched this deeply as it's unsatisfying, works on its own Ajax call and a polling interval. I already do an Ajax call, on demand when a user changes settings and asks for an update, and I'd like the graph to source the data from the return of my call, not its own. Basically, I'm plugging a graph here into an existing context with its modus operandi in place.
Using a CustomJS callback. And this is really hopeful. And at the server side I have access to objectsthat have the
js_on_change
method, but the help provides no real clue on how trigger such a callback a CustomJS defined function, from one's own JS code (with no acces as yet to any Bokeh object). In short, it is interaction with a non-Bokeh widget, an existing refresh button on a page that performs an AJAX call, fetches data, and now wants to update the graph. But that widget has no (seemingly well document or found by me) method of reconfiguring the graph with new data and axes ticks etc.
So what does my ideal solution look like? Well, in my Python view above, I add the data:
# as above ... except:
context.update({"graph_script": graph_script,"graph_div": graph_div, "values": values, "frequency": frequency})
and then client side in JavaScript when I get the table data and process it to update the table I also have some way of saying to "graph_div" something like: "Hey, I have new x and y data for you and axis configs".
In short, a bit of BokehJS (being able to refer to the graph client side and update it) and a bit of standard Bokeh (generating the graph sever side for the first page load).
What this would need of course is for graph_script
(produced by bokeh.embed.components) to contain a name (provided by components) for a graph object, that exposes a BokehJS like interface for the graph to JS.
I can look into graph_div
and see it's given an id
and a data-root-id
:
<div class="bk-root" id="ce45dc38-0977-4d2c-a8ae-033dafad5fc8" data-root-id="1078">
seemingly assigned by Bokeh of course and needed for BokehJS to find the div I'd guess. And I can likewise look at graph_script
and see that it does embed in the root two variables docs_json
and render_items
and also break in the browser Debugger and inspect and see there's a nice global object Bokeh
quite available, lending enormous hope that it can be used to supply new data and axes configs in JS to an existing Bokeh graph when it arrives via our own Ajax call (that delivers more stuff).
I suspect if I find a solution here, or someone can help me find one, it's worth a small tutorial that I might write! ;-)
Solution 1:[1]
I use your blog to find an answer using json request in django. My web page is bokeh.traimaocv.fr/index/slider slider send request to django server and get x and y data using json
def get_arg_post(request, list_var):
"""
Récupération des données POST dans une requète
request --> objet HTTPrequest
list-var --> liste des variables à extraire de la requête
valeur retour --> booléen et liste des valeurs
booléen False si les valeurs n'ont pas été trouvées
"""
resultat = []
if len(request.POST) > 0:
for v in list_var:
if v in request.POST:
try:
resultat.append(request.POST[v])
except:
return False, []
else:
return False, []
else:
return False, []
return True, resultat
@csrf_protect
def sinus_slider(request: HttpRequest) -> HttpResponse:
freq = 1
b_ok, val = get_arg_post(request, ['freq'])
if b_ok:
freq = float(val[0])
x = np.linspace(0, 10, 500)
y = np.sin(freq*x)+freq
source = ColumnDataSource(data=dict(x=x, y=y))
plot = figure(y_range=(-10, 10), width=400, height=400,title="Ma Courbe",name="Mes_donnees")
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6,name="Mon_sinus")
amp_slider = Slider(start=0.1, end=10, value=freq, step=.1, title="Amplitude")
callback = CustomJS(args=dict(source=source, amp=amp_slider),
code="""
var csrfToken = '';
var i=0;
var inputElems = document.querySelectorAll('input');
var reponse='';
for (i = 0; i < inputElems.length; ++i) {
if (inputElems[i].name === 'csrfmiddlewaretoken') {
csrfToken = inputElems[i].value;
break;
}
}
var xhr = new XMLHttpRequest();
xhr.open("POST", "/index/slider_change", true);
xhr.setRequestHeader('mode', 'same-origin');
var dataForm = new FormData();
dataForm.append('csrfmiddlewaretoken', csrfToken);
dataForm.append('freq', amp.value);
xhr.responseType = 'json';
xhr.onload = function() {
reponse = xhr.response
source.data.x = reponse['x'];
source.data.y = reponse['y'];
const plot = Bokeh.documents[0].get_model_by_name('Mes_donnees')
source.change.emit();
}
xhr.send(dataForm);
""")
amp_slider.js_on_change('value', callback)
layout = row(plot, column(amp_slider))
script1, div1 = components(layout, "Graphique")
pos = div1.find('data-root-id="')
id = int(div1[pos+14:pos+18])
#layout.update()
#script2, div2 = components(amp_slider, "slider freq")
#html2 = file_html(layout, CDN, "my plot")
#html2 = html2.replace("</head>","{% csrf_token %}</head>")
code_html = render(request,"sinus_slider.html", dict(script1=script1, div=div1))
return code_html
@csrf_protect
def sinus_slider_change(request: HttpRequest) -> HttpResponse:
freq = 1
b_ok, val = get_arg_post(request, ['freq'])
if b_ok:
freq = float(val[0])
x = np.linspace(0, 10, 500)
y = np.sin(freq*x)+freq
return JsonResponse(dict(x=x.tolist(),y=y.tolist()))
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | LBerger |