My graph has a small footnote which displays the time at which the graph was last updated - basically, I fetch new data for the graph and cache it periodically, so it might be useful for users to know how “recent” the graph they’re looking at is.
My only problem is that my webserver runs on UTC time, which means that the time displayed on the end user’s webpages is UTC time. While it’s easy for me to format the output display string to state that the time is “UTC”, this is still confusing for many - and even if people DO understand the concept of UTC, it’s a pain to have to do the mental calculation back to their local timezone. So, ideally, I’d like to display this information in the user’s local time zone.
Unfortunately, I have no idea how to accomplish this - datetime series, when displayed along a graph axis, are automatically converted to local time; however, I don’t know how to do a local-time trick for something displayed as “text” - ie, as part of a “Title” object.
I think I would need some way to run javascript on the user’s browser (ie, JavaScript Date getTimezoneOffset() Method), and then communicate that information back to the server, so I can do apply the offset there, when formatting the text I will display in the “Title”.
Alternatively, I would need to insert some sort of placeholder text which would get automatically handled / replaced on the browser side.
Unfortunately, I have no idea how to do either of these. Any ideas? Thanks!
This is not true, unless I am misunderstanding your meaning. Datetime values in Bokeh are always assumed to be local time and are displayed exactly as given (i.e. timezone-naive, no transformations happen anywhere)
I would suggest updating a dummy CDS (e.g. in an invisible glyph) with the UTC time, and then have a CustomJS callback on the CDS data property that converts the UTC time to local time and then sets the Title.text proprerty.
You can find the docs and several examples of CustomJS callbacks here:
Ok! I think I’m getting somewhere with this, thank you! The only problem I’m having now is that I can triggerJS callbacks as soon as somebody clicks on an interactive element - such as a button or what not… but I can’t find a way to get javascript to fire when the graph first displays. That is - the javascript callbacks I attach while the plot is first getting built aren’t triggered, even if I change the thing the callback is attached to after - presumably because this is all happening before the “WebSocket connection opened” and “ServerConnection created” happens. Is there any trick to get a js callback to happen after the graph is built, without having to rely on the user “doing something”?
No guarantee this will work in your case. With that caveat, if you are running bokeh as a server and then embedding it in your webserver, you might be able to do something to trigger the callbacks in the server at an appropriate time.
@_jm - Unfortunately, this didn’t work - the on_server_loaded and on_session_created callbacks happen pretty early, even before the code for building out the UI.
Use an assignment so you can unregister that callback via remove_periodic_callback() after it has done what you need; or simply keep some other state within the callback so that it is effectively a no-op after it has done what you want.
@_jm - Ah, yes, add_periodic_callback did the trick!
Basically, I added a call that just kept modifying a value that I had a javascript on-change callback attached to. The on change would then convert the time to a local-time string. Once I detected that the javascript callback had run, I removed the periodic callback. Profit!
For those curious, my final solution looks something like this:
class MyBuilder(object):
UPDATE_FETCHING: "Updated: Fetching"
def build(self, doc):
...
self.updated = mdl.Title(text=self.UPDATE_FETCHING, align="right",
text_font_size="8pt", text_font_style="normal")
update_time_cds = mdl.ColumnDataSource(
data={'t': [self.model.last_update_time()]})
update_text_cb = mdl.CustomJS(args=dict(source=update_time_cds),
code="""
var localUpdateTime = new Date(source.data['t'][0])
cb_obj.text = "Updated: " + localUpdateTime.toString();
""")
self.updated.js_on_change('text', update_text_cb)
...
periods_holder = [0]
def modify_updated_time_placeholder(*args, **kwargs):
if not self.updated.text.startswith(self.UPDATE_FETCHING):
# print("periodic_callback removed...")
self.doc.remove_periodic_callback(periodic_callback)
return
# Just cycle between values with 1 to 3 trailing "." - this just
# makes sure that the value is always changed, so that as soon
# as javascript is "available", the javascript "on change" callback
# will fire.
num_periods = periods_holder[0]
# print("periodic_callback fired! {} - {} - {}"
# .format(num_periods, args, kwargs))
periods_holder[0] = num_periods + 1
ellipsis = '.' * (num_periods % 3 + 1)
self.updated.text = self.UPDATE_FETCHING + ellipsis
periodic_callback = self.doc.add_periodic_callback(
modify_updated_time_placeholder, 1000)