Visualize a 3D array with bokeh

In my work we produce 3D arrays of data (2D maps of spectra). To visualize the data I created a map class to prepare data for plotting with bokeh library. A map object is created by:

rm = RamanMap()

The map class slices the 3D array into multiple 2D maps. A bokeh Slider is used to switch between the maps/slices:

slider = Slider(start=0, 
                end=rm.n_peaks-1,   # the number of slices
                value=0, step=1, 
                title='Select a peak')
slider.on_change('value', peakSelector)

as well as to highlight the maps(slice) position on a full spectrum position using a Span:

span = Span(location=spectra_src.data['x'].mean(), 
            dimension='height', 
            line_color='red',
            line_width=1)

The slider callback function is defined as follows:

def peakSelector(attr, old, new):
    n = slider.value
    rm.sliceMap(n)   # choose a different slice using a map method
    spectra_src.data = rm.sliced_spectra   # one kind of map
    maps_src.data = rm.sliced_maps   # another kind of map
    span.location = spectra_src.data['x'].mean()
    g_mapper.low  = maps_src.data['g_max'][0].min()
    g_mapper.high = maps_src.data['g_max'][0].max()
    int_mapper.low = maps_src.data['intensities'][0].min()
    int_mapper.high = maps_src.data['intensities'][0].max()

g_mapper and int_mapper are LinearColorMapper objects, their upper and lower values need to be changed with the selected map.

The maps are created using a function that returns image and mapper with the following code:

image = figure(...)
image.image(image=image_name ,source = source, ......)
mapper_low  = source.data[image_name][0].min()
mapper_high = source.data[image_name][0].max()
mapper = LinearColorMapper(.... low=mapper_low, high=mapper_high)
color_bar = ColorBar(color_mapper=mapper, location=(0,0))
image.add_layout(color_bar, 'right')
image.js_on_event(
    events.Tap, 
    update_input_multi(user_input, spectra_visible_multi, spectra_available_multi)
)
image.rect('x', 'y', 1, 1, source=user_input, fill_color=None, line_width=2, line_color='black')

The last two lines image.rect and image.js_on_event() link several ColumnDataSources allowing one click on any of three shown maps to affect all other maps, adding a new rect to highlight the selected pixel, and to show the spectrum from the highlighted pixel on another figure.
Watch the video to understand how this works:

The CustomJS function takes the index of the tapped pixel and and uses it to

  1. Draw a new rectangle on all maps, since they share the same input,
  2. add a spectrum from available to visible ColumnDataSources
def update_input_multi(user_source, visible, available):
    return CustomJS(
        args=dict(user_source=user_source, visible=visible, available=available), 
        code ='''
        var data = user_source.data;
        var spectra = visible.data;
        var all_spectra = available.data;
        //var x = parseInt(cb_obj['x']); //updated according to reply by Bryan
        //var y = parseInt(cb_obj['y']);
        const x = parseInt(cb_obj.x);
        const y = parseInt(cb_obj.y);
      //avoid input from tapping outside the edges 
        if((x >= 0) && (y >= 0) && (x < 50) && (y < 50)) { 
            const z = (x+1)+(y*50);
            // check if the only available data is (0,0) then replace it with the first data point the user clicks
            if((data['x'].length == 1) && (data['x'] == 0.5) && (data['y'] == 0.5)) {
                // if this is the first tap
                data['x'] = [x+0.5];
                data['y'] = [y+0.5];
                //data values are shifted 0.5 to center rectangles on index
                spectra['ys'] = [all_spectra['ys'][z-1]];
                visible.change.emit();
                user_source.change.emit();
            }  else  {
                data['x'].push(x+0.5);
                data['y'].push(y+0.5);
                var wave = spectra['waves']['0'];
                spectra['ys'].push(all_spectra['ys'][z-1]);
                spectra['waves'].push(wave);
                visible.change.emit();
                user_source.change.emit();
            }
        }
    ''')

Notice that I had to add 0.5 to the indices of the rectangles since I didn’t know how to draw rectangles from theirs centers.

I still do not know how to plot the selected spectra in multiple colors. It will be a subject of a future update.

GitHub Repository

1 Like

Note: I tweaked the formatting slightly to accomodate narrower/mobile displays better, I hope that’s OK!

This is really fantastic! In a past life I was a physics grad student, and spent some time in a beam physics lab, so I love seeing Bokeh applied in these sorts of areas. How many people will use this tool?

In the spirit of making this category a friendly place for constructive sharing, I’ll also offer just a couple of quick thoughts:

  • rect is always center-anchored at x and y with a width and height. If the coordinates you already have are for corners, a CustomJSTransform applied to x and y in the call to rect might help you simplify the CustsomJS
  • You can save a little typing by accessing attributes as dots and leaving off the unecessary semicolons:
    const y = parseInt(cb_obj.y)
    
    It’s also defintiely possible to use const and I’d suggest applying it wherever you can!

Thanks for sharing @Amjad_Al_Taleb!!

Thank you for the formatting the code, it’s much easier to read, and for the tips.
I switched to const y = parseInt(cb_obj.y); which works perfectly and looks nicer.

I’ll look into the CustomJSTransform method. I know so little JS and try to avoid it as much as possible, but apparently there’s no escape learning it since I found out about bokeh two weeks ago.

This tool will be used by 3-4 people in my lab and whoever finds the code on GitHub is welcome to use it as well.

Thank you for this awesome library!

2 Likes