Simplify creating a plot with changing axis range type

I want to create a bar plot in which I can swap the data that is displayed. This includes changing the x-axis of the data, which may come in forms of both numbers and strings. Also the (relative!) width of the bars should stay consistent.

If I explicitly initialize the figure with the x_range of one of the range types, the plot shows just fine. So bokeh is able to display an x-axis with strings as FactorRange. But this is static, and breaks when trying to update it.

If I start as DataRange1d and then switch the contents of source.data from list of numbers to list of strings, bokeh breaks with the browser message “could not set initial ranges”.

In order to support both numbers and strings, I effectively have to turn all displayed data into strings, via major_label_overrides.

I also have to recalculate the width of the bars manually on each update, in order to reach visual consistency.

In my actual use case, there is always a “flicker” when switching the data: First the height of the bars change, then their width updates, then the labels of the x-axis are updated. This is no dealbreaker, but annoying nevertheless.

My method is also error prone when the data contains mix of ints and floats and the overrides do not get properly overridden.

Ideally I would like to ditch the two “workaround” code segements.

Is there an easier solution to my problem?
Or would I have to turn this into a feature request like “Add support for dynamic update of Range type (e.g. DataRange1d → FactorRange)”?

import numpy as np
import pandas as pd
from bokeh.models import ColumnDataSource, Select, BasicTicker
from bokeh.plotting import figure, curdoc
from bokeh.layouts import column

def update(attr, old, new):
    """Update the contents of the CDS and the xaxis overrides."""
    plot.xaxis.axis_label = dropdown.value

    source_dict = (df
                   .reset_index()
                   .rename(columns={dropdown.value: 'x'})
                   .to_dict(orient='list')
                   )

    # Begin workaround 1
    # "Translate" str data into numbers and later use
    # 'major_label_overrides' to display the desired text
    source_dict['x_orig'] = source_dict['x']
    if df[dropdown.value].dtype == 'object':
        source_dict['x'] = list(range(0, len(df.index)))
    # End workaround 1

    source.data = source_dict

    # Begin workaround 2
    # Update label overrides and bar width
    if df[dropdown.value].dtype != 'object':
        width = round(min(np.diff(df[dropdown.value])) * 0.8, 4)
        ticker_range = BasicTicker()

        # overrides = dict()  # Empty dict is not enough for a reset
        overrides = dict(zip(df[dropdown.value],
                              df[dropdown.value].astype('str')))

    else:
        width = 0.8
        ticker_range = list(range(0, len(df.index)))
        overrides = dict(zip(ticker_range, df[dropdown.value]))

    plot.xaxis.ticker = ticker_range
    plot.xaxis.major_label_overrides = overrides
    r1.glyph.update(width=width)
    # End workaround 2

# Define the test data
df = pd.DataFrame(data={'foo': [0, 1, 2, 3, 4],
                        'fol': [10, 20, 30, 40, 50],
                        'bar': ['a', 'b', 'c', 'd', 'e'],
                        'baz': ['v', 'w', 'x', 'y', 'z']},
                  index=pd.Index([5, 6, 7, 8, 9], name='value'))

source = ColumnDataSource()  # Create an empty CDS

plot = figure(
    # x_range=df['bar'],  # Allows setting a "static" correct range type
    tooltips=[("Value y", "@{value}"),
              ("Value x", "@{x_orig}"),
              ],
    # sizing_mode='stretch_both',  # works in v2.4.3, but breaks v3.2.0
    )
# print(plot.x_range)

r1 = plot.vbar(source=source, x='x', top='value', width=0.8)

start_selection = 'foo'
# start_selection = 'bar'

dropdown = Select(value=start_selection, options=df.columns.to_list(),
                  sizing_mode='scale_width')
dropdown.on_change('value', update)  # Link the callback

layout = column([plot, dropdown])  # Create a layout

update(0, 0, 0)  # Perform an initial update of the ColumnDataSource

curdoc().add_root(layout)
curdoc().title = "Swappable Axis Range Type"

By far it will be easier and better to just create two separate plots, and toggle their visibility, rather than try to rip out and replace the guts of one plot.