Skip to content Skip to sidebar Skip to footer

Adding Foreignobjects To D3 Force-directed Graph Nodes Breaks Events

I'm currently learning D3 visualization. While working on a force-directed graph where I needed to show country flags on nodes (lots of them, for all countries), I decided to go fo

Solution 1:

I was hesitant to type up this up as an answer because it's not an answer to your question but instead a better way to do what you want. Essentially, the foreignObject tag is evil and should never be used.

So the new quesiton becomes how can we replicate CSS style sprites with SVG? The answer is SVG fill patterns in all their glory.

Say we take your css background image definitions and reformat to a JavaScript array:

varimagePos= [{
      name:"ad",
      x:16,
      y:0
    }, {
      name:"ae",
      x:32,
      y:0
    }, 
    ...
 ];

And create a pattern for each flag:

var defs = svg.append("defs")
  .selectAll("pattern")
  .data(imagePos)
  .enter()
  .append("pattern")
  .attr("width", 16)
  .attr("height", 11)
  .attr("patternTransform", function(d) {
    return"translate(" + -d.x + "," + -d.y + ")";
  })
  .attr("id", function(d) {
    return"pattern_" + d.name;
  });

defs.append("image")
  .attr("xlink:href", "https://drive.google.com/uc?export=download&id=0B8_3FL-6NZmAeXdJdlJ0REVzRWs")
  .attr("width", "256")
  .attr("height", "176");

We can then fill our nodes like:

var node = svg.selectAll('node')
  .data(json.nodes)
  .enter().append('g')
  .attr('class', 'node')
  .call(force.drag()
    .on("dragstart", function() {
      bDragging = true
    })
    .on("dragend", function() {
      bDragging = false
    }));

node.append("rect")
  .attr("width", 16)
  .attr("height", 11)
  .style("stroke", "none")
  .attr("fill", function(d) {
    return"url(#pattern_" + d.code + ")";
  });

And BAM we have no more foreignObject and only one image to download.

Note, I rewrote your tick function, it should operate on the nodes.


Updated codepen


Running code:

//var data = [4, 8, 15, 16, 23, 22,20,21,5];var w = 0;
var h = 0;
var barPadding = 0;
var padding = 70var svgOffset = 100var circleRadius = 10var xAxis = d3.svg.axis()
var yAxis = d3.svg.axis()
var xScale, yScale, colorScale, svg, baseTemp
var yearBarWidth, yearBarHeight
var minYear, maxYear
var url = "https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json"var heatMap = [];
var tooltip = d3.select(".my-tooltip")
var force = d3.layout.force();
var bDragging = false

d3.json(url, processJson);

functionprocessJson(json) {
  initializeViewBox();
  //prepJson(json);initializeForce(json);
  drawNodesAndLinks(json);
  force.start()
  console.log("done processing json")
}

functioninitializeViewBox() {
  w = window.innerWidth;
  h = w * 0.6;
  
  svg = d3.select("svg")
    .attr("class", "svg-content")
    .attr("preserveAspectRatio", "xMinYMin meet")
  //.attr("heigth",h)//.attr("width",w)
  .attr("viewBox", "0 0 " + w + " " + h)
  
}

