CustomJS in BokehJS to sort source data after PointDrawTool in Javascript

Hi,

I would like to sort the source data after adding or editing points with PointDrawTool directly in Javascript. In Python, I can use similar to below and it will only emit the change after I create/edit a point with the tool:

source.js_on_change('data', CustomJS(args = dict(source = source), code="""
// ... code
        source.change.emit();

"""))

However, I cannot figure out how to do it with Javascript directly. I tried change.connect but the source changes as I move the points which will mess up other points. Another way is to listen for the mouseup but I don’t think it is a good approach and it doesn’t sort out new points. Below is an example of my attempts.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">

  <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.0.2.min.js"></script>
  <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-api-3.0.2.min.js"></script>  

</head>
<body>
  <div id="plot"></div>

  <script>

    // Import necessary modules and tools
    const { Plot, LinearAxis, Circle, ColumnDataSource, PointDrawTool, Column } = Bokeh;

    // Create a ColumnDataSource
    const source = new ColumnDataSource({
    data: { x: [1, 2, 3, 4, 5], y: [2, 4, 1, 3, 5] }
    });


    // Create plot, axis, and renderer
    const plot = Bokeh.Plotting.figure({
        });
    const circle_renderer = plot.circle({ field: "x" }, { field: "y" }, {source: source, fill_color:"white", size:8});;

    plot.line({ field: "x" }, { field: "y" }, {source: source, line_width: 2});


    // Create the PointDrawTool and add to the plot
    const point_draw_tool = new PointDrawTool({
    renderers: [circle_renderer],
    empty_value: 'black'
    });
    plot.add_tools(point_draw_tool);
    plot.toolbar.active_tap = point_draw_tool;

    // Function to sort source data
    function sortData() {
    const data = source.data;
    const sortedIndices = Array.from(Array(data.x.length).keys()).sort((a, b) => data.x[a] - data.x[b]);
    const sortedX = sortedIndices.map(i => data.x[i]);
    const sortedY = sortedIndices.map(i => data.y[i]);
    source.data = { x: sortedX, y: sortedY };
    }

    // Attach a mouseup event listener to the document
    document.addEventListener('mouseup', function() {
    sortData();
    });

    //---- Other attempts for callback
    //source.connect(source.change, sortData);
    //source.change.connect((_args, source) => sortData());
    //source.js_event_callbacks['change']=function(){sortData;};

    // Create a Column layout and display it
    const layout = new Column({ children: [plot] });
    Bokeh.Plotting.show(layout, '#plot');

  </script>
</body>
</html>

If I understand your question, you want to sort the CDS data, but only when an edit tool action finishes (e.g a new point has been added, or an existing point has been moved to a final new location)? If so, I don’t think there is a good way to accomplish this. What you’d really need is a dedicated event from the edit tool for “edit action completed” but no such even currently exists. It seems like a useful idea, though so I’d encourage you to submit a GitHub Issue to propose a new event for future development. Unfortunately I don’t have any immediate ideas or workarounds to suggest.

But when I publish the webpage with python and use source.js_on_change, it doesn’t trigger the callback until I’m done with the draw tool. It seems to be there but not sure how to access it in Javascript. Below code using python:

python code:

from bokeh.plotting import figure, output_file, save
from bokeh.models import ColumnDataSource, PointDrawTool, CustomJS
from bokeh.layouts import column

# Create a ColumnDataSource
source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[2, 4, 1, 3, 5]))

# Create the figure
p = figure(width=400, height=400, tools=[])

# Add a circle renderer
renderer = p.circle(x='x', y='y', source=source, size=10, color='blue', alpha=0.5)

lineRenderer =p.line('x', 'y', line_width=2, source=source)

# Create the PointDrawTool and add it to the figure
draw_tool = PointDrawTool(renderers=[renderer])
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool

# JavaScript code to sort data
js_code = """
        const data = source.data;
        const sortedIndices = Array.from(data['x'].keys()).sort((a, b) => data['x'][a] - data['x'][b]);
        const sortedX = sortedIndices.map(i => data['x'][i]);
        const sortedY = sortedIndices.map(i => data['y'][i]);
        source.data = { x: sortedX, y: sortedY };

    // update source data
    source.change.emit();
    console.log('sorted')
"""

# Attach the CustomJS code
custom_js = CustomJS(args=dict(source=source), code=js_code)
source.js_on_change('data', custom_js)

