John Trengrove

home posts about

d3.js and external SVGs

23 Nov 2014

Here is another d3.js animation I have been working on for a project.

There are a few tricks to working with external SVG images using d3.js. The first is how to include the SVG on the page so that you can query and modify its attributes using statements like d3.select(). You need to use the d3.xml() function to load the external SVG, simply including it as an image hides it from the DOM and d3. For an example of this, take a look here.

Another thing to realise is that the width and height of the SVG are not related to the width and height of it on the page. If you take a look at a raw exported SVG from an image program you will see it has a viewbox attribute that describes the actual width and height of the SVG. This is important because any transforms or animations you do will all reference these coordinates.

I animated the electrons by transforming the elements using the formula for an ellipse. The tricky part was modifying the SVG so the electrons would go behind the nucleus. Svgs do not have a z-index (if you know css). Everything is based on the ordering of elements. So to move the electrons to the back I had to make them switch containers at a certain point so that they would be higher in DOM and so drawn first.

There is talk of an SVG render-order attribute but I am not certain if it has been implemented in browsers yet.

var redElectron = {
  theta: 0,
  draw: function() {
    if (redElectron.theta > Math.PI*2) {
      redElectron.theta = 0;
    }
    if (redElectron.theta > Math.PI) {
      var removed = d3.selectAll('.red-electron').remove();
      d3.selectAll('.red-far').append(function() {
        return removed.node();
      });
    }
    else {
      var removed = d3.selectAll('.red-electron').remove();
      d3.selectAll('.red-close').append(function() {
        return removed.node();
      });
    }

    var additional = "translate(" + 100*Math.cos(redElectron.theta) + "," + 5*Math.sin(redElectron.theta) +")";
    d3.selectAll('.red-electron').attr("transform", "rotate(35,250,250) translate(-95,75) " + additional);
    redElectron.theta += 0.05;
  }
}

var purpleElectron = {
  theta: 0,
  draw: function() {
    if (purpleElectron.theta > Math.PI*2) {
      purpleElectron.theta = 0;
    }
    if (purpleElectron.theta > Math.PI) {
      var removed = d3.selectAll('.purple-electron').remove();
      d3.selectAll('.purple-far').append(function() {
        return removed.node();
      });
    }
    else {
      var removed = d3.selectAll('.purple-electron').remove();
      d3.selectAll('.purple-close').append(function() {
        return removed.node();
      });
    }

    var additional = "translate("+85*Math.cos(purpleElectron.theta)+","+5*Math.sin(purpleElectron.theta)+")";
    d3.selectAll('.purple-electron').attr("transform", "translate(10,-60) rotate(120,94,90) "+additional);
    purpleElectron.theta += 0.05;
  }
}

var dash = {
  offset: 0,
  draw: function() {
    if (dash.offset > 4.8) dash.offset = 0;
    d3.selectAll('.electron-line').attr('stroke-dashoffset',dash.offset);
    dash.offset += 0.1;
  }
}

d3.xml("/img/atom.svg", "image/svg+xml", function(xml) {
  document.getElementById('atom').appendChild(xml.documentElement);
  window.setInterval(redElectron.draw,50);
  window.setInterval(purpleElectron.draw,45);
  window.setInterval(dash.draw,45);
});