Search code examples
javascriptgraphplotlyonclicklistenerplotly.js

Change marker radius: plotly node selection


I'm trying to change the size of marker when a node is clicked in Plotly

<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: 'red' },
        // 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, arrow: {size: 50, color: 'black', end:1}},
        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());

      // 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: nodes.map((_, i) => i === selection ? 12 : 6),
                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, 0.6, 60],
              y: selection[i].customdata,
            });
          }

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

The following line is included in the plotly.restyle function.

size: nodes.map((_, i) => i in selection ? 12 : 6),

But the size of all nodes change to 6. I wanted the marker size to change (from 6 to 12) when the node is clicked.

Suggestions on how to fix the problem will be really helpful.


Solution

  • The following line is included in the plotly.restyle function. -> you precisely have a typo at this line, that is, i === selection instead of i in selection.


    There is another issue you may have noticed, which is that the size reference for a marker is not the same when the size is a scalar compared to when it is defined by a numerical array, ie. size: 12 won't render the same size as [12, 12, 12, ...]. Because the node trace has its marker size initially defined with a scalar, the first time you select a node it switches to an array, and the numbers in the array that have the same value as the original size won't render the same which is annoying.

    You can fix this by initializing the marker size with a numerical array, you may also want to set an explicit sizeref to control the scale factor :

    sizeref - Has an effect only if marker.size is set to a numerical array. Sets the scale factor used to determine the rendered size of marker points. Use with sizemin and sizemode.
    Default: 1.

    var traceNodes = {
      // ...
      marker: {
        // unselected nodes, `size: 6` for each
        size: Array(nodes.length).fill(6),
        sizeref: 0.5,  // <- so that it renders merely the same as with `size: 6`
        color: 'red' 
      },
      // ...
    }
    

    And then when selection occurs :

    // ... 
    Plotly.restyle('myDiv', {
      marker: {
        sizeref: 0.5,  // <- need to be explicit again
        size: nodes.map((_, i) => i in selection ? 12 : 6),
        color: nodes.map((_, i) => i in selection ? 'blue' : 'red')
      }   
    });