Search code examples
pythonbokeh

bokeh multiple charts linking behaviour


I have two charts. When I zoom in on the top or bottom chart the x-axis both update and show the same date range which is great. The problem is the y-axis on both charts is quite different so when say I zoom in on the top chart both the x & y axis's scale accordingly. On the bottom chart though the x-axis scales accordingly but the y-axis doesn't. I can't use the y_range=fig.y_range as the y ranges are very different.

Is it possible that when I zoom in on the top chart for the bottom chart's y-axis to be scaled accordingly when both charts have different y-axis ranges?

update - what I mean by accordingly

Say my x-axis goes from 1st Jan 2020 to 31st December 2020. Now say I zoom in on say on the whole of July 2020 on the top chart using the inbuilt tools, the bottom chart's x-axis adjusts accordingly automatically, i.e. the x-axis is now zoomed in on the whole of July on both charts. This work brilliantly by using the line x_range=fig.x_range. Both charts share the same x-axis.

However their y-axis are different so I can't use y_range=fig.y_range.

So what I want to do is when say I zoom in on the top chart & both the x & y axis's automatically re-scale. I want the bottom chart y-axis to also rescale (the x-axis as already mentioned do this automatically).

my code below

cds = ColumnDataSource(data=df)   

fig = figure(plot_width=W_PLOT, plot_height=H_PLOT, 
             tools=TOOLS,
             x_axis_type="datetime",
             title=name,
             toolbar_location='above')

# lets add a moving average
fig.line(x='time_stamp', y='ma_20', source=cds, legend_label='MA 20')

fig_ind = figure(plot_width=W_PLOT, plot_height=H_PLOT_IND,
                 tools=TOOLS,
                 x_axis_type="datetime",
                 x_range=fig.x_range)

fig_ind.line(x='time_stamp', y='ma_100', source=cds, legend_label='MA 100')

show(gridplot([[fig],[fig_ind]]))

Solution

  • Here's how to achieve this using a CustomJS callback on the common X range:

    from bokeh.models.ranges import DataRange1d
    from bokeh.layouts import column
    from bokeh.models.sources import ColumnDataSource
    from bokeh.models import CustomJS
    
    import pandas as pd
    import numpy as np
    
    
    df = pd.DataFrame(
        {
            'fig1_y': np.linspace(0, 100, 100),
            'fig2_y': np.linspace(0, 1000, 100),
            'common_x': pd.date_range(
                start='2020-01-01',
                end='2021-01-01',
                periods=100
            )
        }
    )
    
    cds = ColumnDataSource(data=df)
    
    common_x_range = DataRange1d(bounds='auto')
    
    fig = figure(
        plot_width=500,
        plot_height=200,
        x_axis_type="datetime",
        x_range=common_x_range
    )
    
    fig.line(
        x='common_x',
        y='fig1_y',
        source=cds,
        legend_label='MA 20'
    )
    
    fig2 = figure(
        plot_width=500,
        plot_height=200,
        x_axis_type="datetime",
        x_range=common_x_range,
        y_range=DataRange1d(bounds='auto')
    )
    
    fig2.line(
        x='common_x',
        y='fig2_y',
        source=cds,
        legend_label='MA 100'
    )
    
    
    callback = CustomJS(
        args={
            'y_range': fig2.y_range,
            'source': cds
        }, code='''
        var x_data = source.data.common_x,
            fig2_y = source.data.fig2_y,
            start = cb_obj.start,
            end = cb_obj.end,
            min = Infinity,
            max = -Infinity;
    
        for (var i=0; i < x_data.length; ++i) {
            if (start <= x_data[i] && x_data[i] <= end) {
                max = Math.max(fig2_y[i], max);
                min = Math.min(fig2_y[i], min);
            }
        }
        
        y_range.start = min
        y_range.end = max
    ''')
    common_x_range.js_on_change('start', callback)
    common_x_range.js_on_change('end', callback)
    
    show(column([fig,fig2]))