Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

interactive scatter highlight in bokeh

Tags:

python

bokeh

I am trying to visualise sensor output in relation to its path.
I plot path as scatter in one figure and some range of signal amplitude in the second figure. I need to visualise (highlight) a path point at which the particular reading was taken.

I started using bokeh as a backend and in general, got very good results with visualisations I need. But I am stuck on this particular interaction.

I would like to have some marker like a vertical line anchored in the middle of the figure. When I move/scroll the amplitude plot (the bottom one), I would like to highlight the point on the path plot where the reading closest to the marker line was taken.

The example code:
(I would like to anchor the marker line and add interaction between the red dot and the vertical line taking an index of the signal, which is not implemented.)

import numpy as np
import pandas as pd
from bokeh.io import output_file
from bokeh.models import ColumnDataSource, HoverTool, Span
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot

output_file('interactive_path_sig.html', title="interactive path")

class InteractivePath():
    def __init__(self):
        x = np.arange(0, 1000, 0.5)
        self.df = pd.DataFrame({"x": x,
                                "y": np.sin(x),
                                "z": np.cos(x)})
        self.source = ColumnDataSource(self.df)

    def plot_path(self):
        plt = figure(title = "Sensor Path")
        plt.scatter(x="x", y="y",source=self.source, 
                    line_color=None, size = 6)
        # TODO implement interaction instead of hard coded index
        index=500    # this is where I think I need to create working callback
        print("x={}, y={}".format(self.df['x'][index], self.df['y'][index]))
        plt.circle(x=self.df['x'][index], y=self.df['y'][index], 
                   fill_color="red", size=15)
        hover = HoverTool()
        hover.tooltips=[("index", "@index"), ("senosr","@z")]
        plt.add_tools(hover)
        return plt

    def plot_signal(self):
        plt = figure(x_range=(450, 550), title="Signal Amplitude")
        plt.line(x="index", y="z", source=self.source, line_color="black", line_width=2)
        # TODO implement interaction instead of hard coded index
        index = 500  # I think this needs emit some singal to other plot
        vline = Span(location=index, dimension='height', line_color='red', line_width=3)
        plt.renderers.extend([vline])
        return plt

    def get_grid(self):
        """ place visualisation in a grid and display"""
        grid = gridplot([[self.plot_path()], [self.plot_signal()]], 
                 sizing_mode='stretch_both',)
        return grid

    def vis_main(self):
        """ use all visualisations"""
        show(self.get_grid())

if __name__=="__main__":
    vis = InteractivePath()
    vis.vis_main()

enter image description here enter image description here

like image 781
tomasz74 Avatar asked Nov 29 '25 16:11

tomasz74


1 Answers

So a few pointers:

  • I think you'll want both of those plots in the same method because the columndatasource is common between them, and you can set CustomJS behaviors between them if they're in the same scope.
  • The index that you're using already exists within your self.df which will be easier to interact with once it's on your plot, since you can handle it with JS plot behavior instead of going back to a python variable and reloading data.
  • Instead of drawing a new glyph for your 'highlighted' point, consider using the 'hover' or 'selected' functionality built in. hover_color='red' for example could replace drawing and moving another class of glyph. If you want to leave statically selected so you can generate a nice report without a mouse in a screenshot, defining a callback using the built-in selected property of ColumnDataSource

I can post some actual code blocks with more specific examples, but if any of these points is a hard stop for your actual use case, it'll drive solution.


Edit:

So I got pretty close using one class method - the issue is being able to edit the second plot from the first method, not the actual change to the ColumnDataSource itself.

def plot_it(self):
    self.plot_signal = figure(x_range=(450, 550), y_range=(-1, 1), title='signal')
    self.plot_signal.line(x='index', y='z', source=self.source)
    self.plot_signal.segment(x0=500, y0=-2, x1=500, y1=2, source=self.source)

    self.plot_path = figure(title='sensor')
    self.plot_path.scatter(x='x', y='y', source=self.source, hover_color='red')

    jscode='''
    var data = source.data;
    var plot_signal = plot_signal;
    var index = cb_data.index['1d'].indices;
    var xmin = 0;
    var xmax = 0;
    if (index > 0) {
        xmin = index[0] - 50;
        xmax = index[0] + 50;
        plot_signal.x_range.end = xmax;
        plot_signal.x_range.start = xmin;
        plot_signal.change.emit();
    }

    hover_callback = CustomJS(args=dict(source=self.source, plot_signal=self.plot_signal), code=jscode)
    hover.tooltips = [('index', '@index'), ('sensor', '@z')]
    self.plot_path.add_tools(hover)

def get_grid(self):
    self.plot_it()
    grid = gridplot([[self.plot_path], [self.plot_signal]])
    return grid

That should do everything but move the line segment. I couldn't find the segment naming convention to add plot_signal.SOMEOBJECT.x0 and .x1 but it would just get added to the if (index > 0) block just like using index[0]. I took some of the style options out because I'm transcribing from another computer.

This question on moving a line segment might give you the syntax on the segment JSON object.

like image 61
rumble_bumbles Avatar answered Dec 02 '25 05:12

rumble_bumbles



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!