functioninitializeForce(json) {
console.log("initializing force,",force)
  force 
    .size([w, h])
    .nodes(json.nodes)
    .links(json.links)
    //.linkDistance(w/2.5)
    .gravity(0.3)
    .charge(-400);
 // d3.forceCollide(w/200)
}
functionprepJson(json){
  for (var i=0; i<json.nodes.length;i++){
json.nodes[i].x = w*Math.random()
json.nodes[i].y = w*Math.random()
  }
 console.log(json.nodes)
}
functiondrawNodesAndLinks(json) {
  
  var imagePos = [
{name: "ad", x: 16, y: 0},
{name: "ae", x: 32, y: 0},
{name: "af", x: 48, y: 0},
{name: "ag", x: 64, y: 0},
{name: "ai", x: 80, y: 0},
{name: "al", x: 96, y: 0},
{name: "am", x: 112, y: 0},
{name: "an", x: 128, y: 0},
{name: "ao", x: 144, y: 0},
{name: "ar", x: 160, y: 0},
{name: "as", x: 176, y: 0},
{name: "at", x: 192, y: 0},
{name: "au", x: 208, y: 0},
{name: "aw", x: 224, y: 0},
{name: "az", x: 240, y: 0},
{name: "ba", x: 0, y: 11},
{name: "bb", x: 16, y: 11},
{name: "bd", x: 32, y: 11},
{name: "be", x: 48, y: 11},
{name: "bf", x: 64, y: 11},
{name: "bg", x: 80, y: 11},
{name: "bh", x: 96, y: 11},
{name: "bi", x: 112, y: 11},
{name: "bj", x: 128, y: 11},
{name: "bm", x: 144, y: 11},
{name: "bn", x: 160, y: 11},
{name: "bo", x: 176, y: 11},
{name: "br", x: 192, y: 11},
{name: "bs", x: 208, y: 11},
{name: "bt", x: 224, y: 11},
{name: "bv", x: 240, y: 11},
{name: "bw", x: 0, y: 22},
{name: "by", x: 16, y: 22},
{name: "bz", x: 32, y: 22},
{name: "ca", x: 48, y: 22},
{name: "catalonia", x: 64, y: 22},
{name: "cd", x: 80, y: 22},
{name: "cf", x: 96, y: 22},
{name: "cg", x: 112, y: 22},
{name: "ch", x: 128, y: 22},
{name: "ci", x: 144, y: 22},
{name: "ck", x: 160, y: 22},
{name: "cl", x: 176, y: 22},
{name: "cm", x: 192, y: 22},
{name: "cn", x: 208, y: 22},
{name: "co", x: 224, y: 22},
{name: "cr", x: 240, y: 22},
{name: "cu", x: 0, y: 33},
{name: "cv", x: 16, y: 33},
{name: "cw", x: 32, y: 33},
{name: "cy", x: 48, y: 33},
{name: "cz", x: 64, y: 33},
{name: "de", x: 80, y: 33},
{name: "dj", x: 96, y: 33},
{name: "dk", x: 112, y: 33},
{name: "dm", x: 128, y: 33},
{name: "do", x: 144, y: 33},
{name: "dz", x: 160, y: 33},
{name: "ec", x: 176, y: 33},
{name: "ee", x: 192, y: 33},
{name: "eg", x: 208, y: 33},
{name: "eh", x: 224, y: 33},
{name: "england", x: 240, y: 33},
{name: "er", x: 0, y: 44},
{name: "es", x: 16, y: 44},
{name: "et", x: 32, y: 44},
{name: "eu", x: 48, y: 44},
{name: "fi", x: 64, y: 44},
{name: "fj", x: 80, y: 44},
{name: "fk", x: 96, y: 44},
{name: "fm", x: 112, y: 44},
{name: "fo", x: 128, y: 44},
{name: "fr", x: 144, y: 44},
{name: "ga", x: 160, y: 44},
{name: "gb", x: 176, y: 44},
{name: "gd", x: 192, y: 44},
{name: "ge", x: 208, y: 44},
{name: "gf", x: 224, y: 44},
{name: "gg", x: 240, y: 44},
{name: "gh", x: 0, y: 55},
{name: "gi", x: 16, y: 55},
{name: "gl", x: 32, y: 55},
{name: "gm", x: 48, y: 55},
{name: "gn", x: 64, y: 55},
{name: "gp", x: 80, y: 55},
{name: "gq", x: 96, y: 55},
{name: "gr", x: 112, y: 55},
{name: "gs", x: 128, y: 55},
{name: "gt", x: 144, y: 55},
{name: "gu", x: 160, y: 55},
{name: "gw", x: 176, y: 55},
{name: "gy", x: 192, y: 55},
{name: "hk", x: 208, y: 55},
{name: "hm", x: 224, y: 55},
{name: "hn", x: 240, y: 55},
{name: "hr", x: 0, y: 66},
{name: "ht", x: 16, y: 66},
{name: "hu", x: 32, y: 66},
{name: "ic", x: 48, y: 66},
{name: "id", x: 64, y: 66},
{name: "ie", x: 80, y: 66},
{name: "il", x: 96, y: 66},
{name: "im", x: 112, y: 66},
{name: "in", x: 128, y: 66},
{name: "io", x: 144, y: 66},
{name: "iq", x: 160, y: 66},
{name: "ir", x: 176, y: 66},
{name: "is", x: 192, y: 66},
{name: "it", x: 208, y: 66},
{name: "je", x: 224, y: 66},
{name: "jm", x: 240, y: 66},
{name: "jo", x: 0, y: 77},
{name: "jp", x: 16, y: 77},
{name: "ke", x: 32, y: 77},
{name: "kg", x: 48, y: 77},
{name: "kh", x: 64, y: 77},
{name: "ki", x: 80, y: 77},
{name: "km", x: 96, y: 77},
{name: "kn", x: 112, y: 77},
{name: "kp", x: 128, y: 77},
{name: "kr", x: 144, y: 77},
{name: "kurdistan", x: 160, y: 77},
{name: "kw", x: 176, y: 77},
{name: "ky", x: 192, y: 77},
{name: "kz", x: 208, y: 77},
{name: "la", x: 224, y: 77},
{name: "lb", x: 240, y: 77},
{name: "lc", x: 0, y: 88},
{name: "li", x: 16, y: 88},
{name: "lk", x: 32, y: 88},
{name: "lr", x: 48, y: 88},
{name: "ls", x: 64, y: 88},
{name: "lt", x: 80, y: 88},
{name: "lu", x: 96, y: 88},
{name: "lv", x: 112, y: 88},
{name: "ly", x: 128, y: 88},
{name: "ma", x: 144, y: 88},
{name: "mc", x: 160, y: 88},
{name: "md", x: 176, y: 88},
{name: "me", x: 192, y: 88},
{name: "mg", x: 208, y: 88},
{name: "mh", x: 224, y: 88},
{name: "mk", x: 240, y: 88},
{name: "ml", x: 0, y: 99},
{name: "mm", x: 16, y: 99},
{name: "mn", x: 32, y: 99},
{name: "mo", x: 48, y: 99},
{name: "mp", x: 64, y: 99},
{name: "mq", x: 80, y: 99},
{name: "mr", x: 96, y: 99},
{name: "ms", x: 112, y: 99},
{name: "mt", x: 128, y: 99},
{name: "mu", x: 144, y: 99},
{name: "mv", x: 160, y: 99},
{name: "mw", x: 176, y: 99},
{name: "mx", x: 192, y: 99},
{name: "my", x: 208, y: 99},
{name: "mz", x: 224, y: 99},
{name: "na", x: 240, y: 99},
{name: "nc", x: 0, y: 110},
{name: "ne", x: 16, y: 110},
{name: "nf", x: 32, y: 110},
{name: "ng", x: 48, y: 110},
{name: "ni", x: 64, y: 110},
{name: "nl", x: 80, y: 110},
{name: "no", x: 96, y: 110},
{name: "np", x: 112, y: 110},
{name: "nr", x: 128, y: 110},
{name: "nu", x: 144, y: 110},
{name: "nz", x: 160, y: 110},
{name: "om", x: 176, y: 110},
{name: "pa", x: 192, y: 110},
{name: "pe", x: 208, y: 110},
{name: "pf", x: 224, y: 110},
{name: "pg", x: 240, y: 110},
{name: "ph", x: 0, y: 121},
{name: "pk", x: 16, y: 121},
{name: "pl", x: 32, y: 121},
{name: "pm", x: 48, y: 121},
{name: "pn", x: 64, y: 121},
{name: "pr", x: 80, y: 121},
{name: "ps", x: 96, y: 121},
{name: "pt", x: 112, y: 121},
{name: "pw", x: 128, y: 121},
{name: "py", x: 144, y: 121},
{name: "qa", x: 160, y: 121},
{name: "re", x: 176, y: 121},
{name: "ro", x: 192, y: 121},
{name: "rs", x: 208, y: 121},
{name: "ru", x: 224, y: 121},
{name: "rw", x: 240, y: 121},
{name: "sa", x: 0, y: 132},
{name: "sb", x: 16, y: 132},
{name: "sc", x: 32, y: 132},
{name: "scotland", x: 48, y: 132},
{name: "sd", x: 64, y: 132},
{name: "se", x: 80, y: 132},
{name: "sg", x: 96, y: 132},
{name: "sh", x: 112, y: 132},
{name: "si", x: 128, y: 132},
{name: "sk", x: 144, y: 132},
{name: "sl", x: 160, y: 132},
{name: "sm", x: 176, y: 132},
{name: "sn", x: 192, y: 132},
{name: "so", x: 208, y: 132},
{name: "somaliland", x: 224, y: 132},
{name: "sr", x: 240, y: 132},
{name: "ss", x: 0, y: 143},
{name: "st", x: 16, y: 143},
{name: "sv", x: 32, y: 143},
{name: "sx", x: 48, y: 143},
{name: "sy", x: 64, y: 143},
{name: "sz", x: 80, y: 143},
{name: "tc", x: 96, y: 143},
{name: "td", x: 112, y: 143},
{name: "tf", x: 128, y: 143},
{name: "tg", x: 144, y: 143},
{name: "th", x: 160, y: 143},
{name: "tibet", x: 176, y: 143},
{name: "tj", x: 192, y: 143},
{name: "tk", x: 208, y: 143},
{name: "tl", x: 224, y: 143},
{name: "tm", x: 240, y: 143},
{name: "tn", x: 0, y: 154},
{name: "to", x: 16, y: 154},
{name: "tr", x: 32, y: 154},
{name: "tt", x: 48, y: 154},
{name: "tv", x: 64, y: 154},
{name: "tw", x: 80, y: 154},
{name: "tz", x: 96, y: 154},
{name: "ua", x: 112, y: 154},
{name: "ug", x: 128, y: 154},
{name: "um", x: 144, y: 154},
{name: "us", x: 160, y: 154},
{name: "uy", x: 176, y: 154},
{name: "uz", x: 192, y: 154},
{name: "va", x: 208, y: 154},
{name: "vc", x: 224, y: 154},
{name: "ve", x: 240, y: 154},
{name: "vg", x: 0, y: 165},
{name: "vi", x: 16, y: 165},
{name: "vn", x: 32, y: 165},
{name: "vu", x: 48, y: 165},
{name: "wales", x: 64, y: 165},
{name: "wf", x: 80, y: 165},
{name: "ws", x: 96, y: 165},
{name: "xk", x: 112, y: 165},
{name: "ye", x: 128, y: 165},
{name: "yt", x: 144, y: 165},
{name: "za", x: 160, y: 165},
{name: "zanzibar", x: 176, y: 165},
{name: "zm", x: 192, y: 165},
{name: "zw", x: 208, y: 165}
];
  
  var defs = svg.append("defs")
    .selectAll("pattern")
    .data(imagePos)
    .enter()
    .append("pattern")
    .attr("width", 16)
    .attr("height", 11)
    .attr("id", function(d){
      return"pattern_" + d.name;
  });
  
  defs.append("image")
    .attr("xlink:href", "https://drive.google.com/uc?export=download&id=0B8_3FL-6NZmAeXdJdlJ0REVzRWs")
    .attr("x", function(d){
      return -d.x;
    })
    .attr("y", function(d){
      return -d.y;
    })
    .attr("width", "256")
    .attr("height", "176");
  
  var link = svg.selectAll('.link')
    .data(json.links)
    .enter().append('line')
    .attr('class', 'link');

 var node = svg.selectAll('node')
    .data(json.nodes)
    .enter().append('g')
    .attr('class', 'node')
    .call(force.drag()
          .on("dragstart",function(){
      bDragging = true
    })
         .on("dragend",function(){bDragging=false}))
 
///PROBLEM HERE!!//var fo = node.append("rect") //events working, but no css sprite possible
node.append("rect")
  .attr("width", 16)
  .attr("height", 11)
  .style("stroke", "none")
  .attr("fill", function(d){
    return"url(#pattern_" + d.code + ")";
  })
 
 //css spritesheet, but events are disabled//var fo = node.append("image") //separate svg images, events working, but lots of server calls - for each image

  force.on('tick', function() { 
  
    if (force.alpha()<0.3) {
      /*
    fo        
        .attr('height', w/100)
        .attr("width",w/70)
        .attr("class", function(d){
      return "flag flag-" + d.code
    }) //CSS sprite
        .attr('x', function(d) { return d.x; })
        .attr('y', function(d) { return d.y; })
   //     .attr("xlink:href",function(d){
    //    return "http://hewgill.com/flags/"+d.code+".svg"
 //   }) // SVG images
       */
    node.attr("transform", function(d){
      return"translate(" + d.x + "," + d.y + ")";
    })
      
    link.attr('x1', function(d) { return d.source.x; })
        .attr('y1', function(d) { return d.source.y; })
        .attr('x2', function(d) { return d.target.x; })
        .attr('y2', function(d) { return d.target.y; });
  }
  
});
  
 node.on("mouseover",function(d) {
    if (!bDragging) {
      d3.select(".my-popup").html(d.country)
    d3.select(".my-popup").classed("hidden",false)
    }
    
 })
         .on("mousemove",function(d){
          
            d3.select(".my-popup").style('top', (d3.event.layerY + padding*2 + 5) + 'px')
        .style('left', (d3.event.layerX  + padding + 5) + 'px')
        })
        .on("mouseout",function(d){
                      d3.select(".my-popup").classed("hidden", true);

        })
}

functionxdragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x, d.fy = d.y;
}

functionxdragged(d) {
  d.fx = d3.event.x, d.fy = d3.event.y;
}

functionxdragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null, d.fy = null;
}











functiondrawLegend() {
  //get color valuesvar arrThresholds = [];
  var dom = colorScale.domain()
  var len = (dom[0] - dom[1]) / colorScale.range().length
  colorScale.range().map(function(item, index) {
    console.log(index * len + "-" + (index + 1) * len, item)
    arrThresholds.push([index * len, (index + 1) * len])
  })

  var legendRectSize = 20;
  var legendSpacing = 2;
  var startX = w * 8 / 9;
  var startY = h - padding + 35;
  var legend = svg.selectAll('.legend') // NEW
    .data(colorScale.range()) // NEW
    .enter() // NEW
    .append('g') // NEW
    .attr('class', 'legend')
    .attr("fill", function(d, i) {
      //console.log(d)return d
    }) // NEW
    .attr('transform', function(d, i) { // NEWvar horz = startX - i * (legendRectSize + legendSpacing);
      return'translate(' + horz + ',' + startY + ')'; // NEW
    }); // NEW
  legend.append('rect') // NEW
    .attr('width', legendRectSize) // NEW
    .attr('height', legendRectSize) // NEW
    .style('fill', colorScale) // NEW
    .style('stroke', colorScale)
    .on("mouseover", function(d, i) {
      d3.select(".my-popup").html(arrThresholds[colorScale.range().length - 1 - i][0] + "-" + arrThresholds[colorScale.range().length - 1 - i][1] + "&deg;C")

      d3.select(".my-popup").classed("hidden", false)

    })
    .on("mousemove", function(d) {
      d3.select(".my-popup").style('top', (d3.event.layerY + padding * 2 + 20) + 'px')
        .style('left', (d3.event.layerX + padding + 20) + 'px')
    })
    .on("mouseout", function(d) {
      d3.select(".my-popup").classed("hidden", true);

    })
  svg.append("text")
    .attr("x", startX + 25)
    .attr("y", startY + legendRectSize / 1.5)
    .text("hotter")

  svg.append("text")
    .attr("x", startX - colorScale.range().length * legendRectSize - 40)
    .attr("y", startY + legendRectSize / 1.5)
    .text("colder")
    // NEW//svg.append('rect').transform("translate(300,300)")
}

