Is it possible to use javascript to fill histograms from linked scatterplots?

I’ve seen nice examples for linking a filled scatterplot to an empty one + datatable, filling the latter upon selection of the filled scatterplot. That was done with javascript.

What I’d like to do is histogram those selected points. When I make histograms, I use the example of using numpy.histogram and then feed a vbar. That requires a server app to go back to python.

Is there a way to fill a histogram from JavaScript, and hence be standalone?

Thanks!

Richard

It’s certainly possible, but you would have to do all the binning yourself. I am not personally aware of JS code I can point you at. If you know the bins you want up front, things will be much simpler (you can define the vbar plot up front, and binning is only a matter of looping of the data and incrementing the corresponding bin count). If you need dynamic bins, that’s going to be fairly more involved.

Thanks, Bryan. I can ask around of some of my colleagues if they’re aware of any binning code. They do a lot of data display in a different framework. Good question on the dynamic binning.

I was “afraid” the answer would be it’s on me. :wink:

That said, the linked plots example I saw may provide enough enthusiasm for me to add to my server app for a telescope project to select focal plane pixels and histogram values for those pixels. Something to add to the shelter in place todo list.

Richard

To close the loop - doing the binning in JS was pretty easy after all, and works great. Pretty cool :slight_smile:

Thanks again for the tip!

Richard

1 Like

Any hints on the JS to bin from a source that was made with select (I.e. lasso or box select)? :slight_smile:

