Skip to content

Instantly share code, notes, and snippets.

@frankleonrose
Last active January 3, 2016 09:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frankleonrose/8442694 to your computer and use it in GitHub Desktop.
Save frankleonrose/8442694 to your computer and use it in GitHub Desktop.
Skelos Plot
<!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