# Save the plot
output_file("point_draw_tool.html")
save(column(p))

And here is the published webapge:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Bokeh Plot</title>
    <style>
      html, body {
        box-sizing: border-box;
        display: flow-root;
        height: 100%;
        margin: 0;
        padding: 0;
      }
    </style>
    <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.2.2.min.js"></script>
    <script type="text/javascript">
        Bokeh.set_log_level("info");
    </script>
  </head>
  <body>
    <div id="dd23e666-af5f-41e7-a42e-28a2e4bc63f3" data-root-id="p1128" style="display: contents;"></div>
  
    <script type="application/json" id="p1129">
      {"0fcbebd1-1d00-4cb9-ad1e-31ee57b86f06":{"version":"3.2.2","title":"Bokeh Application","roots":[{"type":"object","name":"Column","id":"p1128","attributes":{"children":[{"type":"object","name":"Figure","id":"p1087","attributes":{"width":400,"height":400,"x_range":{"type":"object","name":"DataRange1d","id":"p1088"},"y_range":{"type":"object","name":"DataRange1d","id":"p1089"},"x_scale":{"type":"object","name":"LinearScale","id":"p1096"},"y_scale":{"type":"object","name":"LinearScale","id":"p1097"},"title":{"type":"object","name":"Title","id":"p1094"},"renderers":[{"type":"object","name":"GlyphRenderer","id":"p1114","attributes":{"data_source":{"type":"object","name":"ColumnDataSource","id":"p1084","attributes":{"js_property_callbacks":{"type":"map","entries":[["change:data",[{"type":"object","name":"CustomJS","id":"p1127","attributes":{"args":{"type":"map","entries":[["source",{"id":"p1084"}]]},"code":"\n        const data = source.data;\n        const sortedIndices = Array.from(data['x'].keys()).sort((a, b) =&gt; data['x'][a] - data['x'][b]);\n        const sortedX = sortedIndices.map(i =&gt; data['x'][i]);\n        const sortedY = sortedIndices.map(i =&gt; data['y'][i]);\n        source.data = { x: sortedX, y: sortedY };\n\n    // update source data\n    source.change.emit();\n    console.log('sorted')\n"}}]]]},"selected":{"type":"object","name":"Selection","id":"p1085","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1086"},"data":{"type":"map","entries":[["x",[1,2,3,4,5]],["y",[2,4,1,3,5]]]}}},"view":{"type":"object","name":"CDSView","id":"p1115","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1116"}}},"glyph":{"type":"object","name":"Circle","id":"p1111","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"size":{"type":"value","value":10},"line_color":{"type":"value","value":"blue"},"line_alpha":{"type":"value","value":0.5},"fill_color":{"type":"value","value":"blue"},"fill_alpha":{"type":"value","value":0.5},"hatch_color":{"type":"value","value":"blue"},"hatch_alpha":{"type":"value","value":0.5}}},"nonselection_glyph":{"type":"object","name":"Circle","id":"p1112","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"size":{"type":"value","value":10},"line_color":{"type":"value","value":"blue"},"line_alpha":{"type":"value","value":0.1},"fill_color":{"type":"value","value":"blue"},"fill_alpha":{"type":"value","value":0.1},"hatch_color":{"type":"value","value":"blue"},"hatch_alpha":{"type":"value","value":0.1}}},"muted_glyph":{"type":"object","name":"Circle","id":"p1113","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"size":{"type":"value","value":10},"line_color":{"type":"value","value":"blue"},"line_alpha":{"type":"value","value":0.2},"fill_color":{"type":"value","value":"blue"},"fill_alpha":{"type":"value","value":0.2},"hatch_color":{"type":"value","value":"blue"},"hatch_alpha":{"type":"value","value":0.2}}}}},{"type":"object","name":"GlyphRenderer","id":"p1123","attributes":{"data_source":{"id":"p1084"},"view":{"type":"object","name":"CDSView","id":"p1124","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1125"}}},"glyph":{"type":"object","name":"Line","id":"p1120","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_width":2}},"nonselection_glyph":{"type":"object","name":"Line","id":"p1121","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.1,"line_width":2}},"muted_glyph":{"type":"object","name":"Line","id":"p1122","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.2,"line_width":2}}}}],"toolbar":{"type":"object","name":"Toolbar","id":"p1095","attributes":{"tools":[{"type":"object","name":"PointDrawTool","id":"p1126","attributes":{"renderers":[{"id":"p1114"}]}}],"active_tap":{"id":"p1126"}}},"left":[{"type":"object","name":"LinearAxis","id":"p1103","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1104","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1105"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1106"}}}],"below":[{"type":"object","name":"LinearAxis","id":"p1098","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1099","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1100"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1101"}}}],"center":[{"type":"object","name":"Grid","id":"p1102","attributes":{"axis":{"id":"p1098"}}},{"type":"object","name":"Grid","id":"p1107","attributes":{"dimension":1,"axis":{"id":"p1103"}}}]}}]}}]}}
    </script>
    <script type="text/javascript">
      (function() {
        const fn = function() {
          Bokeh.safely(function() {
            (function(root) {
              function embed_document(root) {
              const docs_json = document.getElementById('p1129').textContent;
              const render_items = [{"docid":"0fcbebd1-1d00-4cb9-ad1e-31ee57b86f06","roots":{"p1128":"dd23e666-af5f-41e7-a42e-28a2e4bc63f3"},"root_ids":["p1128"]}];
              root.Bokeh.embed.embed_items(docs_json, render_items);
              }
              if (root.Bokeh !== undefined) {
                embed_document(root);
              } else {
                let attempts = 0;
                const timer = setInterval(function(root) {
                  if (root.Bokeh !== undefined) {
                    clearInterval(timer);
                    embed_document(root);
                  } else {
                    attempts++;
                    if (attempts > 100) {
                      clearInterval(timer);
                      console.log("Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing");
                    }
                  }
                }, 10, root)
              }
            })(window);
          });
        };
        if (document.readyState != "loading") fn();
        else document.addEventListener("DOMContentLoaded", fn);
      })();
    </script>
  </body>
</html>

@matCL I’m sorry but I guess I don’t understand your question, then. You have a CustomJS callback above, with JS code, that is accessing the CDS data. So in that context, I am not sure what “not sure how to access it in Javascript” could mean.

Also just FYI, if you assign a completely new value to .data, e.g.

source.data = { x: sortedX, y: sortedY }

then there is no need to call source.change.emit()— BokehJS will detect and respond to assigments to .data automatically. Calling source.change.emit() is only needed for in-place changes to the contents inside .data.

Edit:

it doesn’t trigger the callback until I’m done with the draw tool.

I guess first things first: is triggering the callback when to tool is done what you want, or not what you want? js_on_change('data', ...) will trigger when the tool is done because when the tool is done, it updates the data. But it will also trigger for anything and everything else that might happen to update the data.

No, you understood me perfectly in your first reply. To elaborate on my issue, the JavaScript code in my OP, if using “ source.connect(source.change, sortData)”, is doing the onChange as normally done in JavaScript which keeps triggering the sortData while moving the points with the draw tool. On the other hand, with the python code when using “source.js_on_change” it doesn’t trigger the customJS, aka sortData, until I finish adding/editing points. I’m not sure why the python customJS isn’t called when I move the points since it is supposed to be “on_chnage”. But I’m trying to achieve the same thing by the Python code in JavaScript without having a listener on mouseup.

BTW, if I have a table sharing the same CDS data, I can see the point values changing in the table while I move the point but it won’t call the Python customJS for sorting until I release the mouse.

Sorry if this is all confusing but I hope I clarified it a bit.

Just a follow-up with a working solution below for the javascript code instead of the mouseup listener.

source.connect(source.properties["data"]["change"], sortData);

I think the above code (similar to Python) is listening to CDS data change, while below code is listening to any CDS change.

source.connect(source.change, sortData);

Probably slightly preferable:

source.connect(source.properties.data.change, sortData)

I’m still not quite sure how this is better/different than (from Python)

source.js_on_change('data', """ <code  for sortData > """)

but if it works for you that is the most important thing :slight_smile:

Thanks for the suggestion but not sure why it is preferable. It seems exactly the same. Another way which allows to pass arguments:

source.properties.data.change.connect((_args, source) => sortData());

Probably my write-up was poor but I wanted the equivalent of the python code in JavaScript since I’m using BokehJS directly in JavaScript.

This was the crucial part I missed completely, it was not clear to me that you were not intending to use Python.

It seems exactly the same.

Well, BokehJS is written in TypeScipt, so it’s definitely preferable to use real typed attributes rather than untyped-grab-bag-of-string-keys in the context of BokehJS dev, I guess it’s just habit though.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.