// Parallel Sets by Jason Davies, http://www.jasondavies.com/
// Functionality based on http://eagereyes.org/parallel-sets
(function() {
d3.parsets = function() {
var event = d3.dispatch("sortDimensions", "sortCategories"),
dimensions_ = autoDimensions,
dimensionFormat = String,
tooltip_ = defaultTooltip,
categoryTooltip = defaultCategoryTooltip,
value_,
spacing = 20,
width,
height,
tension = 1,
tension0,
duration = 500;
function parsets(selection) {
selection.each(function(data, i) {
var g = d3.select(this),
ordinal = d3.scale.ordinal(),
dragging = false,
dimensionNames = dimensions_.call(this, data, i),
dimensions = [],
tree = {children: {}, count: 0},
nodes,
total,
ribbon;
d3.select(window).on("mousemove.parsets." + ++parsetsId, unhighlight);
if (tension0 == null) tension0 = tension;
g.selectAll(".ribbon, .ribbon-mouse")
.data(["ribbon", "ribbon-mouse"], String)
.enter().append("g")
.attr("class", String);
updateDimensions();
if (tension != tension0) {
var t = d3.transition(g);
if (t.tween) t.tween("ribbon", tensionTween);
else tensionTween()(1);
}
function tensionTween() {
var i = d3.interpolateNumber(tension0, tension);
return function(t) {
tension0 = i(t);
ribbon.attr("d", ribbonPath);
};
}
function updateDimensions() {
// Cache existing bound dimensions to preserve sort order.
var dimension = g.selectAll("g.dimension"),
cache = {};
dimension.each(function(d) { cache[d.name] = d; });
dimensionNames.forEach(function(d) {
if (!cache.hasOwnProperty(d)) {
cache[d] = {name: d, categories: []};
}
dimensions.push(cache[d]);
});
dimensions.sort(compareY);
// Populate tree with existing nodes.
g.select(".ribbon").selectAll("path")
.each(function(d) {
var path = d.path.split("\0"),
node = tree,
n = path.length - 1;
for (var i = 0; i < n; i++) {
var p = path[i];
node = node.children.hasOwnProperty(p) ? node.children[p]
: node.children[p] = {children: {}, count: 0};
}
node.children[d.name] = d;
});
tree = buildTree(tree, data, dimensions.map(dimensionName), value_);
cache = dimensions.map(function(d) {
var t = {};
d.categories.forEach(function(c) {
t[c.name] = c;
});
return t;
});
(function categories(d, i) {
if (!d.children) return;
var dim = dimensions[i],
t = cache[i];
for (var k in d.children) {
if (!t.hasOwnProperty(k)) {
dim.categories.push(t[k] = {name: k});
}
categories(d.children[k], i + 1);
}
})(tree, 0);
ordinal.domain([]).range(d3.range(dimensions[0].categories.length));
nodes = layout(tree, dimensions, ordinal);
total = getTotal(dimensions);
dimensions.forEach(function(d) {
d.count = total;
});
dimension = dimension.data(dimensions, dimensionName);
var dEnter = dimension.enter().append("g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(0," + d.y + ")"; })
.on("mousedown.parsets", cancelEvent);
dimension.each(function(d) {
d.y0 = d.y;
d.categories.forEach(function(d) { d.x0 = d.x; });
});
dEnter.append("rect")
.attr("width", width)
.attr("y", -45)
.attr("height", 45);
var textEnter = dEnter.append("text")
.attr("class", "dimension")
.attr("transform", "translate(0,-25)");
textEnter.append("tspan")
.attr("class", "name")
.text(dimensionFormatName);
textEnter.append("tspan")
.attr("class", "sort alpha")
.attr("dx", "2em")
.text("alpha »")
.on("mousedown.parsets", cancelEvent);
textEnter.append("tspan")
.attr("class", "sort size")
.attr("dx", "2em")
.text("size »")
.on("mousedown.parsets", cancelEvent);
dimension
.call(d3.behavior.drag()
.origin(identity)
.on("dragstart", function(d) {
dragging = true;
d.y0 = d.y;
})
.on("drag", function(d) {
d.y0 = d.y = d3.event.y;
for (var i = 1; i < dimensions.length; i++) {
if (height * dimensions[i].y < height * dimensions[i - 1].y) {
dimensions.sort(compareY);
dimensionNames = dimensions.map(dimensionName);
ordinal.domain([]).range(d3.range(dimensions[0].categories.length));
nodes = layout(tree = buildTree({children: {}, count: 0}, data, dimensionNames, value_), dimensions, ordinal);
total = getTotal(dimensions);
g.selectAll(".ribbon, .ribbon-mouse").selectAll("path").remove();
updateRibbons();
updateCategories(dimension);
dimension.transition().duration(duration)
.attr("transform", translateY)
.tween("ribbon", ribbonTweenY);
event.sortDimensions();
break;
}
}
d3.select(this)
.attr("transform", "translate(0," + d.y + ")")
.transition();
ribbon.filter(function(r) { return r.source.dimension === d || r.target.dimension === d; })
.attr("d", ribbonPath);
})
.on("dragend", function(d) {
dragging = false;
unhighlight();
var y0 = 45,
dy = (height - y0 - 2) / (dimensions.length - 1);
dimensions.forEach(function(d, i) {
d.y = y0 + i * dy;
});
transition(d3.select(this))
.attr("transform", "translate(0," + d.y + ")")
.tween("ribbon", ribbonTweenY);
}));
dimension.select("text").select("tspan.sort.alpha")
.on("click.parsets", sortBy("alpha", function(a, b) { return a.name < b.name ? 1 : -1; }, dimension));
dimension.select("text").select("tspan.sort.size")
.on("click.parsets", sortBy("size", function(a, b) { return a.count - b.count; }, dimension));
dimension.transition().duration(duration)
.attr("transform", function(d) { return "translate(0," + d.y + ")"; })
.tween("ribbon", ribbonTweenY);
dimension.exit().remove();
updateCategories(dimension);
updateRibbons();
}
function sortBy(type, f, dimension) {
return function(d) {
var direction = this.__direction = -(this.__direction || 1);
d3.select(this).text(direction > 0 ? type + " »" : "« " + type);
d.categories.sort(function() { return direction * f.apply(this, arguments); });
nodes = layout(tree, dimensions, ordinal);
updateCategories(dimension);
updateRibbons();
event.sortCategories();
};
}
function updateRibbons() {
ribbon = g.select(".ribbon").selectAll("path")
.data(nodes, function(d) { return d.path; });
ribbon.enter().append("path")
.each(function(d) {
d.source.x0 = d.source.x;
d.target.x0 = d.target.x;
})
.attr("class", function(d) { return "category-" + d.major; })
.attr("d", ribbonPath);
ribbon.sort(function(a, b) { return b.count - a.count; });
ribbon.exit().remove();
var mouse = g.select(".ribbon-mouse").selectAll("path")
.data(nodes, function(d) { return d.path; });
mouse.enter().append("path")
.on("mousemove.parsets", function(d) {
ribbon.classed("active", false);
if (dragging) return;
highlight(d = d.node, true);
showTooltip(tooltip_.call(this, d));
d3.event.stopPropagation();
});
mouse
.sort(function(a, b) { return b.count - a.count; })
.attr("d", ribbonPathStatic);
mouse.exit().remove();
}
// Animates the x-coordinates only of the relevant ribbon paths.
function ribbonTweenX(d) {
var nodes = [d],
r = ribbon.filter(function(r) {
var s, t;
if (r.source.node === d) nodes.push(s = r.source);
if (r.target.node === d) nodes.push(t = r.target);
return s || t;
}),
i = nodes.map(function(d) { return d3.interpolateNumber(d.x0, d.x); }),
n = nodes.length;
return function(t) {
for (var j = 0; j < n; j++) nodes[j].x0 = i[j](t);
r.attr("d", ribbonPath);
};
}
// Animates the y-coordinates only of the relevant ribbon paths.
function ribbonTweenY(d) {
var r = ribbon.filter(function(r) { return r.source.dimension.name == d.name || r.target.dimension.name == d.name; }),
i = d3.interpolateNumber(d.y0, d.y);
return function(t) {
d.y0 = i(t);
r.attr("d", ribbonPath);
};
}
// Highlight a node and its descendants, and optionally its ancestors.
function highlight(d, ancestors) {
if (dragging) return;
var highlight = [];
(function recurse(d) {
highlight.push(d);
for (var k in d.children) recurse(d.children[k]);
})(d);
highlight.shift();
if (ancestors) while (d) highlight.push(d), d = d.parent;
ribbon.filter(function(d) {
var active = highlight.indexOf(d.node) >= 0;
if (active) this.parentNode.appendChild(this);
return active;
}).classed("active", true);
}
// Unhighlight all nodes.
function unhighlight() {
if (dragging) return;
ribbon.classed("active", false);
hideTooltip();
}
function updateCategories(g) {
var category = g.selectAll("g.category")
.data(function(d) { return d.categories; }, function(d) { return d.name; });
var categoryEnter = category.enter().append("g")
.attr("class", "category")
.attr("transform", function(d) { return "translate(" + d.x + ")"; });
category.exit().remove();
category
.on("mousemove.parsets", function(d) {
ribbon.classed("active", false);
if (dragging) return;
d.nodes.forEach(function(d) { highlight(d); });
showTooltip(categoryTooltip.call(this, d));
d3.event.stopPropagation();
})
.on("mouseout.parsets", unhighlight)
.on("mousedown.parsets", cancelEvent)
.call(d3.behavior.drag()
.origin(identity)
.on("dragstart", function(d) {
dragging = true;
d.x0 = d.x;
})
.on("drag", function(d) {
d.x = d3.event.x;
var categories = d.dimension.categories;
for (var i = 0, c = categories[0]; ++i < categories.length;) {
if (c.x + c.dx / 2 > (c = categories[i]).x + c.dx / 2) {
categories.sort(function(a, b) { return a.x + a.dx / 2 - b.x - b.dx / 2; });
nodes = layout(tree, dimensions, ordinal);
updateRibbons();
updateCategories(g);
highlight(d.node);
event.sortCategories();
break;
}
}
var x = 0,
p = spacing / (categories.length - 1);
categories.forEach(function(e) {
if (d === e) e.x0 = d3.event.x;
e.x = x;
x += e.count / total * (width - spacing) + p;
});
d3.select(this)
.attr("transform", function(d) { return "translate(" + d.x0 + ")"; })
.transition();
ribbon.filter(function(r) { return r.source.node === d || r.target.node === d; })
.attr("d", ribbonPath);
})
.on("dragend", function(d) {
dragging = false;
unhighlight();
updateRibbons();
transition(d3.select(this))
.attr("transform", "translate(" + d.x + ")")
.tween("ribbon", ribbonTweenX);
}));
category.transition().duration(duration)
.attr("transform", function(d) { return "translate(" + d.x + ")"; })
.tween("ribbon", ribbonTweenX);
categoryEnter.append("rect")
.attr("width", function(d) { return d.dx; })
.attr("y", -20)
.attr("height", 20);
categoryEnter.append("line")
.style("stroke-width", 2);
categoryEnter.append("text")
.attr("dy", "-.3em");
category.select("rect")
.attr("width", function(d) { return d.dx; })
.attr("class", function(d) {
return "category-" + (d.dimension === dimensions[0] ? ordinal(d.name) : "background");
});
category.select("line")
.attr("x2", function(d) { return d.dx; });
category.select("text")
.text(truncateText(function(d) { return d.name; }, function(d) { return d.dx; }));
}
});
}
parsets.dimensionFormat = function(_) {
if (!arguments.length) return dimensionFormat;
dimensionFormat = _;
return parsets;
};
parsets.dimensions = function(_) {
if (!arguments.length) return dimensions_;
dimensions_ = d3.functor(_);
return parsets;
};
parsets.value = function(_) {
if (!arguments.length) return value_;
value_ = d3.functor(_);
return parsets;
};
parsets.width = function(_) {
if (!arguments.length) return width;
width = +_;
return parsets;
};
parsets.height = function(_) {
if (!arguments.length) return height;
height = +_;
return parsets;
};
parsets.spacing = function(_) {
if (!arguments.length) return spacing;
spacing = +_;
return parsets;
};
parsets.tension = function(_) {
if (!arguments.length) return tension;
tension = +_;
return parsets;
};
parsets.duration = function(_) {
if (!arguments.length) return duration;
duration = +_;
return parsets;
};
parsets.tooltip = function(_) {
if (!arguments.length) return tooltip;
tooltip = _ == null ? defaultTooltip : _;
return parsets;
};
parsets.categoryTooltip = function(_) {
if (!arguments.length) return categoryTooltip;
categoryTooltip = _ == null ? defaultCategoryTooltip : _;
return parsets;
};
var body = d3.select("body");
var tooltip = body.append("div")
.style("display", "none")
.attr("class", "parsets tooltip");
return d3.rebind(parsets, event, "on").value(1).width(400).height(400);
function dimensionFormatName(d, i) {
return dimensionFormat.call(this, d.name, i);
}
function showTooltip(html) {
var m = d3.mouse(body.node());
tooltip
.style("display", null)
.style("left", m[0] + 30 + "px")
.style("top", m[1] - 20 + "px")
.html(html);
}
function hideTooltip() {
tooltip.style("display", "none");
}
function transition(g) {
return duration ? g.transition().duration(duration).ease(parsetsEase) : g;
}
function layout(tree, dimensions, ordinal) {
var nodes = [],
nd = dimensions.length,
y0 = 45,
dy = (height - y0 - 2) / (nd - 1);
dimensions.forEach(function(d, i) {
d.categories.forEach(function(c) {
c.dimension = d;
c.count = 0;
c.nodes = [];
});
d.y = y0 + i * dy;
});
// Compute per-category counts.
var total = (function rollup(d, i) {
if (!d.children) return d.count;
var dim = dimensions[i],
total = 0;
dim.categories.forEach(function(c) {
var child = d.children[c.name];
if (!child) return;
c.nodes.push(child);
var count = rollup(child, i + 1);
c.count += count;
total += count;
});
return total;
})(tree, 0);
// Stack the counts.
dimensions.forEach(function(d) {
d.categories = d.categories.filter(function(d) { return d.count; });
var x = 0,
p = spacing / (d.categories.length - 1);
d.categories.forEach(function(c) {
c.x = x;
c.dx = c.count / total * (width - spacing);
c.in = {dx: 0};
c.out = {dx: 0};
x += c.dx + p;
});
});
var dim = dimensions[0];
dim.categories.forEach(function(c) {
var k = c.name;
if (tree.children.hasOwnProperty(k)) {
recurse(c, {node: tree.children[k], path: k}, 1, ordinal(k));
}
});
function recurse(p, d, depth, major) {
var node = d.node,
dimension = dimensions[depth];
dimension.categories.forEach(function(c) {
var k = c.name;
if (!node.children.hasOwnProperty(k)) return;
var child = node.children[k];
child.path = d.path + "\0" + k;
var target = child.target || {node: c, dimension: dimension};
target.x = c.in.dx;
target.dx = child.count / total * (width - spacing);
c.in.dx += target.dx;
var source = child.source || {node: p, dimension: dimensions[depth - 1]};
source.x = p.out.dx;
source.dx = target.dx;
p.out.dx += source.dx;
child.node = child;
child.source = source;
child.target = target;
child.major = major;
nodes.push(child);
if (depth + 1 < dimensions.length) recurse(c, child, depth + 1, major);
});
}
return nodes;
}
// Dynamic path string for transitions.
function ribbonPath(d) {
var s = d.source,
t = d.target;
return ribbonPathString(s.node.x0 + s.x0, s.dimension.y0, s.dx, t.node.x0 + t.x0, t.dimension.y0, t.dx, tension0);
}
// Static path string for mouse handlers.
function ribbonPathStatic(d) {
var s = d.source,
t = d.target;
return ribbonPathString(s.node.x + s.x, s.dimension.y, s.dx, t.node.x + t.x, t.dimension.y, t.dx, tension);
}
function ribbonPathString(sx, sy, sdx, tx, ty, tdx, tension) {
var m0, m1;
return (tension === 1 ? [
"M", [sx, sy],
"L", [tx, ty],
"h", tdx,
"L", [sx + sdx, sy],
"Z"]
: ["M", [sx, sy],
"C", [sx, m0 = tension * sy + (1 - tension) * ty], " ",
[tx, m1 = tension * ty + (1 - tension) * sy], " ", [tx, ty],
"h", tdx,
"C", [tx + tdx, m1], " ", [sx + sdx, m0], " ", [sx + sdx, sy],
"Z"]).join("");
}
function compareY(a, b) {
a = height * a.y, b = height * b.y;
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : a <= a ? -1 : b <= b ? 1 : NaN;
}
};
d3.parsets.tree = buildTree;
function autoDimensions(d) {
return d.length ? d3.keys(d[0]).sort() : [];
}
function cancelEvent() {
d3.event.stopPropagation();
d3.event.preventDefault();
}
function dimensionName(d) { return d.name; }
function getTotal(dimensions) {
return dimensions[0].categories.reduce(function(a, d) {
return a + d.count;
}, 0);
}
// Given a text function and width function, truncates the text if necessary to
// fit within the given width.
function truncateText(text, width) {
return function(d, i) {
var t = this.textContent = text(d, i),
w = width(d, i);
if (this.getComputedTextLength() < w) return t;
this.textContent = "…" + t;
var lo = 0,
hi = t.length + 1,
x;
while (lo < hi) {
var mid = lo + hi >> 1;
if ((x = this.getSubStringLength(0, mid)) < w) lo = mid + 1;
else hi = mid;
}
return lo > 1 ? t.substr(0, lo - 2) + "…" : "";
};
}
var percent = d3.format("%"),
comma = d3.format(",f"),
parsetsEase = "elastic",
parsetsId = 0;
// Construct tree of all category counts for a given ordered list of
// dimensions. Similar to d3.nest, except we also set the parent.
function buildTree(root, data, dimensions, value) {
zeroCounts(root);
var n = data.length,
nd = dimensions.length;
for (var i = 0; i < n; i++) {
var d = data[i],
v = value(d, i),
node = root;
for (var j = 0; j < nd; j++) {
var dimension = dimensions[j],
category = d[dimension],
children = node.children;
node.count += v;
node = children.hasOwnProperty(category) ? children[category]
: children[category] = {
children: j === nd - 1 ? null : {},
count: 0,
parent: node,
dimension: dimension,
name: category
};
}
node.count += v;
}
return root;
}
function zeroCounts(d) {
d.count = 0;
if (d.children) {
for (var k in d.children) zeroCounts(d.children[k]);
}
}
function identity(d) { return d; }
function translateY(d) { return "translate(0," + d.y + ")"; }
function defaultTooltip(d) {
var count = d.count,
path = [];
while (d.parent) {
if (d.name) path.unshift(d.name);
d = d.parent;
}
return path.join(" → ") + "
" + comma(count) + " (" + percent(count / d.count) + ")";
}
function defaultCategoryTooltip(d) {
return d.name + "
" + comma(d.count) + " (" + percent(d.count / d.dimension.count) + ")";
}
})();