Last active
November 20, 2015 07:32
-
-
Save cool-Blue/5f135758b27fb1672cfd to your computer and use it in GitHub Desktop.
Using d3 transitions on dummy nodes to transition data
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> | |
<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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment