bokeh: Can't change type of figure.x_range dynamically

ALL software version info (bokeh, python, notebook, OS, browser, any other relevant packages)

$ bokeh info
Python version      :  3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24)
IPython version     :  7.1.1
Tornado version     :  5.1.1
Bokeh version       :  1.0.1
BokehJS static path :  /path/to/my/python3.7/site-packages/bokeh/server/static
node.js version     :  v9.7.1
npm version         :  5.6.0

Description of expected behavior and the observed behavior

I am trying to implement a plot where the user can select a column from a dropdown and see the histogram of values in the column. Some columns may be categorical, and some are continuous, so I’ve using a vbar and setting the width accordingly. However, this means that I sometimes need a DataRange1d, and sometimes a FactorRange. Changing my figure’s x_range to a FactorRange causes my plot to be reset and not show any output. There is no error, the glyphs just disappear.

Complete, minimal, self-contained example code that reproduces the issue

This is about as minimal as I could get it, sorry.

from bokeh.plotting import Figure, ColumnDataSource
from bokeh.models.widgets import Dropdown
from bokeh.models.ranges import DataRange1d, FactorRange
from bokeh.layouts import row
from bokeh.io import curdoc

import pandas as pd
import numpy as np


def _col_stats(col,):
    """Value counts or histogram of column"""
    is_str = isinstance(col.values[0], str)

    if is_str:
        counts = col.value_counts().sort_index()
        hist = counts.values
        edges = np.array(counts.index)

    else:
        hist, edges = np.histogram(col, bins="auto")

    return hist, edges


def _hist_source_dict(hist, edges):
    if isinstance(edges[0], str):
        return {"top": hist, "x": edges, "width": 0.8 * np.ones_like(edges)}

    else:
        w = np.diff(edges)
        x = np.array(edges[:-1]) + w / 2
        return {"top": hist, "x": x, "width": w}


def _update_histogram(figure, source, hist, edges, name=""):
    """Why doesn't this work?"""
    if isinstance(edges[0], str):
        figure.x_range = FactorRange(factors=edges)
    else:
        figure.x_range = DataRange1d()

    source.data = _hist_source_dict(hist, edges)

    if name:
        figure.title.text = name


df = pd.DataFrame(
    {"a": [1, 2, 3, 4, 3, 1, 2, 3, 4, 1], "b": list("abcdcbaeda")}
)
columns = df.columns

# initial values
first_col = columns[0]
first_hist, first_edges = _col_stats(df[first_col])

# set up data sources
hist_source = ColumnDataSource(data=_hist_source_dict(first_hist, first_edges))

# set up dashboard components
plot = Figure(
    title=first_col,
    tools="xpan,xwheel_zoom,box_zoom,reset",
    active_drag="xpan",
    active_scroll="xwheel_zoom",
)

plot.vbar(top="top", x="x", width="width", bottom=0, source=hist_source)

dropdown = Dropdown(
    label="Column", menu=[(col, col) for col in df.columns], value=first_col
)

# configure callbacks
def dropdown_callback(attr, old, new):
    global df, plot, hist_source
    hist, edges = _col_stats(df[new])
    _update_histogram(plot, hist_source, hist, edges, new)


dropdown.on_change("value", dropdown_callback)

# Position elements in document
root = row(plot, dropdown)
curdoc().add_root(root)

If you save this and run it with bokeh serve, selecting ‘b’ from the dropdown will blank out the plot, but there is no error.

Screenshots or screencasts of the bug in action

Continuous column: image

Categorical column: image

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Comments: 15 (6 by maintainers)

Most upvoted comments

Not quite shure if the issue has been solved, but a way to get around is by overwriting the factor attribute of the figure.x_renge object. so for your particular case will look something like :

figure.x_range.factors = edges

This on the docs is not completely clear and its necessary an example that covers this figure() related modifications made dynamically, i’ll be glad to share an example of this.

I’ve been attempting to update fig{x,y}_range.{start,end} in a callback function in a mapping application, to show different state maps in the correct aspect ratio. I update the data source for the figure, then directly update the x_range.start, etc. Running this under my bokeh server (2.0.1) this works, but randomly fails. Sometimes all limits are refreshed and the map displays properly. Other times, one or more limit fails to update, leading to a distorted map or no visible rendering of the state. This may be related to the problem reported by @mattpap above. It appears to be unresolved.

I’m currently linking/unlinking my x-ranges with a button with two functions:

def unlink_func():
    s1.x_range.js_property_callbacks.pop('change:start')
    s1.x_range.js_property_callbacks.pop('change:end')

    s2.x_range.js_property_callbacks.pop('change:start')
    s2.x_range.js_property_callbacks.pop('change:end')

def link_func():
    s1.x_range.js_link('start', s2.x_range, 'start')
    s1.x_range.js_link('end', s2.x_range, 'end')

    s2.x_range.js_link('start', s1.x_range, 'start')
    s2.x_range.js_link('end', s1.x_range, 'end')

Where s1 and s2 are my two figures. Is this the way to do it or is there a ‘better’ way?