Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add event listener per nodes level on d3 chart

I am still on my chart, and I need to default close the level 2 & 3 nodes, and keep the expand/collapse function on click.

Depending on the node clicked and its level, run a specific action (change color for example). My link must be a value of my data object (var pubs in my codepen) as you can see bellow (level 0 has no link, "TOOLS" in my example) :

{
    "name": "TOOLS",
    "children": 
      [
        {
            "name": "Localization",
            "url": "http://#",
            "children":             
                [
                   {"name": "FRANCE", "url": "http://france.fr"}
...

Finaly another event listener on "mouseover" to do some styling on the node (closed or open) etc...

My current code : https://codepen.io/anon/pen/BqjJJv

like image 430
andrea06590 Avatar asked Oct 03 '18 13:10

andrea06590


People also ask

Can I add event listener to all elements of class?

To add an event listener to all elements with class: Use the document. querySelectorAll() method to select the elements by class. Use the forEach() method to iterate over the collection of elements.

Can you add an EventListener to a function?

The method addEventListener() works by adding a function, or an object that implements EventListener , to the list of event listeners for the specified event type on the EventTarget on which it's called.


2 Answers

If you want to collapse all nodes except the root and the first set of children, you can run the following code:

root.children.forEach(collapse);

This recursively applies the collapse function to all the children of the root.

There is already a click event listener on your nodes, which you can alter according to your plans. Adding listeners for mouseover and mouseout events is very simple:

var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("click", click)
.on('mouseover', mouseover) // mouseover!
.on('mouseout', mouseout)   // mouseout!

You then need to add functions that will define the actions that occur. Here are a couple of sample functions:

// `d` is the data item attached to the DOM node;
// `this` is the node that triggered the event (`g.node`)
function mouseover(d) {
  // select the `rect` node that is the child of the DOM node that triggered the event
  d3.select(this).select('rect').style('fill', function(d){
    // depending on the level of the node, give it one of these tacky colours
    if (d.depth === 0) {
      return 'deepskyblue'
    } else if (d.depth === 1) {
      return 'deeppink'
    } else if (d.depth === 2) {
      return 'goldenrod'
    }
    return 'papayawhip' // default
  })
}

function mouseout(d) {
  // select the rect element
  d3.select(this).select('rect')
    // if the node has collapsed children, turn it light blue; otherwise turn it red
    .style('fill', d => d._children ? "lightsteelblue" : "red")
}

You can also add extra functionality to the click function in a similar manner.

Here are the event listeners in action:

var pubs = {
  "name": "TOOLS",
  "children": [{
      "name": "Localization",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "Test",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "Oh My God",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "Another Tool",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },

    {
      "name": "And Again",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "And Again",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "Production",
      "children": [{
          "name": "FRANCE"
        },
        {
          "name": "SUISSE"
        },
        {
          "name": "USA"
        },
        {
          "name": "UK"
        }
      ]
    },
    {
      "name": "Audio",
      "children": [{
          "name": "AUT-11"
        },
        {
          "name": "AUT-12"
        }
      ]
    },
    {
      "name": "Animation",
      "children": [{
          "name": "AUT-11"
        },
        {
          "name": "AUT-12"
        }
      ]
    },
    {
      "name": "Tags",
      "children": [{
          "name": "AUT-11"
        },
        {
          "name": "AUT-12"
        }
      ]
    }
  ]
};

var diameter = 800;

var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
  width = diameter,
  height = diameter;

var i = 0,
  duration = 350,
  root;

var tree = d3.layout.tree()
  .size([360, diameter / 2 - 80])
  .separation(function(a, b) {
    return (a.parent == b.parent ? 1 : 1) / a.depth;
  });


var diagonal_extras = {
  path: {
    // diagonal line
    direct: function(p) {
        return [p.source, p.target];
      }

      // this is also the default path in radial trees
      ,
    l_shape: function(p) {
        return [p.source, {
          x: p.target.x,
          y: p.source.y
        }, p.target];
      }

      ,
    l_shape_2: function(p) {
        return [p.source, {
          x: p.source.x,
          y: p.target.y
        }, p.target];
      }

      ,
    dogleg: function(p) {
        return [p.source,
          {
            x: p.source.x,
            y: (p.source.y + p.target.y) / 2
          },
          {
            x: (p.source.x + p.target.x) / 2,
            y: (p.source.y + p.target.y) / 2
          },
          {
            x: p.target.x,
            y: (p.source.y + p.target.y) / 2
          },
          p.target
        ];
      }

      ,
    dogleg_2: function(p) {
      return [p.source,
        {
          x: (p.source.x + p.target.x) / 2,
          y: p.source.y
        },
        {
          x: (p.source.x + p.target.x) / 2,
          y: (p.source.y + p.target.y) / 2
        },
        {
          x: (p.source.x + p.target.x) / 2,
          y: p.target.y
        },
        p.target
      ];
    }

  }

  ,
  polar_obj_to_cart: function(pt) {
      var angle = pt.x / 180 * Math.PI;
      return [pt.y * Math.cos(angle), pt.y * Math.sin(angle)];
    }

    ,
  polar_coords_to_cart: function(xy) {
    var angle = xy[0] / 180 * Math.PI;
    return [xy[1] * Math.cos(angle), xy[1] * Math.sin(angle)];
  }

}

diagonal_extras.right_angle = function() {

  var projection = d3.svg.diagonal().projection(),
    path_type = 'dogleg';

  function diagonal(d) {
    return diagonal.path_maker(diagonal_extras.path[diagonal.path_type()](d));
  }

  diagonal.path_maker = function(pathData) {
    return "M" + pathData.map(projection).join(' ');
  };

  diagonal.valid_path_types = function() {
    return Object.keys(diagonal_extras.path);
  };

  diagonal.path_type = function(x) {
    if (!arguments.length) {
      return path_type;
    }
    if (diagonal_extras.path[x]) {
      path_type = x;
      return diagonal;
    }
    throw new Error(x + ' is not a valid path type');
  };

  diagonal.projection = function(x) {
    if (!arguments.length) {
      return projection;
    }
    projection = x;
    return diagonal;
  };

  diagonal.path = function(x) {
    if (!arguments.length) {
      return path;
    }
    path = x;
    return diagonal;
  };

  diagonal.draw = function(d) {
    return diagonal(d);
  };

  return diagonal;
}
diagonal_extras.radial = function() {
  var diagonal = diagonal_extras.right_angle(),
    projection = function(pt) {
      return [pt.x, pt.y];
    };

  diagonal.path_type('direct');

  diagonal.projection = function(x) {
    if (!arguments.length) {
      return projection;
    }
    projection = x;
    return diagonal;
  };

  diagonal.path_maker = function(pathData) {

    var projected = pathData.map(function(x) {
        return projection(x);
      }),
      pl = projected.length,
      points, prev_angle;

    // direct link:
    if (2 === pl) {
      return 'M' + projected.map(function(x) {
        return diagonal_extras.polar_coords_to_cart(x);
      }).join(' ');
    }

    points = projected.map(function(obj) {
      return {
        angle: obj[0] / 180 * Math.PI,
        radius: obj[1]
      };
    });

    return "M" + points.map(function(pt) {
      var str = '';
      if (prev_angle) {
        if (prev_angle === pt.angle) {
          // draw a straight line
          str = 'L';
        } else {
          // draw an arc to the new radius and angle
          str = 'A' + pt.radius + ',' + pt.radius
            // x axis rotation
            +
            " 0 "
            // large arc flag
            +
            " 0,"
            // sweep
            +
            (pt.angle > prev_angle ? 1 : 0) + " ";

        }
      }
      prev_angle = pt.angle;
      return str + pt.radius * Math.cos(pt.angle) + "," + pt.radius * Math.sin(pt.angle);

    }).join(' ');

  };
  return diagonal;
}

var diagonal = diagonal_extras.radial()
  .path_type('dogleg')
  .projection(function(d) {
    return [d.x - 90, d.y];
  });

var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height)
  .append("g")
  .attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");

var rect = {
  l: 95,
  w: 20
}

root = pubs;
root.x0 = height / 2;
root.y0 = 0;

root.children.forEach(collapse); // start with all children collapsed
update(root);

d3.select(self.frameElement).style("height", "800px");

function update(source) {

  // Compute the new tree layout.
  var nodes = tree.nodes(root),
    links = tree.links(nodes),
    offset = nodes[0].x;
  // Normalize for fixed-depth.
  nodes.forEach(function(d) {
    d.y = d.depth * 150;
  });

  // Normalise angles so that the root is horizontal
  if (nodes[0].x > 180) {
    nodes[0].x = nodes[0].x - 90
  } else {
    nodes[0].x = nodes[0].x + 90
  }
  // Update the nodes…
  var node = svg.selectAll("g.node")
    .data(nodes, function(d) {
      return d.id || (d.id = ++i);
    });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("g")
    .attr("class", "node")
    //.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
    .on("click", click)
    .on('mouseover', mouseover)
    .on('mouseout', mouseout)

  nodeEnter.append("rect")
    .attr("width", rect.l)
    .attr("height", rect.w)
    .attr("x", -rect.l / 2)
    .attr("y", -rect.w / 2)
    .style("fill", function(d) {
      return d._children ? "lightsteelblue" : "red";
    });

  nodeEnter.append("text")
    .attr("x", 0)
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    //.attr("transform", function(d) { return d.x < 180 ? "translate(0)" : "rotate(180)translate(-" + (d.name.length * 8.5)  + ")"; })
    .text(function(d) {
      return d.name;
    })
    .style("fill-opacity", 1e-6);

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
    })

  // nodeUpdate.select("circle")
  //     .attr("r", 4.5)
  //     .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });

  nodeUpdate.select("text")
    .style("fill-opacity", 1)
    .attr("transform", function(d) {
      return d.x < 180 ? "translate(0)" : "rotate(180)";
    });

  // TODO: appropriate transform
  var nodeExit = node.exit().transition()
    .duration(duration)
    //.attr("transform", function(d) { return "diagonal(" + source.y + "," + source.x + ")"; })
    .remove();

  // nodeExit.select("circle")
  //     .attr("r", 1e-6);

  nodeExit.select("text")
    .style("fill-opacity", 1e-6);

  // Update the links…
  var link = svg.selectAll("path.link")
    .data(links, function(d) {
      return d.target.id;
    });

  // Enter any new links at the parent's previous position.
  link.enter().insert("path", "g")
    .attr("class", "link")
    .attr("d", function(d) {
      var o = {
        x: source.x0,
        y: source.y0
      };
      return diagonal({
        source: o,
        target: o
      });
    });

  // Transition links to their new position.
  link.transition()
    .duration(duration)
    .attr("d", function(d) {
      return diagonal({
        source: {
          x: d.source.x,
          y: d.source.y + rect.l / 2
        },
        target: {
          x: d.target.x,
          y: d.target.y - rect.l / 2
        }
      })
    });

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
    .duration(duration)
    .attr("d", function(d) {
      var o = {
        x: source.x,
        y: source.y
      };
      return diagonal({
        source: o,
        target: o
      });
    })
    .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
}

function mouseover(d) {
  d3.select(this).select('rect').style('fill', function(d){
    if (d.depth === 0) {
      return 'deepskyblue'
    } else if (d.depth === 1) {
      return 'deeppink'
    } else if (d.depth === 2) {
      return 'goldenrod'
    }
    return 'papayawhip'
  })
}

function mouseout(d) {
  d3.select(this).select('rect').style('fill', d => d._children ? "lightsteelblue" : "red")
}

// Toggle children on click.
function click(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }

  update(d);
}

// Collapse nodes
function collapse(d) {
  if (d.children) {
    d._children = d.children;
    d._children.forEach(collapse);
    d.children = null;
  }
}
.node {
  cursor: pointer;
}

.node text {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}
<script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script>
like image 147
i alarmed alien Avatar answered Oct 04 '22 07:10

i alarmed alien


If what you want is for the levels 2 and 3 to be closed by default you could just rename every children property to _children in the pubs object. Like you already do in your click handler, only start out with underscored children and that's it. For your other questions, I have no answer yet.

Like this:

{
    "name": "TOOLS",
    "_children": 
      [
        {
            "name": "Localization",
            "url": "http://#",
            "_children":             
                [
                   {"name": "FRANCE", "url": "http://france.fr"}
...
like image 21
HelgeFox Avatar answered Oct 04 '22 08:10

HelgeFox