Here’s a test script that does it: (pardon the ugly formatting - the boldface text were comments.

# Generate linked plots + TABLE displaying data + save button to export cvs of selected data

from random import random
import numpy as np

from bokeh.io import show

from bokeh.layouts import row
from bokeh.layouts import grid
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.models import Button  # for saving data
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn, Div
from bokeh.models import HoverTool
from bokeh.plotting import figure


# create data
x = [random() for x in range(500)]
y = [random() for y in range(500)]

# create first subplot
plot_width = 400
plot_height = 400

s1 = ColumnDataSource(data=dict(x=x, y=y))
fig01 = figure(
    plot_width=plot_width,
    plot_height=plot_height,
    tools=["box_select", "reset", "save"],
    title="Select Here",
)
fig01.circle("x", "y", source=s1, alpha=0.6)

# create second subplot
s2 = ColumnDataSource(data=dict(x=[], y=[]))

# demo smart error msg:  `box_zoom`, vs `BoxZoomTool`
fig02 = figure(
    plot_width=400,
    plot_height=400,
    x_range=(0, 1),
    y_range=(0, 1),
    tools=["box_zoom", "wheel_zoom", "reset", "save"],
    title="Watch Here",
)

fig02.circle("x", "y", source=s2, alpha=0.6, color="firebrick")

# demo smart error msg:  `box_zoom`, vs `BoxZoomTool`
fig03 = figure(
    plot_width=400,
    plot_height=400,
    x_range=(0, 1),
    tools=["box_zoom", "wheel_zoom", "reset", "save"],
    title="Watch Histogram Here",
)

hist, bins = np.histogram(x, bins=20)
source_hist = ColumnDataSource(data=dict(hist=hist, bins=bins[0:-1]))

fig03.vbar(top="hist", x="bins", width=0.05, source=source_hist, alpha=0.6, color="firebrick")

# create dynamic table of selected points
columns = [
    TableColumn(field="x", title="X axis"),
    TableColumn(field="y", title="Y axis"),
]

table = DataTable(
    source=s2,
    columns=columns,
    width=400,
    height=600,
    sortable=True,
    selectable=True,
    editable=True,
)

# fancy javascript to link subplots
# js pushes selected points into ColumnDataSource of 2nd plot
# inspiration for this from a few sources:
# credit: https://stackoverflow.com/users/1097752/iolsmit via: https://stackoverflow.com/questions/48982260/bokeh-lasso-select-to-table-update
# credit: https://stackoverflow.com/users/8412027/joris via: https://stackoverflow.com/questions/34164587/get-selected-data-contained-within-box-select-tool-in-bokeh

# https://stackoverflow.com/questions/37445495/binning-an-array-in-javascript-for-a-histogram

s1.selected.js_on_change(
    "indices",
    CustomJS(
        args=dict(s1=s1, s2=s2, s3=source_hist, table=table),
        code="""
        var inds = cb_obj.indices;
        var d1 = s1.data;
        var d2 = s2.data;
        var dhist = s3.data;
        var bhist = dhist['bins'];
        // get the selected items and push them into the s2 CDS
        d2['x'] = []
        d2['y'] = []
        for (var i = 0; i < inds.length; i++) {
            d2['x'].push(d1['x'][inds[i]])
            d2['y'].push(d1['y'][inds[i]])
        }
        s2.change.emit();
        table.change.emit();

        // grab the x values to re-histogram
        var dh = []
        for (var i = 0; i < inds.length; i++) {
            dh.push(d1['x'][inds[i]])
        }
        // get the histogram numbins, bin width from initial histogram
        var bins = [];
        var binCount = 0;
        var interval = bhist[1] - bhist[0];
        var numOfBuckets = bhist.length;
        
        //Setup Bins
        for(var i = 0; i < numOfBuckets; i += interval){
          bins.push({
            binNum: binCount,
            minNum: bhist[i],
            maxNum: bhist[i] + interval,
            count: 0
          })
          binCount++;
        }
        //Loop through data and add to bin's count
        for (var i = 0; i < dh.length; i++){
          var item = dh[i];
          var a = (item - bhist[0])/interval;
          var j = Math.floor(a);
          var bin = bins[j];
          bin.count++;
          }  
        // push the bin values back into the original histogram]]]}
        dhist['hist'] = []
        for (var j = 0; j < 20; j++) {
            dhist['hist'].push(bins[j].count)
        }
        s3.change.emit();
    """,
    ),
)

# create save button - saves selected datapoints to text file onbutton
# inspriation for this code:
# credit:  https://stackoverflow.com/questions/31824124/is-there-a-way-to-save-bokeh-data-table-content
# note: savebutton line `var out = "x, y\\n";` defines the header of the exported file, helpful to have a header for downstream processing

savebutton = Button(label="Save", button_type="success")
callback = CustomJS(
    args=dict(source_data=s1),
    code="""
        var inds = source_data.selected.indices;
        var data = source_data.data;
        var out = "x, y\\n";
        for (i = 0; i < inds.length; i++) {
            out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
        }
        var file = new Blob([out], {type: 'text/plain'});
        var elem = window.document.createElement('a');
        elem.href = window.URL.createObjectURL(file);
        elem.download = 'selected-data.txt';
        document.body.appendChild(elem);
        elem.click();
        document.body.removeChild(elem);
        """,
)
savebutton.js_on_click(callback)

# add Hover tool
# define what is displayed in the tooltip
tooltips = [
    ("X:", "@x"),
    ("Y:", "@y"),
    ("static text", "static text"),
]

fig02.add_tools(HoverTool(tooltips=tooltips))

# display results
# demo linked plots
# demo zooms and reset
# demo hover tool
# demo table
# demo save selected results to file

get_d3 = '<script src="https://d3js.org/d3.v5.min.js"></script>'
d3_div = Div(text=get_d3)


layout = grid([fig01, fig02, fig03, table, savebutton], ncols=3)

show(layout)

Hi @richardxdubois please edit your post to use code formatting so that the code is intelligible (either with the </> icon on the editing toolbar, or triple backtick ``` fences around the code blocks)

Thanks for the reminder. Done!

I have successfully done this within bokeh by “importing” d3js into the script portion of the bokeh components. With that added, I get access to the d3js library in my callbacks, which I’ve used to do dynamic binning etc with standalone html/no server involved.