Home > other >  Unable to include parentheses in hierarchical edge bundle text labels with D3 (v3)
Unable to include parentheses in hierarchical edge bundle text labels with D3 (v3)

Time:06-14

I'm creating a hierarchical edge bundle chart using D3 (v3). I need my text labels to include parentheses in them (e.g. "name":"root.b.yes(1)"), however, this seems to stop the line highlighting from working when hovering over the label. According to the console, this renders the selector invalid. I've included code for a minimal example of just two labels that are connected to each other. If I remove the parentheses in my data.json file then on hover the lines highlight correctly, if the parentheses are included there's no line highlight. I'm not sure why parentheses aren't allowed and I haven't been able to find any help for this problem so far.

console error message

Uncaught DOMException: Failed to execute 'querySelector' on 'Element': '#node-yes(1)' is not a valid selector.

html

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <style type="text/css">

path.arc {
  cursor: move;
  fill: #fff;
}

.node {
  font-size: 10px;
}

.node:hover {
  fill: #1f77b4;
}

.link {
  fill: none;
  stroke: #1f77b4;
  stroke-opacity: .4;
  pointer-events: none;
}

.link.source, .link.target {
  stroke-opacity: 1;
  stroke-width: 2px;
}

.node.target {
  fill: #d62728 !important;
}

.link.source {
  stroke: #d62728;
}

.node.source {
  fill: #2ca02c;
}

.link.target {
  stroke: #2ca02c;
}

    </style>
  </head>
  <body>
    <h2>
    </h2>
    <div id="bundle"></div>
    <div style="position:absolute;bottom:0;font-size:18px;">tension: <input style="position:relative;top:3px;" type="range" min="0" max="100" value="85"></div>
    <script type="text/javascript" src="//code.jquery.com/jquery-1.12.3.min.js"></script>
    <script type="text/javascript" src="//d3js.org/d3.v3.min.js"></script>
    <script type="text/javascript" src="packages.js"></script>
    <script type="text/javascript">

var w = 740,
    h = 700,
    rx = w / 2,
    ry = h / 2,
    m0,
    rotate = 0
    pi = Math.PI;

var splines = [];

var cluster = d3.layout.cluster()
    .size([360, ry - 180])
    .sort(function(a, b) { return d3.ascending(a.key, b.key); });

var bundle = d3.layout.bundle();

var line = d3.svg.line.radial()
    .interpolate("bundle")
    .tension(.85)
    .radius(function(d) { return d.y; })
    .angle(function(d) { return d.x / 180 * Math.PI; });

// Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
var div = d3.select("#bundle")
    .style("width", w   "px")
    .style("height", w   "px")
    .style("position", "absolute");

var svg = div.append("svg:svg")
    .attr("width", w)
    .attr("height", w)
  .append("svg:g")
    .attr("transform", "translate("   rx   ","   ry   ")");

