Last active
January 3, 2016 09:29
-
-
Save frankleonrose/8442694 to your computer and use it in GitHub Desktop.
Skelos Plot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<title>Skelos Plot</title> | |
<style> | |
body { | |
font: 10px sans-serif; | |
} | |
.chord path { | |
fill-opacity: .67; | |
stroke: #000; | |
stroke-width: .5px; | |
} | |
</style> | |
<body> | |
<H1>Skelos Plot</H1> | |
<p>A Skelos Plot represents sets (the multi-legged shapes) that belong to one or more groups (the arcs around the edge).</p> | |
<p>The input data (at bottom) is a hash of {groups: [...], skels: [...]}. The interesting field of each group object is its 'size'. | |
The legged shapes are each defined simply with an array of objects detailing the groups they connect to. | |
In the code I refer to where the legs touch the groups as a 'foot'. Each foot has a 'group' (index into group array), | |
'start' (where on the group it is placed), and 'size' (the size of the foot). | |
</p> | |
<p>The name "Skelos" comes from combining "<a href="http://en.wikipedia.org/wiki/Triskelion">Triskelion</a>" and "<a href="http://circos.ca/">Circos</a>". I dropped the "tri" prefix because sets can have more and fewer than three legs.</p> | |
<p>This implementation is derived from Mike Bostock's <a href="http://bl.ocks.org/mbostock/4062006">Chord Diagram</a>.</p> | |
<p>I have some code in Scala that optimizes the layout to minimize overlapping by sorting groups around the edges by | |
relatedness and sorting where sets touch groups. I'll translate it to JS sometime to make this code a bit more useful.</p> | |
<div id="content"/> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script> | |
d3.skelos = function() { | |
var radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle; | |
function subgroup(self, d, i) { | |
var subgroup = d, r = radius.call(self, subgroup, i), a0 = startAngle | |
.call(self, subgroup, i) | |
+ d3_svg_arcOffset, a1 = endAngle.call(self, subgroup, i) | |
+ d3_svg_arcOffset; | |
return { | |
r : r, | |
a0 : a0, | |
a1 : a1, | |
p0 : [ r * Math.cos(a0), r * Math.sin(a0) ], | |
p1 : [ r * Math.cos(a1), r * Math.sin(a1) ] | |
}; | |
} | |
function skelos(d, i) { | |
var feet = []; | |
d.forEach(function(foot) { | |
var renderFoot = subgroup(this, foot, i); | |
feet.push(renderFoot) | |
}) | |
if (feet.length == 1) { | |
var s = feet[0], result; | |
result = "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) | |
+ curve(s.r, s.p1, s.r, s.p0) + "Z"; | |
//console.log(result); | |
return result; | |
} else if (feet.length > 1) { | |
var s = feet[0], result; | |
var partial = function(s, t) { | |
return arc(s.r, s.p1, s.a1 - s.a0) | |
+ curve(s.r, s.p1, t.r, t.p0) | |
} | |
result = "M" + s.p0 | |
for ( var i = 0, len = feet.length; i < len; i++) { | |
var f1 = feet[i], f2 = feet[(i + 1) % feet.length]; | |
result = result + partial(f1, f2) | |
} | |
//+ arc(s.r, s.p1, s.a1 - s.a0) | |
//+ curve(s.r, s.p1, t.r, t.p0) | |
//+ arc(t.r, t.p1, t.a1 - t.a0) | |
//+ curve(t.r, t.p1, s.r, s.p0) | |
result = result + "Z"; | |
//console.log(result); | |
return result; | |
} else { | |
console.log("Nothing to draw"); | |
console.log(feet) | |
} | |
} | |
function equals(a, b) { | |
return a.a0 == b.a0 && a.a1 == b.a1; | |
} | |
function arc(r, p, a) { | |
return "A" + r + "," + r + " 0 " + +(a > Math.PI) + ",1 " + p; | |
} | |
function curve(r0, p0, r1, p1) { | |
return "Q 0,0 " + p1; | |
} | |
skelos.radius = function(v) { | |
if (!arguments.length) | |
return radius; | |
radius = d3.functor(v); | |
return skelos; | |
}; | |
skelos.startAngle = function(v) { | |
if (!arguments.length) | |
return startAngle; | |
startAngle = d3.functor(v); | |
return skelos; | |
}; | |
skelos.endAngle = function(v) { | |
if (!arguments.length) | |
return endAngle; | |
endAngle = d3.functor(v); | |
return skelos; | |
}; | |
return skelos; | |
} | |
function d3_svg_chordRadius(d) { | |
return d.radius; | |
} | |
var d3_svg_arcOffset = -Math.PI / 2, d3_svg_arcMax = 2 * Math.PI - 1e-6; | |
function d3_svg_arcInnerRadius(d) { | |
return d.innerRadius; | |
} | |
function d3_svg_arcOuterRadius(d) { | |
return d.outerRadius; | |
} | |
function d3_svg_arcStartAngle(d) { | |
return d.startAngle; | |
} | |
function d3_svg_arcEndAngle(d) { | |
return d.endAngle; | |
} | |
window.showSkelos = function(skelos) { | |
//console.log(skelos); | |
var totalSize = 0; | |
skelos.groups.forEach(function(g) { | |
g.index = g.group; | |
g.value = g.size; | |
g.start = totalSize; | |
totalSize += g.size; | |
}) | |
var gap = (2 * Math.PI) / 100; | |
var circumference = (2 * Math.PI) - skelos.groups.length * gap; | |
skelos.groups | |
.forEach(function(g, i) { | |
var space = i * gap; | |
g.startAngle = circumference * g.start / totalSize + space; | |
g.endAngle = circumference * (g.start + g.size) / totalSize | |
+ space; | |
}) | |
//console.log(skelos.groups); | |
skelos.skels.forEach(function(s) { | |
s.forEach(function(f) { | |
var g = skelos.groups[f.group]; | |
f.index = f.group; | |
f.value = f.size; | |
f.startAngle = g.startAngle + (g.endAngle - g.startAngle) | |
* f.start / g.size; | |
f.endAngle = g.startAngle + (g.endAngle - g.startAngle) | |
* (f.start + f.size) / g.size; | |
}) | |
}) | |
//console.log(skelos.skels); | |
var width = 960, height = 500, innerRadius = Math.min(width, height) * .41, outerRadius = innerRadius * 1.1; | |
var fill = d3.scale.ordinal().domain(d3.range(skelos.groups.size)) | |
.range([ "#000000", "#FFDD89", "#957244", "#F26223" ]); | |
var content = d3.select("#content"); | |
//content.clear(); | |
d3.select("svg").remove(); | |
var svg = d3.select("#content").append("svg").attr("width", width) | |
.attr("height", height).append("g").attr("transform", | |
"translate(" + width / 2 + "," + height / 2 + ")"); | |
svg.append("g").selectAll("path").data(skelos.groups).enter().append( | |
"path").style("fill", function(d) { | |
return fill(d.index); | |
}).style("stroke", function(d) { | |
return fill(d.index); | |
}).attr("d", | |
d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius)) | |
.on("mouseover", fade(.1)).on("mouseout", fade(1)); | |
var ticks = svg.append("g").selectAll("g").data(skelos.groups).enter() | |
.append("g").selectAll("g").data(groupTicks).enter() | |
.append("g").attr( | |
"transform", | |
function(d) { | |
return "rotate(" + (d.angle * 180 / Math.PI - 90) | |
+ ")" + "translate(" + outerRadius + ",0)"; | |
}); | |
ticks.append("line").attr("x1", 1).attr("y1", 0).attr("x2", 5).attr( | |
"y2", 0).style("stroke", "#000"); | |
ticks.append("text").attr("x", 8).attr("dy", ".35em").attr( | |
"transform", | |
function(d) { | |
return d.angle > Math.PI ? "rotate(180)translate(-16)" | |
: null; | |
}).style("text-anchor", function(d) { | |
return d.angle > Math.PI ? "end" : null; | |
}).text(function(d) { | |
return d.label; | |
}); | |
svg.append("g").attr("class", "chord").selectAll("path").data( | |
skelos.skels).enter().append("path").attr("d", | |
d3.skelos().radius(innerRadius)).style("fill", | |
function(d) { | |
return fill(d[0].group); | |
}).style("opacity", 1); | |
// Returns an array of tick angles and labels, given a group. | |
function groupTicks(d) { | |
var tickScale = 1; // 1000 | |
var k = (d.endAngle - d.startAngle) / d.value; | |
return d3.range(0, d.value, tickScale).map(function(v, i) { | |
return { | |
angle : v * k + d.startAngle, | |
label : i % 5 ? null : "" + (v / tickScale) | |
}; | |
}); | |
} | |
// Returns an event handler for fading a given chord group. | |
function fade(opacity) { | |
return function(g, i) { | |
svg.selectAll(".chord path").filter(function(d) { | |
for ( var j = 0; j < d.length; ++j) { | |
if (d[j].index == i) | |
return false; | |
} | |
return true; | |
}).transition().style("opacity", opacity); | |
}; | |
} | |
}; | |
(function() { | |
var skelosData = { | |
groups : [ { | |
group : 0, | |
skel : [ 0, 1, 3, 6 ], | |
start : 0, | |
size : 18 | |
}, { | |
group : 1, | |
skel : [ 0, 1, 2, 4 ], | |
start : 0, | |
size : 21 | |
}, { | |
group : 2, | |
skel : [ 2 ], | |
start : 0, | |
size : 10 | |
}, { | |
group : 3, | |
skel : [ 1, 3, 5 ], | |
start : 0, | |
size : 26 | |
} ], | |
skels : [ [ { | |
group : 0, | |
skel : 0, | |
start : 16, | |
size : 2 | |
}, { | |
group : 1, | |
skel : 0, | |
start : 0, | |
size : 2 | |
} ], [ { | |
group : 0, | |
skel : 1, | |
start : 13, | |
size : 3 | |
}, { | |
group : 1, | |
skel : 1, | |
start : 5, | |
size : 6 | |
}, { | |
group : 3, | |
skel : 1, | |
start : 9, | |
size : 9 | |
} ], [ { | |
group : 1, | |
skel : 2, | |
start : 11, | |
size : 10 | |
}, { | |
group : 2, | |
skel : 2, | |
start : 0, | |
size : 10 | |
} ], [ { | |
group : 0, | |
skel : 3, | |
start : 0, | |
size : 8 | |
}, { | |
group : 3, | |
skel : 3, | |
start : 18, | |
size : 8 | |
} ], [ { | |
group : 1, | |
skel : 4, | |
start : 2, | |
size : 3 | |
} ], [ { | |
group : 3, | |
skel : 5, | |
start : 0, | |
size : 9 | |
} ], [ { | |
group : 0, | |
skel : 6, | |
start : 8, | |
size : 5 | |
} ] ] | |
}; | |
//console.log(skelosData); | |
window.showSkelos(skelosData); | |
})(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment