block by emeeks 531f107a0ff6eff5d543

Orbital Layout 1

Full Screen

This is an early draft of a hierarchical orbital layout. Like other hierarchical layouts (pack, tree, treemap, etc) It takes nested data annotates it with xy values for display, in this case arranging the data into orbits, with child nodes orbiting parents and the root node at the center.

You can set the layout size as an array but for now only circular orbits are supported (no ellipses) and as such only the first value in the array is honored.

It will likely improve from this initial version but it’s already got orbital rings and animation.

index.html

<html xmlns="//www.w3.org/1999/xhtml">
<head>
  <title>Orbits 1</title>
  <meta charset="utf-8" />
</head>
<style>

  #viz, svg {
    width: 500px;
    height: 500px;
  }
  
</style>
<script>


function makeViz() {
nodes = [];

///All of this is just to fake some nested data
  randomCountry = d3.scale.quantize().domain([0,1]).range(["USA", "FRA", "MEX", "GBR", "CAN"])
  randomStatus = d3.scale.quantize().domain([0,1]).range(["amazing","okay", "cool", "boss", "dope", "lame"])
  randomRole = d3.scale.quantize().domain([0,1]).range(["capital","metropole", "port"])
  trafficCategories = ["high","medium","low","fargo"];
  quantizeTraffic = d3.scale.quantize().domain([0,500]).range(trafficCategories);

  //200 random things with random categorical attributes
  nodes = d3.range(200).map(function(d,i) {return {i: i} })

  nodes.forEach(function (node) {
    node.country = randomCountry(Math.random());
    node.status = randomStatus(Math.random());
    node.traffic = parseInt(Math.random() * 500);
    node.trafficRank = quantizeTraffic(node.traffic);
    node.role = randomRole(Math.random())
  })

  var nest = d3.nest()
  .key(function(d) {return d.country})
  .key(function(d) {return d.trafficRank})
  .key(function(d) {return d.status})
  .key(function(d) {return d.role})

  var awesomeFakeNestedData = nest.entries(nodes);

//If you already have some nested data, just send it to drawOrbit

  drawOrbit(awesomeFakeNestedData)
}

function drawOrbit(_data) {

  //down with category20a()!!
  colors = d3.scale.category20b();

  orbit = d3.layout.orbit().size([500,500]).nodes(_data);

  d3.select("svg").selectAll("circle").data(orbit.nodes())
  .enter()
  .append("circle")
  .attr("r", function(d) {return Math.max(1, 5 - d.depth)})
  .attr("cx", function(d) {return d.x})
  .attr("cy", function(d) {return d.y})
  .style("fill", function(d) {return colors(d.depth)})




}

</script>
<body onload="makeViz()">
<div id="viz"><svg></svg></div>
<footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="d3.layout.orbit.js" charset="utf-8" type="text/javascript"></script>
</footer>
</body>
</html>

d3.layout.orbit.js

d3.layout.orbit = function() {
	var currentTickStep = 0;
	var orbitNodes;
	var orbitSize = [1,1];
	var nestedNodes;
	var flattenedNodes = [];
	var tickRadianStep = 0.004363323129985824;
    var orbitDispatch = d3.dispatch('tick');
    var tickInterval;
    var orbitalRings = [];
    var orbitDepthAdjust = 2.95;

	function _orbitLayout(_data) {

		return _orbitLayout;
	}

	_orbitLayout.mode = function() {
		//Atomic, Solar, other?
	}

	_orbitLayout.start = function() {
		//activate animation here
		tickInterval = setInterval(
			function() {
			currentTickStep++;
			flattenedNodes.forEach(function(_node){
				if (_node.parent) {
					_node.x = _node.parent.x + ( (_node.parent.ring / 2) * Math.sin( _node.angle + (currentTickStep * tickRadianStep)) );
					_node.y = _node.parent.y + ( (_node.parent.ring / 2) * Math.cos( _node.angle + (currentTickStep * tickRadianStep)) );
				}
			})
			orbitalRings.forEach(function(_ring) {
				_ring.x = _ring.source.x;
				_ring.y = _ring.source.y;
			})
			orbitDispatch.tick();
		}, 
		10);
	}

	_orbitLayout.stop = function() {
		//deactivate animation here
		clearInterval(tickInterval);
	}

	_orbitLayout.speed = function(_degrees) {
		if (!arguments.length) return tickRadianStep / (Math.PI / 360);
		tickRadianStep = tickRadianStep = _degrees * (Math.PI / 360);
		return this;
	}

	_orbitLayout.size = function(_value) {
		if (!arguments.length) return orbitSize;
		orbitSize = _value;
		return this;
		//change size here
	}

	_orbitLayout.orbitSize = function(_value) {
		//change ring size reduction (make that into dynamic function)
		if (!arguments.length) return orbitDepthAdjust;
		orbitDepthAdjust = _value;
		return this
	}

	_orbitLayout.orbitalRings = function() {
		//return an array of data corresponding to orbital rings
		if (!arguments.length) return orbitalRings;
		return this;
	}

	_orbitLayout.nodes = function(_data) {
    	if (!arguments.length) return flattenedNodes;
    	nestedNodes = _data;
    	calculateNodes();
		return this;
	}

    d3.rebind(_orbitLayout, orbitDispatch, "on");

	return _orbitLayout;
	function calculateNodes() {
		var _data = nestedNodes; 
	//If you have an array of elements, then create a root node (center)
		//In the future, maybe make a binary star kind of thing?
		if (!_data.values) {
			orbitNodes = {key: "root", values: _data}
			orbitNodes.values.forEach(function (_node) {
				_node.parent = orbitNodes;
			})
		}
		//otherwise assume it is an object with a root node
		else {
			orbitNodes = _data;
		}
			orbitNodes.x = orbitSize[0] / 2;
			orbitNodes.y = orbitSize[1] / 2;
			orbitNodes.deltaX = function(_x) {return _x}
			orbitNodes.deltaY = function(_y) {return _y}
			orbitNodes.ring = orbitSize[0] / 2;
			orbitNodes.depth = 0;

			flattenedNodes.push(orbitNodes);

			traverseNestedData(orbitNodes)

		function traverseNestedData(_node) {
			if(_node.values) {
				var thisPie = d3.layout.pie().value(function() {return 1});
				var piedValues = thisPie(_node.values);

				orbitalRings.push({source: _node, x: _node.x, y: _node.y, r: _node.ring / 2});

				for (var x = 0; x<_node.values.length;x++) {

					_node.values[x].angle = piedValues[x].endAngle;
					_node.values[x].parent = _node;
					_node.values[x].depth = _node.depth + 1;

					_node.values[x].x = _node.values[x].parent.x + ( (_node.values[x].parent.ring / 2) * Math.sin( _node.values[x].angle ) );
					_node.values[x].y = _node.values[x].parent.y + ( (_node.values[x].parent.ring / 2) * Math.cos( _node.values[x].angle ) );

					_node.values[x].deltaX = function(_x) {return _x}
					_node.values[x].deltaY = function(_y) {return _y}
					_node.values[x].ring = _node.values[x].parent.ring / orbitDepthAdjust;
					flattenedNodes.push(_node.values[x]);
					traverseNestedData(_node.values[x]);
				}
			}
		}
	}

}