svg.append("svg:path")
    .attr("class", "arc")
    .attr("d", d3.svg.arc().outerRadius(ry - 180).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
    .on("mousedown", mousedown);

d3.json("data.json", function(classes) {
  var nodes = cluster.nodes(packages.root(classes)),
      links = packages.imports(nodes),
      splines = bundle(links);

  var path = svg.selectAll("path.link")
      .data(links)
    .enter().append("svg:path")
      .attr("class", function(d) { return "link source-"   d.source.key   " target-"   d.target.key; })
      .attr("d", function(d, i) { return line(splines[i]); });

  var groupData = svg.selectAll("g.group")
    .data(nodes.filter(function(d) { return (d.key=='a' || d.key == 'b') && d.children; }))
  .enter().append("group")
    .attr("class", "group");
    
  var groupArc = d3.svg.arc()
  .innerRadius(ry - 177)
  .outerRadius(ry - 157)
  .startAngle(function(d) { return (findStartAngle(d.__data__.children)-2) * pi / 180;})
  .endAngle(function(d) { return (findEndAngle(d.__data__.children) 2) * pi / 180});
  
  svg.selectAll("g.arc")
  .data(groupData[0])
.enter().append("svg:path")
  .attr("d", groupArc)
  .attr("class", "groupArc")
  .style("fill", "#1f77b4")
  .style("fill-opacity", 0.5);

  svg.selectAll("g.node")
      .data(nodes.filter(function(n) { return !n.children; }))
    .enter().append("svg:g")
      .attr("class", "node")
      .attr("id", function(d) { return "node-"   d.key; })
      .attr("transform", function(d) { return "rotate("   (d.x - 90)   ")translate("   d.y   ")"; })
    .append("svg:text")
      .attr("dx", function(d) { return d.x < 180 ? 25 : -25; })
      .attr("dy", ".31em")
      .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
      .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
      .text(function(d) { return d.key.replace(/_/g, ' '); })
      .on("mouseover", mouseover)
      .on("mouseout", mouseout);

  d3.select("input[type=range]").on("change", function() {
    line.tension(this.value / 100);
    path.attr("d", function(d, i) { return line(splines[i]); });
  });
});

d3.select(window)
    .on("mousemove", mousemove)
    .on("mouseup", mouseup);

function mouse(e) {
  return [e.pageX - rx, e.pageY - ry];
}

function mousedown() {
  m0 = mouse(d3.event);
  d3.event.preventDefault();
}

function mousemove() {
  if (m0) {
    var m1 = mouse(d3.event),
        dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
    div.style("-webkit-transform", "translate3d(0,"   (ry - rx)   "px,0)rotate3d(0,0,0,"   dm   "deg)translate3d(0,"   (rx - ry)   "px,0)");
  }
}

function mouseup() {
  if (m0) {
    var m1 = mouse(d3.event),
        dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;

    rotate  = dm;
    if (rotate > 360) rotate -= 360;
    else if (rotate < 0) rotate  = 360;
    m0 = null;

    div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");

    svg.attr("transform", "translate("   rx   ","   ry   ")rotate("   rotate   ")")
      .selectAll("g.node text")
        .attr("dx", function(d) { return (d.x   rotate) % 360 < 180 ? 25 : -25; })
        .attr("text-anchor", function(d) { return (d.x   rotate) % 360 < 180 ? "start" : "end"; })
        .attr("transform", function(d) { return (d.x   rotate) % 360 < 180 ? null : "rotate(180)"; });
  }
}

function mouseover(d) {
  svg.selectAll("path.link.target-"   d.key)
      .classed("target", true)
      .each(updateNodes("source", true));

  svg.selectAll("path.link.source-"   d.key)
      .classed("source", true)
      .each(updateNodes("target", true));
}

function mouseout(d) {
  svg.selectAll("path.link.source-"   d.key)
      .classed("source", false)
      .each(updateNodes("target", false));

  svg.selectAll("path.link.target-"   d.key)
      .classed("target", false)
      .each(updateNodes("source", false));
}

function updateNodes(name, value) {
  return function(d) {
    if (value) this.parentNode.appendChild(this);
    svg.select("#node-"   d[name].key).classed(name, value);
  };
}

function cross(a, b) {
  return a[0] * b[1] - a[1] * b[0];
}

function dot(a, b) {
  return a[0] * b[0]   a[1] * b[1];
}

function findStartAngle(children) {
    var min = children[0].x;
    children.forEach(function(d) {
       if (d.x < min)
           min = d.x;
    });
    return min;
}

function findEndAngle(children) {
    var max = children[0].x;
    children.forEach(function(d) {
       if (d.x > max)
           max = d.x;
    });
    return max;
}
    </script>
  </body>
</html>

json

[
    {"name":"root.a.no","imports":["root.b.yes(1)"]},
    {"name":"root.b.yes(1)","imports":[]}
]

css

body {
  font: 300 36px "Helvetica Neue";
  height: 640px;
  /*margin: 80px 160px 80px 160px;
  overflow: hidden;*/
  position: relative;
}

a:link, a:visited {
  color: rgb(202, 0, 0);
  text-decoration: none;
}

a:hover {
  color: rgb(245, 6, 6);
}

.invert {
  background: #1f1f1f;
  color: #dcdccc;
}

.invert h2, .invert h3 {
  color: #3eec3e;
}

.string, .regexp {
  color: #f39;
}

.keyword {
  color: #00c;
}

.comment {
  color: rgb(255, 28, 28);
  font-style: oblique;
}

.number {
  color: rgb(255, 21, 21);
}

.class, .special {
  color: #1181B8;
}

body > svg {
  position: absolute;
  top: -80px;
  left: -160px;
}

I know that future versions of D3 allow parentheses in the text labels but I need to specifically use version 3.

Any help is appreciated.

CodePudding user response:

There's two things that I think could work:

  1. Try adding escape backslashes for the query string

  2. If after that, d3v3 still does not support this directly, I believe that the workaround would be to pass to select the HTML node instead of the selector string. So you could do something like:

     const node = document.querySelector('#node-yes(1)')
     d3.select(node).classed(...)
    

And then again, if it doesn't work, try escaping the parenthesis when selecting: (edit)

  const parsedKey = d[name].key.replace("(", "\\(").replace(")", "\\)")
  svg.select("#node-"   parsedKey).classed(name, value)

(update) This would have to be done every time a node is selected

. I believe that if none of these two options work, then you would need to bump up the version of d3

  • Related