Skip to content

Instantly share code, notes, and snippets.

@cool-Blue
Last active November 20, 2015 07:32
Show Gist options
  • Save cool-Blue/5f135758b27fb1672cfd to your computer and use it in GitHub Desktop.
Save cool-Blue/5f135758b27fb1672cfd to your computer and use it in GitHub Desktop.
Using d3 transitions on dummy nodes to transition data
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/button/style.css">
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.css">
<style>
body {
margin: 0;
position: relative;
}
#vis {
background: steelblue;
}
text {
white-space: pre;
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
cursor: move;
fill: #ccc;
stroke: #000;
stroke-width: 1.5px;
opacity: 0.5;
}
.node.fixed {
opacity: 1;
stroke: red;
}
button, input {display: inline-block}
.input-div {
position: absolute;
top: 0;
left: 0;
/*white-space: pre;*/
margin: 0;
}
#timeDisplay #xAxis {
opacity: 0.6;
}
#timeDisplay .domain, #timeDisplay .tick line {
fill: none;
stroke: black;
}
#timeDisplay .tick text {
font-size: 10px;
}
#timeDisplay {
pointer-events: none;
}
.g-button {
color: #804700;
background: black;
border-color: orange;
}
.g-button.g-active {
color: orange;
background: #333333;
border-color: orange;
}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/transitions/end-all/1.0.0/endAll.js" charset="UTF-8"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/inputs/select/select.js" charset="UTF-8"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/tool-tip/0.0.0/tool-tip.js" charset="UTF-8"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/inputs/number/input-number.js" charset="UTF-8"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/button/2.0.0/button.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/inputs/select/select.js"></script>
<div id="input-div">
<!--
<label><input id="showTimeLines" name="showTimeLines" value="show" type="checkbox">show timelines</label>
-->
<button onclick = 'refresh()'> refresh</button>
steps <input id="steps-selector" onchange = 'refresh()' type="number" name="steps" value = 5 min="1" max="100"/>
</div>
<div id="vis"></div>
<script>
var graph ={
"nodes": [
{"x": 469, "y": 410, move: true},
{"x": 493, "y": 364, move: true},
{"x": 442, "y": 365, move: true},
{"x": 467, "y": 314, move: true},
{"x": 477, "y": 248, move: true},
{"x": 425, "y": 207, move: true},
{"x": 402, "y": 155, move: true},
{"x": 369, "y": 196, move: true},
{"x": 350, "y": 148, move: true},
{"x": 539, "y": 222, move: true},
{"x": 594, "y": 235, move: true},
{"x": 582, "y": 185, move: true},
{"x": 633, "y": 200, move: true}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0},
{"source": 1, "target": 3},
{"source": 3, "target": 2},
{"source": 3, "target": 4},
{"source": 4, "target": 5},
{"source": 5, "target": 6},
{"source": 5, "target": 7},
{"source": 6, "target": 7},
{"source": 6, "target": 8},
{"source": 7, "target": 8},
{"source": 9, "target": 4},
{"source": 9, "target": 11},
{"source": 9, "target": 10},
{"source": 10, "target": 11},
{"source": 11, "target": 12},
{"source": 12, "target": 10}
]
};
/*
var graph ={
"nodes": [
{"x": 469, "y": 410, move: true},
{"x": 477, "y": 248, move: false},
{"x": 633, "y": 200, move: false}
],
"links": [
{"source": 0, "target": 1},
{"source": 1, "target": 2},
{"source": 2, "target": 0}
]
};
*/
var inputDiv = d3.select("#input-div"),
tooltip = d3.ui.tooltip({
base: "body",
offset: {
top: {ref: "bottom", offset: 6},
left: function(rect) {
return (rect.right + rect.left) / 2;
}
},
style: {background: "#ccc", color: "red"}
}),
easeings = ["linear", "quad", "cubic", "sin", "exp", "circle", "elastic", "back", "bounce"],
xEase = d3.ui.select({
base: inputDiv,
oninput: refresh,
data: easeings,
initial: "bounce",
onmouseover: tooltip("x"),
onmouseout: tooltip()
}),
yEase = d3.ui.select({
base: inputDiv,
oninput: refresh,
data: easeings,
initial: "circle",
onmouseover: tooltip("y"),
onmouseout: tooltip()
}),
toggleTransitions = {
label: "transitions",
onclick: function() {
this.blur();
this.value != this.value
},
value: false
},
cleanUp = {
label: "clean up",
onclick: function() {
this.blur();
this.value != this.value;
d3.selectAll(".dummy-segment").remove()
},
value: false
},
buttons = Object.defineProperties(
inputDiv.append("div")
.attr("id", "controls")
.style({display: "inline-block", padding: "0 6px 0 6px", "text-align": "center"})
.call(d3.ui.buttons.toggle, [toggleTransitions, cleanUp]),
{
"useTransitions": {
get: function() {
return toggleTransitions.value
}
},
"cleanUp": {
get: function() {
return cleanUp.value
}
},
"height": {
get: function(){
return this.node().getBoundingClientRect().height;
}
}
}),
aveTransTime = d3.ui.number({
attributes: {
varying: [{name: "tx", value: 0.1}, {name: "ty", value: 0.1}],
uniform: {min: 0.1, max: 10, step: 0.1}
},
events: {
varying: {mouseover: function(v){return tooltip(v.name)}},
uniform: {
change: refresh,
mouseout: tooltip()
}
}
}),
elapsedTime = outputs.ElapsedTime("#input-div", {
border: 0, margin: 0, "box-sizing": "border-box",
padding: "0 0 0 6px", background: "#2B303B", "color": "orange"
})
.message(function(value) {
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap)
return d3.format(" >4,.1f")(1 / aveLap) + " fps "
}),
hist = d3.ui.FpsMeter("#input-div", {display: "inline-block"}, {
height: 10, width: 100,
values: function(d){return 1/d},
domain: [0, 60]
});
var width = 960,
height = 500 - inputDiv.node().getBoundingClientRect().height - buttons.height - 3,
steps = function(){return +d3.select("#steps-selector").property("value")};
var colors = d3.scale.category10();
graph.nodes.forEach(function(d, i){
d.color = colors(i), d.index = i
});
var force = d3.layout.force()
.size([width, height])
.charge(-600)
.linkDistance(40)
.on("start", function() {
elapsedTime.start(100);
})
.on("tick", tick);
var drag = force.drag()
.on("dragstart", dragstart);
var svg = d3.select("#vis").append("svg")
.attr("width", width)
.attr("height", height);
var link = svg.selectAll(".link"),
nodes = svg.selectAll("#nodes");
d3.ns.prefix.CB = "CB:emit/drag/transition/or-whatever-you-feel-like";
// create an object for mapping the transition timelines
var mapTransitions = (function mapTransitions() {
var timeInterval, t, w, tAxis = d3.svg.axis().orient("top").tickFormat(d3.time.format("%H:%M:%S:%L")),
nodeTransitions = [],
timeDisplay,
nodes,
lane,
timeLines,
timeSegments,
mark;
var size = {
h: 10, leading: 3,
margin: {left: 30, right: 30, top: 40, get width() {return width - this.right}}
},
nodeTransition = Transition(500),
scaleTransition = Transition(1000),
element = d3.ui.select({
base: inputDiv,
data: [{text: "SVG element", value: "svg"}, {text: "dummy nodes", value: "custom"}],
element: function() {
return {
svg: "rect", custom: "CB:rect"
}[this.value()]
}
}),
method = d3.ui.select({
base: inputDiv,
// onUpdate: update,
data: [{text: "in timeline", value: "nodes"}, {text: "global", value: "global"}],
get dummyNode(){
return this.methods[this.value()]
},
methods: {
nodes: function () {
var n = Math.round(Math.random() * nodes.size()), tl = Math.round(Math.random());
return nodes.filter(function(d, i) {return i === n})
.selectAll("g").filter(function(d, i) {return i == tl});
},
global: function (){
return svg
}
},
get h(){
return size.h;
},
w: width,
get y(){
return {nodes: 0, global: Math.random()*height}[this.value()];
}
});
return function mapTransitions(selection, update) {
// build a data structure for the transitions on the proxy nodes
// convert:
// node {}
// transition []
// lock {}
// delay
// duration
// time
// event
// to this:
// node {} <- nodes
// index
// color <string>
// transitions []
// transition {} <- timeLines
// name
// color
// active
// stops []
// stop {} <- timeSegments
// stop
// id
// active get:
// duration
// t0
// t1
// y
// h
// nop
// overlaps function
// if a node is selected then it is an end event so flag it for deletion
if(update/* && update.transaction === "disconnect"*/) {
// get the location of the current transition of the event emitting node
var node, transition, stop, exitNodeStops, exitStops,
stops = nodeTransitions.filter(function(n, i) {
var f = n.index == update.node;
if(f) node = i;
return f
})[0]
.transitions.filter(function(t, i) {
var f = t.name == update.attr;
if(f) transition = i;
return f
})[0]
.stops,
s, j = 0;
/*
console.log(
[update.node, update.attr, update.transition.active].join("\t")
);
*/
// get a reference to the active stop in the transition
for(s = 0;
s < stops.length && stops[s].id != update.transition.active; s++)
/*console.log([s, "of", stops.length, stops[s].id].join("\t"))*/;
stop = stops[s];
// collect all segments representing stops scheduled to end before the start time
// of the current stop
// include the current stop (event source) in this collection
// first get all of the stops from the earliest, up to and including the current one
exitNodeStops = stops.slice(0, s + 1);
// filter out the stops that are scheduled to start later and remove them from the map
exitNodeStops = exitNodeStops
.filter(function(s, i){
var past = stop.t0 > s.t1 ||
update.transaction == "disconnect" && (stop === s ||
+stop.id >= +s.id && stop.t1 >= s.t0);
if(past){
stops.splice(i + j--, 1);
}
return past
});
/*
exitStops = exitNodeStops
.map(function(d) {
console.log([d.id, d.t1 - Date.now(), d.t1 - stop.t1].join("\t"));
return d.id
});
console.log(
[stop.t1 - Date.now(), d3.time.format("%H:%M:%S:%L")(new Date(stop.t1)), stop.nop ? "nop" : "active"
].join("\t")
);
printDiff();
printState();
*/
}
function printDiff(){
console.log("exitStops " + (exitNodeStops ? exitNodeStops.length : "none"));
if(exitNodeStops) {
exitNodeStops.forEach(function(s) {
var t1 = s.t1;
console.log([ "delete " +
s.id,
d3.time.format("%H:%M:%S:%L")(new Date(t1)),
(s.nop ? "nop" : "active")
].join("\t"))
})
}
}
function printState() {
console.log("state");
nodeTransitions.forEach(function(n) {
n.transitions.forEach(function(t) {
t.stops.forEach(function(s) {
var t0 = s.t1, t1 = s.t1;
console.log([
n.index, t.name, s.id,
t0, d3.time.format("%H:%M:%S:%L")(new Date(t0)),
t1, d3.time.format("%H:%M:%S:%L")(new Date(t1)),
(s.nop ? "nop" : "active"),
(s.node ? "node" : "deleted")
].join("\t"))
})
})
});
}
// build a visualisation based on the map
var oldScales = null;
// for a normal update, keep the same time scale
// otherwise, calculate the time scale based on the transitions data in the map
if(!update) {
// console.log("initialise " + initEvents++);
// make a clean snap-shot of the transitions
nodeTransitions = [];
// create a map of the transitions structure on the proxy nodes
// with transition stops sorted by id (temporal order)
selection.each(function d(nodeData) {
var n = this,
transitionNames = Object.keys(n).filter(function(k) {return k.match(/^__transition/)}),
transitions = transitionNames.map(function(k) {
return {
name: k.match(/^__transition_(.*?)__/)[1],
stops: Object.keys(n[k]).filter(function(id, i) {
//only include the stops and exclude the last one added by positionNodes
return id.match(/\d+/) && i != n[k].count-1;
}).sort(function(a, b){return a - b}).map(function(index, i) {
var l = n[k][index];
return {
stop: i,
id: index,
get active() { return n[k] ? n[k].active == index : null},
duration: l.duration,
t0: l.time + l.delay,
t1: l.time + l.delay + l.duration,
nop: l.tween.empty(),
h: size.h,
y: 0,
overlaps: function(seg2) {
var seg1 = this;
return (
seg1.t1 >= seg2.t0 && seg2.t1 >= seg1.t0 ||
seg2.t1 >= seg1.t0 && seg1.t1 >= seg2.t0
)
}
}
}),
active: n[k].active,
color: d3.select(n).datum().color
}
});
nodeTransitions.push({
index: nodeData.index,
color: nodeData.color,
transitions: transitions});
});
t = d3.time.scale()
.range([0, size.margin.width])
// find the min delay and the maximum finish time for all transitions
.domain([
d3.min(nodeTransitions, function(node) {
return d3.min(node.transitions, function(transition) {
return d3.min(transition.stops, function(stop) {
return stop.t0
})
})
}),
d3.max(nodeTransitions, function(node) {
return d3.max(node.transitions, function(transition) {
return d3.max(transition.stops, function(stop) {
return stop.t1
})
})
})
]);
// record the time interval, before nicing the domain
timeInterval = t.domain();
t.nice();
// a line segment for each transition
// width scale
w = d3.scale.linear()
.range(t.range().map(function(d) {
return d - t.range()[0];
}))
.domain(t.domain().map(function(d) {
return d - t.domain()[0];
}));
tAxis.scale(t);
// outer wrapper
timeDisplay = svg.selectAll("#timeDisplay").data([nodeTransitions]);
timeDisplay.enter().append("g")
.attr({
id: "timeDisplay",
transform: "translate(" + [size.margin.left, size.margin.top] + ")"
})
.append("g")
.attr({
id: "xAxis",
transform: "translate(0," + -size.leading + ")"
});
timeDisplay.exit().call(nodeTransition, fadeOut, {name: "timeDisplay", target: "opacity"});
// node wrappers
// bind node transition structures
nodes = timeDisplay.selectAll(".node-timeline").data(function(d) {
return d
}, function(d){return d.index});
nodes.enter().append("g")
.attr({
class: function(d){return "node-timeline _" + d.index},
opacity: 1
});
nodes.attr({
transform: function(d, i) {
return "translate(0," + (i * (2 * (size.h + size.leading))) + ")"
},
fill: function(d) {
return d.color;
}
});
if(buttons.cleanUp) {
nodes.exit().call(nodeTransition, {
then: fadeOut,
name: "nodes",
attr: ["opacity"],
data: [function(d) {return "transitions: " + d.transitions.length}]
});
}
// set up the selection for the time cursor early because it has the previous scales attached
// and this is needed for the pre-transition positioning the time segments
mark = nodes.selectAll(".mark")
.data([{scales: {t: t.copy(), w: w.copy()}}], function(d) {
// the key function is called before the new data is bound, so oldScales gets
// the previously bound value
if(!Array.isArray(this)) oldScales = d.scales;
return d;
})
.attr("class", "mark");
// add the cursor for current time
mark.enter().append("line")
.attr({
stroke: "black", "stroke-width": 1,
class: "mark"
});
// a lane for each attribute transitioning on each node
lane = d3.scale.ordinal().range([0,1]).domain(["cx","cy"]);
timeLines = nodes.selectAll(".timeLine").data(function(d) {
return d.transitions;
}, function(d){
return d.name
});
if(buttons.cleanUp) {
timeLines.exit().call(nodeTransition, {
then: fadeOut,
name: "timeLines",
attr: ["opacity"],
data: ["name", "active", function(d) {return "stops: " + d.stops.length}]
});
}
timeLines.enter().append("g").attr({
class: function(d){return "timeLine " + d.name},
opacity: 1
});
timeLines.attr({
transform: function(d) {
return "translate(0," + (lane(d.name) * (size.h + size.leading)) + ")";
}
});
// re-build the visualisation for the transition stops
timeSegments = timeLines.selectAll(".segment")
.data(function(d) {return d.stops}, function(d) {
return d.id;
});
// mark the exit nodes for disposal, fade them out and remove them
var tsExit = timeSegments.exit()
.attr({class: "garbage"});
tsExit.filter(function(d) {
return !d.nop;
}).call(nodeTransition, {
then: fadeOut,
name: "exit",
attr: ["opacity"],
data: ["id"],
debug: false
});
tsExit.filter(function(d) {
return d.nop;
}).remove();
// enter new nodes, faded out and position on the previous scale
// sort them to match the data sequence (temporal order)
timeSegments.enter().append("rect")
.attr({
class: function(d) {return "segment _" + d.id},
stroke: "black",
"stroke-opacity": 0,
opacity: 0,
x: function(d) {
return (oldScales ? oldScales.t : t)(d.t0)
},
width: function(d) {
return (oldScales ? oldScales.w : w)(d.duration)
},
y: 0, height: size.h
}).order();
timeSegments.each(function(d){
d.node = this;
});
// on the first pass, fade the new nodes in and slide all nodes into the new scale
// if its not just an update, slide the nodes into place
timeSegments
.call(nodeTransition, {
then: function(selection) {
selection.attr({
opacity: function o(d) {
return d.nop ? 0 : 0.6
},
x: function(d) {
return t(d.t0)
},
width: function(d) {
return w(d.duration)
}
});
},
name: "x",
attr: ["opacity"],
data: ["id"],
debug: false
});
// offset colliding segments
timeLines
.each(function() {
var segments = d3.select(this).selectAll(".segment");
segments.each(function(s1) {
// for each segment...
if(!s1.nop) {
// if the segment is not just a spacer
// create an array of overlapping siblings and store it on the node datum object
var l;
var key1 = this.t0 + "" + this.t1;
s1.group = [d3.select(this)];
s1.map = d3.map();
segments.each(function(s2, j) {
if(s2 !== s1 && (!s2.map || !s2.map.has(key1)) && !s2.nop && s1.overlaps(s2)) {
var key2 = this.t0 + "" + this.t1;
s1.group.push(d3.select(this));
s1.map.set(this, key2);
}
});
// if there are overlapping siblings...
if((l = s1.group.length) > 1) {
// divide them evenly in the vertical slot
s1.group.forEach(function(n, i) {
var h = size.h / l;
n.call(nodeTransition, {
then: function(transition) {
transition.attr({y: h * i, height: h})
},
name: "y"
})
})
}
}
})
});
} else{
// if refresh only, manually exit completed segments
if(update.transaction === "disconnect") {
// printState();
// exit the expired stops on the updating node
exitNodeStops.forEach(function ex(s){
var n = d3.select(s.node);
delete s.node;
if(n.datum().nop){
dummyElement(n);
n.remove();
}else
n.call(nodeTransition, {
then: fadeOut,
name: "exit",
attr: ["opacity"],
data: ["id"],
debug: false
});
})
}
// if only an update, pop the active nodes and restore their full height
var activeSegment = d3.select(stop.node);
if(activeSegment.size() && !activeSegment.datum().nop)
activeSegment
.attr({
opacity: 0.8,
"stroke-opacity": 1
})
.call(nodeTransition, {
name: "restoreHeight",
ease: "linear",
then: function restore(selection) {
// eagerly restore to full height
selection.attr({
y: 0, height: size.h
});
d3.select(selection.node()).datum().active = false;
}
});
}
var currentX = t(Date.now());
mark.attr({
y2: 2 * (size.h + size.leading),
x1: currentX,
x2: currentX
}).interrupt()
.transition().duration(t.domain()[1] - Date.now()).ease("linear")
.attr({x1: t.range()[1], x2: t.range()[1]})
.each("start", function() {
mark.attr({running: true})
})
.each("end", function() {
mark.attr({running: null})
});
if(!update) timeDisplay.select("#xAxis").attr("opacity", 1).call(scaleTransition, {
then: tAxis,
name: "tAxis",
attr: ["opacity"],
data: ["id"],
debug: false
});
function fadeOut(selection){
dummyElement(selection);
selection.attr({opacity: 0}).remove();
selection.attr({opacity: 0.3});
}
function dummyElement(base){
if(buttons.cleanUp) return base.remove();
method.dummyNode()
.append(element.element())
.attr({
x: Math.random()*method.w,
y: method.y,
height: Math.random()*method.h,
width: Math.random()*10,
fill: colors(Math.random()*10),
class: "dummy-segment"
});
}
};
function Transition(t) {
// generalised transition that accepts options controlling what is logged
// for the transition events and then pass remaining arguments to a then function
// the then function is called with the transition as the <this> context
function getTarget(node, target) {
return target && node && (target + " from " + node.attr(target)) || "";
}
function m(s){
var d = s.datum();
return function(k){
return typeof k == "function" ? k(d) : (k + ": " + d[k]);
}
}
function a(s){
return function(a){
a = d3.functor(a)(s);
return a + ": " + s.attr(a);
}
}
return function transition(selection, opt) {
opt = opt || {};
var name = d3.functor(opt.name)(selection), target = opt.target, then = opt.then,
data = (opt.data || []), attr = (opt.attr || []),
n = buttons.useTransitions ?
(name ? selection.transition(name) : selection.transition())
.duration(t)
.ease(opt.ease || "cubicInOut"):
selection;
function log(s, event){
if(opt.debug)
console.log([name].concat(data.map(m(s)), attr.map(a(s))
, [getTarget(selection, target), event]).join("\t"))
}
if (name && n.duration)
n
.each("start", function() {
log(d3.select(this), "start");
})
.each("interrupt", function() {
log(d3.select(this), "interrupted");
})
.each("end", function() {
log(d3.select(this), "end");
});
if(then)
n.call.bind(n, then).apply(n, [].slice.call(arguments, 1));
}
}
})();
//d3.json("graph.json", function(error, graph) {
// if (error) throw error;
force
.nodes(graph.nodes)
.links(graph.links);
link = link.data(graph.links)
.enter().append("line")
.attr("class", "link");
nodes = nodes.data([graph.nodes])
.enter().append("g")
.attr({id: "nodes"});
var node = nodes.selectAll(".node")
.data(function(d){return d})
.enter().append("g")
.attr("class", "node")
.classed("fixed", function(d){return d.move})
.on("dblclick", dblclick)
.call(drag)
.call(positionnodes);
var circle = node.append("circle")
.attr({
r: 12
})
.style({
fill: function(d) {return d.move ? d.color : null}
}),
label = node.append("text")
.attr({
"text-anchor": "middle",
"font-size": "12px",
dy: "0.35em"
});
//});
d3.timer(function(){
elapsedTime.mark();
if(elapsedTime.aveLap.history.length)
hist(elapsedTime.aveLap.history);
});
function tick(e) {
if(link && label && node) {
link.attr({
"x1": function a(d) { return d.source.x; },
"y1": function a(d) { return d.source.y; },
"x2": function a(d) { return d.target.x; },
"y2": function a(d) { return d.target.y; }
});
label.text(function(d) {
// return an array of transition stop counts
return d ?
d.transitions ?
d.transitions.map(function(t) {
return t.stops.filter(function(s) {
return !s.nop
}).length
}) :
null :
null;
});
node.attr("transform", function n(d) {return "translate(" + [d.x, d.y] + ")"})
}
force.alpha(0.1)
}
function refresh(){
node.call(positionnodes)
}
function dblclick(d) {
d3.select(this).classed("fixed", d.move = false)
.selectAll("circle").style("fill", null);
}
var dragFlag = {cx: 16, cy: 32};
function dragstart(d, attr) {
d.fixed |= dragFlag[attr] || 2;
d3.select(this).classed("fixed", d.move = true)
.selectAll("circle").style("fill", d.color);
}
function dragend(d, attr) {
d3.select(this).classed("fixed", d.fixed &= ~(dragFlag[attr] || 2))
}
function positionnodes(selection){
// reset the groups in the endAll detector
var endAll = d3.cbTransition.endAll(),
ease = {cx: xEase, cy: yEase},
// set up a structure of privately namespaced elements as transition proxies
// for the nodes with move set to true and bind to the same data
// ns = "CB:emit/drag/transition/or-whatever-you-feel-like",
// get the parent data with grouping preserved
// todo generalise this for a complex group structure
selectionData = selection.map(function(g){return d3.select(g.parentNode).datum()})[0],
transitions = d3.select("body").selectAll("transitions")
.data([selectionData.filter(function(d){
return d.move
})], function(d){return d.index}),
transitionsEnter = transitions.enter().append("CB:transitions"),
shadowNodes = transitions.selectAll("emitdrag")
.data(function(d){return d});
shadowNodes.enter().append("CB:emitdrag");
selection.style("fill", null); // reset the node colors
function updateTransitions(selection, update){
// if(!showTimeLines.checked) return;
selection = selection || d3.select("body").selectAll("transitions").selectAll("emitdrag");
return selection.call(mapTransitions, update);
}
// create a chain of transitions on the shadow nodes cx and cy attributes
["cx", "cy"].forEach(function(attr, index) {
var routes = {cx: ["x", "px"], cy: ["y", "py"]};
function connect(a){
return function(d, i) {
// select the proxy
var n = d3.select(this);
// and align it to the current position of the selected node
n.attr({cx: d.x, cy: d.y});
// redirect the selected node data to the attributes of the transition proxies
Object.defineProperty(d, routes[a][1], {
get: function() {return d[routes[a][0]] = +n.attr(a)},
set: function(_){
n.attr(a, _);
},
configurable: true,
enumerable: true
});
// map the current state after the transition cleanup is complete
// console.log("connect\t" + a + "\t" + n.datum().index);
updateTransitions(null, {
node: d.index,
attr: a,
transition: n.property(["__transition_", a, "__"].join("")),
selection: n,
transaction: "connect"
});
}
}
function disconnect(a){
return function(d, i) {
var n = d3.select(this), that = this;
Object.defineProperty(d, routes[a][1], {
value: +n.attr(a),
writable: true
});
// map the current state after the transition cleanup is complete
// window.setTimeout(function(){
// console.log("disconnect\t" + a + "\t" + n.datum().index);
updateTransitions.call(that, null, {
node: d.index,
attr: a,
transition: n.property(["__transition_", a, "__"].join("")),
selection: n,
transaction: "disconnect"
});
// }, 0);
}
}
function onStart(d, i) {
dragstart.call(selection.filter(function(p) {return p === d}).node(), d, attr);
connect(attr).call(this, d, i);
force.start();
if(!d.starts++) d.starts =1;
}
function onInterrupt(d, i){
// console.log(d.index + " interrupted")
dragend.call(selection.filter(function(p) {return p === d}).node(), d, attr);
disconnect(attr).call(this, d, i);
}
function onEnd(d, i) {
dragend.call(selection.filter(function(p) {return p === d}).node(), d, attr);
disconnect(attr).call(this, d, i);
if(!d.starts--) console.log("end before start!");
}
function cleanUp(selection){
// remove the shadow nodes after all their last transitions completes
selection.call(endAll, function(){
transitions.remove();
// map the current state after the transition cleanup is complete
window.setTimeout(function(){updateTransitions(null)}, 0);
}, "move-node");
}
d3.range(steps()).reduce(function(o, s) {
var minTime = 20;
function tms() {return (aveTransTime()[0] * 1000)}
return (
o.transition(attr)
// nop transition for delay
.duration(function(d) {
return d.delay = (minTime + Math.random() * tms()).toFixed()
})
.each("start.step", onStart)
.each("interrupt.nop", onInterrupt)
.each("end.nop", onEnd))
// operative transition
.transition()
.duration(function(d) {
return d.duration = (minTime + Math.random() * tms()).toFixed()
})
.ease(ease[attr].value())
.attr(Object.defineProperty({}, attr, {
value: function(d) {
var m = 1/5;
return (m + (1 - 2 * m) * Math.random()) * [width, height][index]
},
enumerable: true
}))
.each("start.step", onStart)
.each("interrupt", onInterrupt)
.each("end.step", onEnd)
}, shadowNodes.interrupt()) // delete any existing transitions on the initial object
// add a cleanup on the last transition in the chain
.transition("service").duration(0)
.call(cleanUp, attr);
});
shadowNodes.call(updateTransitions);
}
force.start();
</script>
</body>
</html>
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment