Trying to use a rangetool with categorical data and running into problems

I have some data I want to display as a bar chart. There are over 100 categories, so I want to use a RangeTool to show the whole range and select a subset to show with more detail on a main plot.

RangeTools work with continuous ranges, but I’ve used them with categorical data (in a different way) before. This time I’m stuck.

I’m using a CustomJS callback to update my main plot. I know I have to update the x_range. Because it’s categorical data, I’m assuming if I update the x_range.factors attribute, everything should work well. Problem is, that’s not working. I can’t figure out how to update the categorical range on the main plot.

Here’s my code, with one line of JavaScript commented out (this line didn’t work and froze the range selector).

Anyone got any ideas?

import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, RangeTool, CustomJS
from bokeh.io import show
from bokeh.layouts import column

data = [
    {'name': 'a', 'amount': 1},
    {'name': 'b', 'amount': 2},
    {'name': 'c', 'amount': 3},
    {'name': 'd', 'amount': 4},
    {'name': 'e', 'amount': 5},
    {'name': 'f', 'amount': 6},
    {'name': 'g', 'amount': 7},
    {'name': 'h', 'amount': 8},
    {'name': 'i', 'amount': 9},
    {'name': 'j', 'amount': 10},
    {'name': 'k', 'amount': 11},
    {'name': 'l', 'amount': 12},
    {'name': 'm', 'amount': 13},
    {'name': 'n', 'amount': 14},
    {'name': 'o', 'amount': 15},
    {'name': 'p', 'amount': 16},
    {'name': 'q', 'amount': 17},
    {'name': 'r', 'amount': 18},
    {'name': 's', 'amount': 19},
    {'name': 't', 'amount': 20},
    {'name': 'u', 'amount': 21},
    {'name': 'v', 'amount': 22},
    {'name': 'w', 'amount': 23},
    {'name': 'x', 'amount': 24},
    {'name': 'y', 'amount': 25},
    {'name': 'z', 'amount': 26}
]

data_df = pd.DataFrame(data)

cds = ColumnDataSource(data_df)

main_plot = figure(
    title="Select the name",
    x_axis_label="Name",
    x_range=data_df.iloc[0:5]['name'].to_list(),
    height=200,
)

main_plot.vbar(
    x='name',
    top='amount',
    width=0.8,
    source=cds,
)

overview_plot = figure(
    title="Select the range",
    x_axis_label="Name",
    x_range=data_df['name'].to_list(),
    height=50,
)

overview_plot.xaxis.visible = False
overview_plot.yaxis.visible = False

overview_plot.vbar(
    x='name',
    top='amount',
    width=0.8,
    source=cds,
)

range_tool = RangeTool(x_range=main_plot.x_range)
range_tool.overlay.fill_color = "navy"
range_tool.overlay.fill_alpha = 0.2

overview_plot.add_tools(range_tool)

callback_range_code = CustomJS(
    args=dict(mainPlot=main_plot,
              cds=cds),
    code="""
        // Quantize the start and end values to the nearest integer.
        // Keep selection width constant.

        // Quantize the start and end positions.
        cb_obj.setv({
            'start': Math.round(cb_obj.start),
            'end': Math.round(cb_obj.end)
        });

        // Update the main plot x_range
        mainPlot.x_range.setv({
            'start': cb_obj.start,
            'end': cb_obj.end,
            //'factors': Array.from(cds.data.name.slice(cb_obj.start, cb_obj.end))
        });

        console.log('here')
    """
)

main_plot.x_range.js_on_change('start', callback_range_code)
main_plot.x_range.js_on_change('end', callback_range_code)

show(column(main_plot, overview_plot))

I solved this by using a dummy intermediary figure. Here’s the code.

import pandas as pd
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, RangeTool, CustomJS, Range1d
from bokeh.io import show
from bokeh.layouts import column

