What's the correct JS code to change between NumeralTickFormatter and DatetimeTickFormatter axis types? And also to set their formatters?

I’ve got a figure where the axis value is changed via a Select and it’s all working nicely except I don’t know how to code the JS to tell it to change between axes types and formatting types.

Changing axis types and setting the formatters is easy in Python (only now I’ve figured it out lol):

  • x_axis_type="datetime" within p.figure(), and p.xaxis[0].formatter = DatetimeTickFormatter(minutes= "%M"), stated after p.figure().

  • don’t need to state axis type as linear is already default. And for the formatter: p.xaxis[0].formatter = NumeralTickFormatter(format="0,0").

But now I want to control these conditions in JS. x_axis.formatter.format = "0,0" worked well for changing the format in JS, although it doesn’t specify it needs to be a NumeralTickFormatter. I also thought x_axis.type = "datetime" might work to change the axis type in JS, but it seems it didn’t.

So in short:

  • what’s the JS code to set datetime / default axis types?
  • what’s the JS code to set the axes formatter type and the formatter value? x_axis.formatter.format = "0,0" worked fine in JS before changing axis types but I notice it doesn’t specify the formatter type. Perhaps the formatter type was already taken from p.xaxis[0].formatter = NumeralTickFormatter(format="0,0") in Python?

On a sidenote, is there anywhere we can find the JS equivalent code for things explained in the documentation?

Thanks again

I’ll answer only the last question for now, because it will automatically answer everything else. Or at the very least, the answer will provide enough information for you to figure everything else out.

is there anywhere we can find the JS equivalent code for things explained in the documentation?

Bokeh and BokehJS have roughly the same model API. The are some inconsistencies - some of them are documented, some are not (and probably because it was meant to be consistent but something went wrong). One main inconsistency is that in Python you can just change something (like ColumnDataSource.data), but in JS you have to also emit the relevant signal if you want other models to be notified of the change (like ColumnDataSource.change.emit()).
The best way to find out what you need to do on the JS side is to:

  • Find or come up with a relevant Python code
  • For each Bokeh model used in the code, find the corresponding model in BokehJS
  • For each attribute of the Bokeh models, find the corresponding attributes of the BokehJS models
  • Make sure you have figured out this correspondence

Then it’s just a matter of writing JS code that would do the same thing the Python code does. Keep in mind the discrepancies though - if they are not documented, it’s probably a bug that should be reported and fixed.

1 Like

I just wanted to clarify this. BokehJS can auto-magically detect actual full assignments to property values, e.g.

source.data = new_data // change.emit() is not needed

But it cannot detect “in place” changes to those values:

source.data['some_col'] = new_column //  change.emit() *is* needed

The Python side can and does detect the latter.

3 Likes

OK so that all makes sense except I don’t know Javascript. I chose to use JS instead of the Bokeh server because I wanted to learn how to create standalone files that didn’t interact with the broader internet, and at the time I thought entering a little code in a language I didn’t know would be easier than figuring out how servers work (spoiler: it wasn’t).

Could you please elaborate on how to do this?

  • For each Bokeh model used in the code, find the corresponding model in BokehJS
  • For each attribute of the Bokeh models, find the corresponding attributes of the BokehJS models

Here’s what I’ve drafted to change the axis type, the axis type formatter, and the formatter string. Perhaps someone could please point out where I’ve gone wrong?

Setting everything to datetime:
x_axis.type = "datetime"
x_axis.formatter = "DatetimeTickFormatter
x_axis.formatter.minutes = "%M"

Setting everything to linear (or default):
x_axis.type = "linear"
x_axis.formatter = "NumeralTickFormatter"
x_axis.formatter.format = "0,0"

Thanks again for all the help and for being so patient with me.

@tvkyq There are lots of things that are simple with a little JS, but this is not one of them. And if fact, it would not be so simple in Python either. Axes are some of the most complicated parts of a Bokeh plot, and one of the few places with a multi-level object graph. I can’t think of any time has ever asked about switching between datetime and another axis type on the same plot, so we have definitely not prioritized making that task simple or easy, and in fact I can’t even guarantee that it is possible. It may be the case that some things only respect their initial settings and don’t respond to changes

If you want my honest opinion: if you really need plots with wildly different axis types, I suggest trying something else entirely, namely create two entirely separate plots, toggle their visibility.

Or maybe create two axes and just switch their visible attribute? Never tried it though.

I FIGURED OUT A WORKAROUND!!

I’m after times in some kind of H:MM format along an axis so I don’t necessarily need a datetime axis if I can trick something else into showing it as H:MM. I was looking at the NumeralTickFormatter options and unexpectedly saw there’s a time section, where inputs of seconds are displayed in ‘00:00:00’ format. Supplying the same data in seconds in what I’m building is no problem, and I tested it out with this:

from bokeh.plotting import figure, show
from bokeh.models import CDSView, ColumnDataSource, CustomJS, NumeralTickFormatter
from bokeh.models.widgets import Select
from bokeh.layouts import layout

data = dict(Times=[8520, 3000, 2460, 9360, 9600, 4680, 1980, 9060, 8880, 10140, 2700, 4380, 10260, 5280, 6840],
        Apples=[1, 10, 1, 8, 10, 2, 0, 7, 2, 4, 7, 4, 1, 1, 9],
        Percentages=[0.21, 0.65, 0.86, 0.39, 0.32, 0.62, 0.46, 0.51, 0.17, 0.79, 0.64, 0.43, 0.54, 0.5, 0.47
])
data['x_active'] = data['Percentages']

source = ColumnDataSource(data=data)
view = CDSView(source=source)

p = figure()
p.circle('x_active', 'Apples', source=source, view=view, size=20)

x_select = Select(value="Percentages", options=["Percentages", "Times"])
x_select.js_on_change('value', CustomJS(args=dict(source=source,
                                                    x_select = x_select,
                                                    x_axis=p.xaxis[0]),
                                          code="""
  source.data['x_active'] = source.data[x_select.value]
  source.change.emit()
  time_options = ["Times"]
  if (time_options.includes(x_select.value)) {
    x_axis.formatter.format= "00:00:00"
    }
    else {
    x_axis.formatter.format= "0%"
    }
"""))

p.xaxis[0].formatter = NumeralTickFormatter(format="0%")
layout = layout([[x_select, p]])
show(layout)

Which gives me almost what I want - except tick mark intervals are displayed in seconds: I’d like HH-MM and I’m getting HH-MM-SS. Any ideas?

I suggest trying something else entirely, namely create two entirely separate plots, toggle their visibility.

Hmm that could work but that’ll involve a lot more coding than this workaround. Also what I’ve made is already starting to get a little laggy after ~3k rows for reasons I don’t understand and I don’t expect making another plot would speed things up.