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:

var imagePos = [{
      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 = 70
var svgOffset = 100
var circleRadius = 10
var 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);

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

function initializeViewBox() {
  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)
  
}

function initializeForce(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)
}
function prepJson(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)
}
function drawNodesAndLinks(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);

        })
}

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

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

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











function drawLegend() {
  //get color values

  var 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) { // NEW
      var 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)")
}

function round(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: 0 0 5px #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: 0 0 5px #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: 10px 50px;
}
.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;
}
<script src="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"