data = [
    {'name': 'a', 'amount': 1, 'index': 0},
    {'name': 'b', 'amount': 2, 'index': 1},
    {'name': 'c', 'amount': 3, 'index': 2},
    {'name': 'd', 'amount': 4, 'index': 3},
    {'name': 'e', 'amount': 5, 'index': 4},
    {'name': 'f', 'amount': 6, 'index': 5},
    {'name': 'g', 'amount': 7, 'index': 6},
    {'name': 'h', 'amount': 8, 'index': 7},
    {'name': 'i', 'amount': 9, 'index': 8},
    {'name': 'j', 'amount': 10, 'index': 9},
    {'name': 'k', 'amount': 11, 'index': 10},
    {'name': 'l', 'amount': 12, 'index': 11},
    {'name': 'm', 'amount': 13, 'index': 12},
    {'name': 'n', 'amount': 14, 'index': 13},
    {'name': 'o', 'amount': 15, 'index': 14},
    {'name': 'p', 'amount': 16, 'index': 15},
    {'name': 'q', 'amount': 17, 'index': 16},
    {'name': 'r', 'amount': 18, 'index': 17},
    {'name': 's', 'amount': 19, 'index': 18},
    {'name': 't', 'amount': 20, 'index': 19},
    {'name': 'u', 'amount': 21, 'index': 20},
    {'name': 'v', 'amount': 22, 'index': 21},
    {'name': 'w', 'amount': 23, 'index': 22},
    {'name': 'x', 'amount': 24, 'index': 23},
    {'name': 'y', 'amount': 25, 'index': 24},
    {'name': 'z', 'amount': 26, 'index': 25}
]

data_df = pd.DataFrame(data)

cds = ColumnDataSource(data_df)

main_plot = figure(
    title="Select the name",
    x_range=data_df.iloc[0:5]['name'].to_list(),
    x_axis_label="Name",
    height=200,
)

main_plot.vbar(
    x='name',
    top='amount',
    width=0.8,
    source=cds,
)

dummy_plot = figure(
    title="Dummy",
    x_range=Range1d(start=0, end=5),
    height=200,
)

dummy_plot.vbar(
    x='index',
    top='amount',
    width=0.8,
    source=cds,
)

overview_plot = figure(
    title="Select the range",
    x_axis_label="Name",
    x_range=data_df['name'].to_list(),
    height=50,
)

overview_plot.xaxis.visible = False
overview_plot.yaxis.visible = False

overview_plot.vbar(
    x='name',
    top='amount',
    width=0.8,
    source=cds,
)

range_tool = RangeTool(x_range=dummy_plot.x_range)
range_tool.overlay.fill_color = "navy"
range_tool.overlay.fill_alpha = 0.2

overview_plot.add_tools(range_tool)

range_width = 5

callback_range_code = CustomJS(
    args=dict(mainPlot=main_plot,
              cds=cds,
              rangeWidth=range_width),
    code="""
        // Quantize the start and end values to the nearest integer.
        // Keep selection width constant.

        // Quantize the start and end positions.
        cb_obj.setv({
            'start': Math.round(cb_obj.start),
            'end': Math.round(cb_obj.end)
        });

        // Keep the selection width constant
        const range_width = rangeWidth;

        // Get current start and end positions
        let start = cb_obj.start;
        let end = cb_obj.end;
        let current_width = end - start;

        // Only adjust if the width has changed significantly
        // This prevents infinite recursion while maintaining range width
        if (Math.abs(current_width - range_width) > 0.001) {
            // Calculate center point and adjust range to maintain width
            let center = (start + end) / 2;
            let new_start = Math.round(center - range_width / 2);
            let new_end = Math.round(center + range_width / 2);

            // Update the range while avoiding infinite recursion
            cb_obj.setv({
                'start': new_start,
                'end': new_end
            });
        }

        // Update the main plot x_range
        mainPlot.x_range.setv({
            'factors': cds.data['name'].slice(
                Math.round(cb_obj.start), Math.round(cb_obj.end)
            )
        });
    """
)

dummy_plot.x_range.js_on_change('start', callback_range_code)
dummy_plot.x_range.js_on_change('end', callback_range_code)

show(column(main_plot, dummy_plot, overview_plot))

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.