1

Is it possible to use js_on_change with bokeh to dynamically update text that is placed next to a plot?

For example, using this code snippet from a different question


from random import random
from bokeh.models import CustomJS, ColumnDataSource, Span
from bokeh.plotting import figure, output_file, show

output_file("callback.html")

x = [random() for x in range(500)]
y = [random() for y in range(500)]
color = ["navy"] * len(x)

s = ColumnDataSource(data=dict(x=x, y=y, color=color))
p = figure(plot_width=400,
           plot_height=400,
           tools="lasso_select",
           title="Select Here")
p.circle(x='x', y='y', color='color', size=8, source=s, alpha=0.4)

slope = Span(location=.5,
             dimension="width",
             line_alpha=.6,
             line_width=5)
p.add_layout(slope)

s.selected.js_on_change(
    'indices',
    CustomJS(args=dict(s=s, slope=slope),
             code="""       
        var inds = cb_obj.indices;
        
        if (inds.length == 0) {
            slope.location = 0.5
            return 
        }
        
        var total = 0;
        for (var i = 0; i < inds.length; i++) {
            total += s.data["y"][inds[i]]
        }
        var avg = total / inds.length;
        slope.location = avg;
    """))

show(p)

I'd like to include a text right of the figure that shows the value of the computed slope.location and updates whenever I select new points.

Thomas
  • 1,199
  • 1
  • 14
  • 29

1 Answers1

1

You can Use a Text Widget like PreText, place it on the right side of the plot e.g via layout and update the .text property of that widget in your JSCallback. Make sure you use the .toString() methode to assign the value.

from random import random
from bokeh.models import CustomJS, ColumnDataSource, Span, PreText
from bokeh.plotting import figure, output_file, show
from bokeh.layouts import layout

output_file("callback.html")

x = [random() for x in range(500)]
y = [random() for y in range(500)]
color = ["navy"] * len(x)

s = ColumnDataSource(data=dict(x=x, y=y, color=color))
p = figure(plot_width=400,
           plot_height=400,
           tools="lasso_select",
           title="Select Here")
p.circle(x='x', y='y', color='color', size=8, source=s, alpha=0.4)

slope = Span(location=.5,
             dimension="width",
             line_alpha=.6,
             line_width=5)
p.add_layout(slope)

slope_text = PreText(text='Slope_Text')

s.selected.js_on_change(
    'indices',
    CustomJS(args=dict(s=s, slope=slope, slope_text=slope_text),
             code="""       
        var inds = cb_obj.indices;
        
        if (inds.length == 0) {
            slope.location = 0.5
            return 
        }
        
        var total = 0;
        for (var i = 0; i < inds.length; i++) {
            total += s.data["y"][inds[i]]
        }
        var avg = total / inds.length;
        slope.location = avg;
        slope_text.text = avg.toString();
    """))

layout_ = layout([[p,slope_text]])
show(layout_)
Crysers
  • 455
  • 2
  • 13
  • Thank you for the answer! I tried this out and it works perfectly. Would it be possible to edit your answer to include another remark on the following: What if I want to use `box_zoom` instead of `lasso_select`? How would I get the mean of the points I zoomed in on? (I know this is somewhat a new question, but maybe you know the answer, too.) – Thomas Mar 23 '21 at 16:29
  • 1
    Do you want to select points via `box_select`? Therefor, you simply replace `lasso_select` with `box_select`. Or do you really want to calculate the average of exactly those points shown in the plot, after zooming in with `box_zoom`? – Crysers Mar 23 '21 at 19:13
  • No, not via `box_select`, but via zooming in with `box zoom` and "selecting" those points inside the zoom region. Does that make sense? – Thomas Mar 23 '21 at 20:00
  • 1
    Yes, it does. However, I don't think there is a built-in way to select only visible samples. What you could do is iterating over all samples in your `ColumnDataSource` and check if `x` and `y` values are within your visible ranges. Eg. check if `p.x_range.start < x < p.x_range.end` is True. (Same needs to be done for your y-values). If both conditions are met, store those indices and then calculate the average you need only of those samples. – Crysers Mar 24 '21 at 10:50