How to display "last updated" information in local time?

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,, 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:

Ah - my mistake, I misread - I thought that the offsetting behavior for timezoned datetimes was intentional, and that it was only unintentional for naive datetimes.

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.

Specifically, bokeh server has lifecycle hooks, one of which is on_session_created() which is called whenever a session is created. See the documentation here to understand the syntax and directory requirements, etc.

@_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.


I see. It makes sense that would be the case for on_server_loaded(), but I did not expect it to be the case for on_session_created().

One other thing that might work, again within the context of the on_session_created() lifecycle hook, add a periodic callback via the add_periodic_callback() document method described here.

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.

Or if you wanted something less robust / more empirical, you could add a timeout callback with a suitably long wait to run only once after a prescribed interval. That mechanism is the add_timeout_callback() method.

@_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),
            var localUpdateTime = new Date(['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...")

            # 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)