1

This is a follow-up to my previous question posted here How to add custom values to nodes of plotly graph. The solution posted works well for selecting a single node using plotly_click. As suggested in the comments, I tried to use plotly_selected for displaying multiple nodes.

I am trying to select multiple nodes and display the values associated with the nodes using the following code.

<html>
  <head>
    <script src="https://cdn.plot.ly/plotly-1.58.5.min.js"></script>
    <style>
      .graph-container {
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .main-panel {
        width: 70%;
        float: left;
      }

      .side-panel {
        width: 30%;
        background-color: lightgray;
        min-height: 300px;
        overflow: auto;
        float: right;
      }
    </style>
  </head>
  <body>
    <div class="graph-container">
      <div id="myDiv" class="main-panel"></div>
      <div id="lineGraph" class="side-panel"></div>
    </div>
    <script>
      var nodes = [
        { x: 0, y: 0, z: 0, value: [1, 2, 3] },
        { x: 1, y: 1, z: 1, value: [4, 5, 6] },
        { x: 2, y: 0, z: 2, value: [7, 8, 9] },
        { x: 3, y: 1, z: 3, value: [10, 11, 12] },
        { x: 4, y: 0, z: 4, value: [13, 14, 15] }
      ];

      var edges = [
        { source: 0, target: 1 },
        { source: 1, target: 2 },
        { source: 2, target: 3 },
        { source: 3, target: 4 }
      ];

      var x = [];
      var y = [];
      var z = [];

      for (var i = 0; i < nodes.length; i++) {
        x.push(nodes[i].x);
        y.push(nodes[i].y);
        z.push(nodes[i].z);
      }

      var xS = [];
      var yS = [];
      var zS = [];
      var xT = [];
      var yT = [];
      var zT = [];

      for (var i = 0; i < edges.length; i++) {
        xS.push(nodes[edges[i].source].x);
        yS.push(nodes[edges[i].source].y);
        zS.push(nodes[edges[i].source].z);
        xT.push(nodes[edges[i].target].x);
        yT.push(nodes[edges[i].target].y);
        zT.push(nodes[edges[i].target].z);
      }

      var traceNodes = {
        x: x, y: y, z: z,
        mode: 'markers',
        marker: { size: 12, color: 'red' },
        type: 'scatter3d',
        text: [0, 1, 2, 3, 4],
        hoverinfo: 'text',
        hoverlabel: {
          bgcolor: 'white'
        },
        customdata: nodes.map(function(node) {
            if (node.value !== undefined)
               return node.value;
        }),
        type: 'scatter3d'
      };

      var layout = {
        margin: { l: 0, r: 0, b: 0, t: 0 }
      };

      Plotly.newPlot('myDiv', [traceNodes], layout, { displayModeBar: false });

      // max y value for the line plot
      const ymax = Math.max(...nodes.map(n => n.value).flat());

      document.getElementById('myDiv').on('plotly_selected', function(data){
       var selectedNodeIndices = data.points.map(function(point) {
        return point.pointNumber;
      });
      var values = [];
      for (var i = 0; i < selectedNodeIndices.length; i++) {
        values.push(nodes[selectedNodeIndices[i]].value);
      }

      var data = [];
      for (var i = 0; i < values.length; i++) {
        data.push({
          type: 'scatter',
          mode: 'lines',
          x: [0, 1, 2],
          y: values[i],
          name: 'Node ' + selectedNodeIndices[i]
        });
      }

      Plotly.newPlot('lineGraph', data, {
        margin: { t: 0 },
        yaxis: {autorange: false, range: [0, ymax + 1]}
      });
      });
    </script>
   </body>
</html>

There is some issue, I am not able to select multiple nodes and the line graph is not plotted when nodes are clicked.

Suggestions on how to fix this will be really helpful.

EDIT:

<html>
  <head>
    <script src="https://cdn.plot.ly/plotly-1.58.5.min.js"></script>
    <style>
      .graph-container {
        display: flex;
        justify-content: center;
        align-items: center;
      }

      .main-panel {
        width: 70%;
        float: left;
      }

      .side-panel {
        width: 30%;
        background-color: lightgray;
        min-height: 300px;
        overflow: auto;
        float: right;
      }
    </style>
  </head>
  <body>
    <div class="graph-container">
      <div id="myDiv" class="main-panel"></div>
      <div id="lineGraph" class="side-panel"></div>
    </div>
    <script>
      var nodes = [
        { x: 0, y: 0, z: 0, value: [1, 2, 3] },
        { x: 1, y: 1, z: 1, value: [4, 5, 6] },
        { x: 2, y: 0, z: 2, value: [7, 8, 9] },
        { x: 3, y: 1, z: 3, value: [10, 11, 12] },
        { x: 4, y: 0, z: 4, value: [13, 14, 15] }
      ];

      var edges = [
        { source: 0, target: 1 },
        { source: 1, target: 2 },
        { source: 2, target: 3 },
        { source: 3, target: 4 }
      ];

      var x = [];
      var y = [];
      var z = [];

      for (var i = 0; i < nodes.length; i++) {
        x.push(nodes[i].x);
        y.push(nodes[i].y);
        z.push(nodes[i].z);
      }

      const edge_x  = [];
      const edge_y  = [];
      const edge_z  = [];

      for (var i = 0; i < edges.length; i++) {
        const a = nodes[edges[i].source];
        const b = nodes[edges[i].target];
        edge_x.push(a.x, b.x, null);
        edge_y.push(a.y, b.y, null);
        edge_z.push(a.z, b.z, null);
      }

      var traceNodes = {
        x: x, y: y, z: z,
        mode: 'markers',
        marker: { size: 12, color: Array.from({length: nodes.length}, () => 'red') },
        text: [0, 1, 2, 3, 4],
        hoverinfo: 'text',
        hoverlabel: {
          bgcolor: 'white'
        },
        customdata: nodes.map(function(node) {
            if (node.value !== undefined)
               return node.value;
        }),
        type: 'scatter3d'
      };

      var traceEdges = {
        x: edge_x,
        y: edge_y,
        z: edge_z,
        type: 'scatter3d',
        mode: 'lines',
        line: { color: 'red', width: 2},
        opacity: 0.8
      };

      var layout = {
        margin: { l: 0, r: 0, b: 0, t: 0 }
      };

      Plotly.newPlot('myDiv', [traceNodes, traceEdges], layout, { displayModeBar: false });

      // max y value for the line plot
      const ymax = Math.max(...nodes.map(n => n.value).flat());

      document.getElementById('myDiv').on('plotly_click', function(data){
      var nodeIndex = data.points[0].pointNumber;
      var values = nodes[nodeIndex].value;

      // Change color of the selected node
      traceNodes.marker.color = Array.from({length: nodes.length}, (_, i) => i === nodeIndex ? 'blue' : 'red');
      Plotly.update('myDiv', {
        marker: {
            color: traceNodes.marker.color
        }
      }, [0]);

      Plotly.newPlot('lineGraph', [{
          type: 'scatter',
          mode: 'lines',
          x: [0, 1, 2],
          y: values
      }], {
          margin: { t: 0 },
          yaxis: {autorange: false, range: [0, ymax + 1]}
      });
      });
    </script>
   </body>
</html>

I tried to define the selected marker's color i.e. blue to indicate that it has been selected. Unfortunately, this is not working.

Regarding selecting multiple nodes, enter image description here

Currently, one single curve is visible on the side panel. I would like to know how to select multiple nodes and display multiple curves.

10
  • Unfortunately the selection features don't work with 3d scatter plot. See this issue for more info : github.com/plotly/plotly.js/issues/3511. Commented Feb 10, 2023 at 17:33
  • @EricLavault In that case can we integrate plotly scatter plot with any other library that supports multiple selections? Could you please give some suggestions? Commented Feb 11, 2023 at 4:50
  • Or could we store the selections and pass it to plotly_click? Commented Feb 11, 2023 at 5:26
  • I'm not sure we can rely on another lib. But I think it's possible to use the plotly_click event to make a selection, ie. when the event fires, we can only receive data for the (single) point that is clicked, so you might want to use the shift or ctrl key to detect whether that point should be added to the current selection or if it is a new selection (the selection should persist accordingly in the handler). The second thing would be to highlight the selected points so that the user can properly distinguish between them and the other points. This can be done manually without external lib. Commented Feb 11, 2023 at 15:14
  • @EricLavault Thanks a lot, could you please have a look at my edit? I tried to highlight the selected points (i.e. change the color of selected nodes to blue). But this is not working. Could you have a look at the code snippet in the edit? Commented Feb 11, 2023 at 16:52

1 Answer 1

1

Unfortunately selection features don't work with 3d scatter plot : lasso and select tools are not available and the event plotly_selected won't fire. See this issue for more info.

That said, it is still possible to handle multi-point selection manually using the plotly_click event with some extra code. The idea is to use a flag and keypress/keyup events to determine whether a given point should be added to or removed from the current selection, or if it should be added to a new selection (ie. the flag is on when holding the shift or ctrl key).

The second thing is to define a selection object that can persist across different click events, which means we need to define it outside the handler.

(only the end of the script changes)

// max y value for the line plot
const ymax = Math.max(...nodes.map(n => n.value).flat());

// Accumulation flag : true when user holds shift key, false otherwise.
let accumulate = false;
document.addEventListener('keydown', event => {
  if (event.key === 'Shift') accumulate = true;
});
document.addEventListener('keyup', event => {
  if (event.key === 'Shift') accumulate = false;
});

// Selected points {<nodeIndex> : <nodeData>}
let selection = {};

document.getElementById('myDiv').on('plotly_click', function(data){
  if (data.points[0].curveNumber !== 0)
     return;

  const nodeIndex = data.points[0].pointNumber;

  if (accumulate === false)
    selection = {[nodeIndex]: data.points[0]};
  else if (nodeIndex in selection)
    delete selection[nodeIndex];
  else
    selection[nodeIndex] = data.points[0];

  // Highlight selected nodes (timeout is set to prevent infinite recursion bug).
  setTimeout(() => {
    Plotly.restyle('myDiv', {
      marker: {
        size: 12,
        color: nodes.map((_, i) => i in selection ? 'blue' : 'red')
      }
    });
  }, 150);

  // Create a line trace for each selected node.
  const lineData = [];
  for (const i in selection) {
    lineData.push({
      type: 'scatter',
      mode: 'lines',
      x: [0, 1, 2],
      y: selection[i].customdata,
    });
  }

  Plotly.react('lineGraph', lineData, {
    margin: {t: 0},
    yaxis: {autorange: false, range: [0, ymax + 1]},
  });
});
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.