functionround(number, decimals) {
  return +(Math.round(number + "e+" + decimals) + "e-" + decimals);
}
// NEW
.title {
  text-align: center;
  font-size: xx-large;
  color: grey;
}
.subtitle {
  @extend .title;
  font-size: medium;
}
.circle {
  stroke: grey;
}


svg .bar {
  padding: 1px;
  margin: auto;
  fill: blue;
}
.axis path,
.axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}
.yAxis {
  @extend .axis;
}
.yAxis path,line {
  stroke: none;
}
.axis text {
    font-family: sans-serif;
    font-size: 11px;
}
.my-tooltip {
  background: rgba(250,250,250,0.95);
  border-radius: 5%;
  box-shadow: 005px#999999;
  color: grey;
  left: 30px;
  padding: 5px;
  position: absolute;
  text-align: center;
  top: 60px;
  //width: 350px;
  display: block;
}
.my-tooltip.hidden {
  display:none
}
.tooltip-allegation {
  font-size: small;
  color:black;
  font-style: italic;
}
.tooltip-time {
  font-size: medium
}
.tooltip-name {
  font-size: large
}
.my-popup {
  background: white;
  border-radius: 5%;
  box-shadow: 005px#999999;
  color: black;
  left: 30px;
  padding: 5px;
  position: absolute;
  text-align: center;
  top: 60px;
  //width: 210px;
  display: block;
}
.my-popup.hidden {
  display: none;
}
.desc {
  text-align: left;
  font-size: x-small;
  color: blue;
  //width: 1000px;
  margin: 10px50px;
}
.svg-container {
    display: inline-block;
    position: relative;
    width: 100%;
    padding: 50px;
    vertical-align: top;
    overflow: hidden;
}
.svg-content {
    display: inline-block;
    position: relative;
    top: 0;
    left: 0;
    background-color: rgba(0, 158, 150, 0.8)
}
.sources,.note{
  font-style: italic;
}
.node {
    fill: #ccc;
    stroke: #fff;
    stroke-width: 2px;
}

.link {
    stroke: #777;
    stroke-width: 2px;
}
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script><svg></svg>

Post a Comment for "Adding Foreignobjects To D3 Force-directed Graph Nodes Breaks Events"