Skip to content

Instantly share code, notes, and snippets.

@kenpenn
Last active October 8, 2021 15:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kenpenn/9476266 to your computer and use it in GitHub Desktop.
Save kenpenn/9476266 to your computer and use it in GitHub Desktop.
d3 gangnam style

A work in progress. A force-directed graph using transforms to move nodes, point-along-path interpolation to move nodes in ellipses, etc.

/* d3 gangnam style!
* simplifed test case
* moving force-directed graph nodes via transform
* coded by Ken Penn
*/
(function () {
window.reqAniFrame = function(win, t) {
return win["r" + t] || win["webkitR" + t] || win["mozR" + t] || win["msR" + t] || function(fn) { setTimeout(fn, 60) }
} (window, "equestAnimationFrame");
var gs = {
svgBox : d3.select('.psy-svg-box'),
svg : d3.select('.psy-svg-box svg'),
height : 0,
ctrY : 0,
baseY : 806, // maximized viewport height on my 15" macbook
adjY : 0,
width : 0,
ctrX : 0,
baseX : 1392, // maximized viewport width on my 15" macbook
adjX : 0,
smallest : 0,
nodes : '',
links : '',
figs : [],
fignewt : 0,
trace : true,
// bpm : 60000 / 64,
bpm : 60000 / 128, // 128 bpm, ~469 ms
init : function () {
var fig = {};
// create a stick figure, start the graph
fig = gs.crtGrp(gs.figs.length);
setTimeout(function () { gs.standUp(fig); }, gs.bpm * 4);
setTimeout(function () { gs.akimbo(fig); }, gs.bpm * 8);
setTimeout(function () { gs.gandy(fig);
gs.jive(fig);
//gs.slideGrp(fig, 16);
}, gs.bpm * 12);
setTimeout(function () { gs.wave(fig); }, gs.bpm * 24);
},
setDims : function () {
var part,
dims = gs.svgBox.node().getBoundingClientRect();
// set the dimensions for the svg element
gs.height = dims.height;
gs.ctrY = gs.height / 2;
gs.width = dims.width;
gs.ctrX = gs.width / 2;
gs.smallest = gs.height < gs.width ? gs.height : gs.width;
gs.svg = gs.svgBox.select('svg')
.attr('height', gs.height)
.attr('width', gs.width);
// scale the body parts
gs.adjX = gs.width / gs.baseX;
gs.adjY = gs.height / gs.baseY;
gs.adjust = gs.smallest === gs.height ? gs.adjY : gs.adjX;
for (part in gs.parts) {
if (gs.parts.hasOwnProperty(part)) {
if (gs.parts[part].ld) {
gs.parts[part].ld = gs.adjust * gs.parts[part].ld > 4 ?
Math.round(gs.adjust * gs.parts[part].ld) : 4;
}
}
}
recurse(gs.bod);
function recurse (part) {
if (part.reqX) {
part.reqX = Math.round(part.reqX * gs.adjust);
}
if (part.reqY) {
part.reqY = Math.round(part.reqY * gs.adjust);
}
if (part.children) {
part.children.forEach(recurse);
}
}
},
crtGrp : function (ct) {
var fig = gs.svg.append('g')
.classed('fig-' + ct, true);
fig.nodes = '';
fig.links = '';
// init the force layout
fig.force = d3.layout.force()
.on('tick', function (d) { gs.tick(fig); })
.size([gs.width, gs.height]);
gs.update(fig);
gs.classLine(fig);
gs.figs.push(fig);
return fig;
},
classLine : function (fig) {
fig.links.each(function (d) {
d3.select(this).classed('src-' + d.source.name, true)
.classed('trg-' + d.target.name, true);
});
},
update : function (fig) {
var root = gs.clone(gs.bod),
fit = 0,
charge = 0,
gravity = 0;
fig.nodes = gs.flatten(root),
fig.links = d3.layout.tree().links(fig.nodes),
fit = Math.sqrt(fig.nodes.length / (gs.smallest * gs.smallest)),
charge = ( -1 / fit ) * .5 * gs.adjust,
gravity = ( 5 * fit );
fig.selectAll('line.link').remove();
fig.selectAll('circle.node').remove();
// start the force layout
fig.force
.charge(charge)
.linkDistance( function (d) { return gs.parts[d.target.part].ld; })
.gravity(gravity)
.nodes(fig.nodes)
.links(fig.links)
.start();
// Update the links…
fig.links = fig.selectAll('line.link')
.data(fig.links, function(d) { return d.target.id; });
// Enter any new links.
fig.links.enter().insert('line', '.node')
.attr({ 'class' : 'link',
x1 : function(d) { return d.source.x; },
y1 : function(d) { return d.source.y; },
x2 : function(d) { return d.target.x; },
y2 : function(d) { return d.target.y; }
});
// Exit any old links.
fig.links.exit().remove();
// Update the nodes…
fig.nodes = fig.selectAll('circle.node')
.data(fig.nodes, function(d) { return d.id; })
// Enter any new nodes
fig.nodes.enter()
.append('circle')
.attr('class', function (d) {
var outer = d.outer? ' outer' : '';
return 'node ' + d.name + outer;
})
.attr('transform', function(d) {
return 'translate(' + gs.to3(d.x) + ',' + gs.to3(d.y) + ')';
})
.attr('r', function (d) {
var r, adj;
adj = gs.adjX < gs.adjY ? gs.adjX : gs.adjY;
r = Math.ceil(gs.parts[d.part].r * adj);
r = r > 4 ? r : 4;
return r;
})
.call(fig.force.drag)
// Exit any old nodes.
fig.nodes.exit().remove();
},
tick : function (fig) {
fig.nodes.attr('transform', function(d) {
return 'translate(' + gs.to3(d.x) + ',' + gs.to3(d.y) + ')';
});
fig.links.attr('x1', function(d) { return d.source.x; })
.attr('y1', function(d) { return d.source.y; })
.attr('x2', function(d) { return d.target.x; })
.attr('y2', function(d) { return d.target.y; });
},
standUp : function (fig) {
var cb = function (d) {
if ( d.outer && d.name !== 'head' || d.name === 'bod') {
return d.fixed = true;
}
},
ctrX = fig.ctrX || gs.ctrX,
ctrY = fig.ctrY || gs.ctrY;
fig.lines = fig.selectAll('line.link');
fig.nodes.each(function (d, i) {
var mov = { d : d,
el : this,
endX : ctrX + d.reqX,
endY : ctrY + d.reqY,
fig : fig
};
fig.force.stop();
mov = gs.buildMove(mov);
gs.transNode(mov, cb);
});
},
akimbo : function (fig) {
var hands = fig.selectAll('.node.lHand, .node.rHand');
fig.selectAll('circle.lElbow, circle.rElbow')
.each(function (d) { return d.fixed = true; })
hands.each(function (d) {
var mov = {
d : d,
el : this,
fig : fig
};
if (d.name === 'lHand') {
mov.endX = gs.to3(gs.ctrX + (gs.adjust * 33));
mov.endY = gs.to3(gs.ctrY + (gs.adjY * -48));
} else {
mov.endX = gs.to3(gs.ctrX + (gs.adjust * -33));
mov.endY = gs.to3(gs.ctrY + (gs.adjY * -48));
}
mov = gs.buildMove(mov);
gs.transNode(mov);
});
},
jive : function (fig) {
var hands = fig.selectAll('.node.lHand, .node.rHand'),
lbows = fig.selectAll('.node.lElbow, .node.rElbow'),
radX = gs.adjust * 5,
radY = gs.adjust * 25;
hands.each(function (d) { jivin(d, this); });
function elbows () {
lbows.each(function (d) { jivin(d, this); });
}
function jivin (d, el) {
var mov = { d : d,
el : el,
pc : 'jive-',
fig: fig
};
mov = gs.buildMove(mov);
if (d.name === ('lHand' || 'rElbow') ) {
mov.endX = mov.begX - radX;
} else {
mov.endX = mov.begX + radX;
}
mov.endY = gs.to3(mov.begY - (gs.adjust * 10));
mov = gs.buildMove(mov);
mov.pc = mov.pc + d.name;
crtPath(mov);
gs.transNode(mov, function (d) {
gs.ptAlongPath({ path : fig.select('path.' + mov.pc),
circle : d3.select(mov.el),
lines : mov.lines,
count : mov.d.part === 'hand' ? 8 : 7,
fig : mov.fig
});
if (mov.d.name === 'lHand') { elbows(); }
});
}
function crtPath (mov) {
var rad;
if ( mov.d.name === ('rHand' || 'rElbow' ) ) {
rad = -radX;
} else {
rad = radX;
}
mov.fig.append('path')
.attr('d', 'M ' + mov.endX + ',' + mov.endY +
' a ' + rad + ',' + radY + ' 0 0,0 ' +
-rad + ',' + (gs.adjY * -8) +
' a ' + rad + ',' + radY + ' 0 1,0 ' +
rad + ',' + (gs.adjY * 8) + ' z'
)
.attr('class', mov.pc)
.attr('stroke', 'none')
.attr('fill', 'none')
}
},
gandy : function(fig) {
var hoppers = fig.selectAll('.node.lFoot, .node.lKnee, .node.rFoot, .node.rKnee');
hoppers.each(function(d) {
var hopper = gs.getRectCtr(this);
var radX = gs.to3(60 * gs.adjust);
var radY = gs.to3(8 * radX);
var dir = d.name === ('lFoot' || 'lKnee') ? 1 : -1;
var sweep = d.name === ('lFoot' || 'lKnee') ? [1,0] : [0,1];
var stroke = d.name === 'lFoot' ? 'limegreen' : 'magenta';
var dpath;
if (d.part === 'knee') {
radX *= 0.5;
radY *= 0.5;
}
dpath = 'M' + hopper.x + ',' + hopper.y +
' a' + radX + ',' + radY + ' 0 0,' + sweep[0] + ' ' +
(dir * radX) + ',0 v1' +
' a' + radX + ',' + radY + ' 0 0,' + sweep[1] + ' ' +
(-dir * radX) + ',0 v-1 z';
fig.append('path')
.classed(d.name + '-hop', true)
.attr('d', dpath)
.attr('stroke', 'none')
.attr('fill', 'none')
});
hoppers.each(function (d) {
var hopper = gs.getRectCtr(this);
var mov = { d : d,
endX : hopper.x,
endY : hopper.y,
el : this,
fig : fig
};
mov = gs.buildMove(mov);
gs.transNode(mov, function (d) {
if ( d.name === 'rFoot' || d.name === 'rKnee' ) {
hop();
} else {
setTimeout(function () { hop(); }, gs.bpm);
}
function hop() {
gs.ptAlongPath({ path : fig.select('path.' + d.name + '-hop'),
circle : d3.select(mov.el),
lines : mov.lines,
count : 8,
fig : fig,
dur : gs.bpm * 2
});
}
});
});
},
wave : function (fig) {
var fist = fig.select('circle.lHand');
var strX = gs.ctrX - (gs.adjust * 80);
var strY = gs.ctrY - (gs.adjust * 220);
var radX = gs.adjust * 65;
var radY = gs.adjust * 25;
var cb = function (d) { return d.fixed = true; };
fig.append('path')
.attr('d', 'M ' + strX + ',' + strY +
' a ' + radX + ',' + radY + ' 10 0,0 ' +
-radX + ',' + (gs.adjY * -8) +
' a ' + radX + ',' + radY + ' 10 1,0 ' +
radX + ',' + (gs.adjY * 8) + ' z'
)
.attr('class', 'wave')
.attr('stroke', 'none')
.attr('fill', 'none')
fist.each(function (d) {
var mov = { d : d,
endX : strX,
endY : strY,
el : this,
pc : 'wave',
fig : fig
};
mov = gs.buildMove(mov);
gs.transNode(mov, function (d) {
gs.ptAlongPath({ path : fig.select('path.' + mov.pc),
circle : d3.select(mov.el),
lines : mov.lines,
count : 7,
fig : fig
});
});
});
elbows();
function elbows() {
var lBows = fig.selectAll('.node.lElbow, .node.rElbow');
lBows.each(function (d) {
var mov = { el : this,
d : d,
dur : gs.bpm * 2,
fig : fig
};
if (d.name === 'lElbow') {
mov.endX = gs.ctrX - (111 * gs.adjust);
mov.endY = gs.ctrY - (130 * gs.adjust);
} else {
mov.endX = gs.ctrX + (48 * gs.adjust);
mov.endY = gs.ctrY - (57 * gs.adjust);
}
mov = gs.buildMove(mov);
gs.crtLine(mov);
gs.transNode(mov);
});
}
},
crtLine : function(mov) {
console.dir(mov)
mov.fig.append('path')
.attr('d', 'M ' + mov.begX + ',' + mov.begY +
'L ' + mov.endX + ',' + mov.endY
)
.attr('fill', 'none')
.attr('stroke', 'none')
},
buildMove : function (mov) {
var begXY = gs.getTransXY(mov.el);
mov.begX = begXY.x;
mov.begY = begXY.y;
mov.lines = {
source : mov.fig.selectAll('line.src-' + mov.d.name),
target : mov.fig.selectAll('line.trg-' + mov.d.name),
}
return mov;
},
transNode : function (mov, callback) {
var dur = mov.dur || gs.bpm * 2;
d3.select(mov.el)
.transition()
.duration(dur)
.attr('transform', function(d) { return 'translate(' + mov.endX + ',' + mov.endY + ')'; })
.tween('tweenLine', function (d) { return function (t) { gs.tweenLine(t, mov) }})
.each('end', function (d) {
d.x = mov.endX;
d.y = mov.endY;
d.px = mov.endX;
d.py = mov.endY;
mov.fig.force.resume();
if (callback) { callback(d); }
});
},
slideGrp : function ( grp, ct, x, arr ) {
var idx = x || 0;
var slides = arr || [ { x: 100, y: 0 }, { x: 0, y: 0 },
{ x: -100, y: 0 }, { x: 0, y: 0 }/*,
{ x: 200, y: 0 }, { x: 0, y: 0 },
{ x: -200, y: 0 }, { x: 0, y: 0 }*/
];
var len = slides.length;
grp.transition()
.duration(gs.bpm * 2)
.attr('transform', function(d) { return 'translate(' + slides[idx].x + ',' + slides[idx].y + ')'; })
.each('end', function (d) {
ct -= 1;
if (!ct) {return;}
idx = idx === len - 1 ? 0 : idx + 1;
reqAniFrame(function () {
gs.slideGrp ( grp, ct, idx, slides);
});
});
},
tweenLine : function (t, mov) {
var cr = gs.getRectCtr(mov.el);
var trc = {
el : mov.el.parentElement,
cx : cr.x,
cy : cr.y,
rad : 3
};
mov.fig.force.stop();
// manipulating the line(s) the node is attached to
mov.lines.source.each(function (d) {
d.source.x = cr.x;
d.source.y = cr.y;
d3.select(this).attr('x1', cr.x)
.attr('y1', cr.y);
});
mov.lines.target.each(function (d) {
d.target.x = cr.x;
d.target.y = cr.y;
d.target.px = cr.x;
d.target.py = cr.y;
d3.select(this).attr('x2', cr.x)
.attr('y2', cr.y);
});
// mov.fig.force.resume();
if ( gs.trace ) { gs.tracers(trc) }
},
ptAlongPath : function(m) {
m.circle.transition()
.duration(m.dur || gs.bpm)
.attrTween('transform', gs.transAlong(m))
.tween('lines', gs.ptLines(m))
.each('end', function () {
m.fig.force.resume();
if (m.count) {
m.count -= 1;
reqAniFrame(function () { gs.ptAlongPath(m); });
}
});
},
ptLines : function(m) {
var path = m.path.node(),
len = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * len);
// manipulating the line(s) the node is attached to
m.lines.source.each(function (d) {
d3.select(this).attr('x1', p.x)
.attr('y1', p.y);
});
m.lines.target.each(function (d) {
d3.select(this).attr('x2', p.x)
.attr('y2', p.y);
});
};
};
},
transAlong : function(m) {
var path = m.path.node(),
len = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * len),
trc = {
el : path.parentNode,
cx : p.x,
cy : p.y
};
if ( gs.trace ) { gs.tracers(trc) }
return 'translate(' + p.x + ',' + p.y + ')';
};
};
},
getTransXY : function (el) {
var s = d3.select(el).attr('transform'),
t = [];
s = s.substring(s.indexOf('translate('));
s = s.substring(0, s.indexOf(')'));
t = s.replace('translate(','')
.replace(')','')
.split(',');
return { x : parseFloat(t[0]), y : parseFloat(t[1]) };
},
getRectCtr : function (el) {
var cr = el.getBoundingClientRect();
return { x : gs.to3((cr.left + cr.right) * 0.5),
y : gs.to3((cr.top + cr.bottom) * 0.5)
};
},
tracers : function (trc) {
setTimeout( function () {
var d3el = d3.select(trc.el),
rad = trc.rad || 2,
dur = trc.dur || gs.bpm,
fill = trc.fill || 'magenta';
d3el.append('circle')
.attr('r', rad)
.attr('cx', trc.cx)
.attr('cy', trc.cy)
.attr('fill', fill )
.attr('stroke', 'none')
.transition()
.duration(dur)
.delay(300)
.attr('opacity', 0)
.remove();
}, 200)
},
tweenChk : function (t, mov, d) {
var cr = gs.getRectCtr(mov.el),
cx = cr.x,
cy = cr.y,
pel = mov.el.parentElement,
trc = {
el : pel,
cx : cx,
cy : cy,
dur : gs.bpm * 2
};
gs.tracers(trc);
},
showXY : function () {
d3.select('body').on('mousemove', function() {
console.log('page X: ' + d3.event.pageX +'\npage Y: ' + d3.event.pageY)
});
},
showPos : function (fig) {
var adjustX = parseFloat(gs.to3(gs.baseX / gs.width)),
adjustY = parseFloat(gs.to3(gs.baseY / gs.height));
console.log('adjust x: ' + adjustX);
console.log('adjust y: ' + adjustY);
fig.nodes.each(function (d, i) {
var str = '\n' + d.name +
'\nadjust x: ' + Math.round((d.x - gs.ctrX) * adjustX) +
' actual x: ' + Math.round(d.x - gs.ctrX) +
'\nadjust y: ' + Math.round((d.y - gs.ctrY) * adjustY) +
' actual y: ' + Math.round(d.y - gs.ctrY);
console.log(str);
});
},
flatten : function (fig) {
var nodes = [],
i = 0;
function recurse (node) {
if (node.children) {
node.children.forEach(recurse);
} else {
node.outer = true;
}
if (!node.id) { node.id = ++ i; }
nodes.push(node);
}
recurse(fig);
return nodes;
},
parts : {
bod : { ld : 10 },
head : { ld : 50, r : 24 },
arm : { ld : 40 },
elbow : { ld : 50 },
hand : { ld : 50, r : 12 },
hips : { ld : 70 },
leg : { ld : 10 },
knee : { ld : 60 },
foot : { ld : 100, r : 15 }
},
bod : {
name : 'bod', part : 'bod', reqX : 0, reqY : -125,
children : [ { name : 'hips', part : 'hips', reqX : 0, reqY : -26,
children : [ { name : 'lLeg', part : 'leg', reqX : -17, reqY : 3,
children : [ { name : 'lKnee', part: 'knee', reqX : -46, reqY : 72,
children : [ { name : 'lFoot', part : 'foot', reqX : -37, reqY : 180 } ]
}
]
},
{ name : 'rLeg', part : 'leg', reqX : 20, reqY : 7,
children : [ { name : 'rKnee', part: 'knee', reqX : 46, reqY : 72,
children : [ { name : 'rFoot', part : 'foot', reqX : 37, reqY : 180 } ]
}
]
}
]
},
{ name : 'head', part : 'head', reqX : 0, reqY : -180 },
{ name : 'lArm', part : 'arm', reqX : -45, reqY : -120,
children : [ { name : 'lElbow', part: 'elbow', reqX : -70, reqY : -60,
children : [ { name : 'lHand', part : 'hand', reqX : -45, reqY : 11 } ]
}
]
},
{ name : 'rArm', part : 'arm', reqX : 45, reqY : -120,
children : [ { name : 'rElbow', part: 'elbow', reqX : 70, reqY : -60,
children : [ { name : 'rHand', part : 'hand', reqX : 45, reqY : 11 } ]
}
]
}
]
},
clone : function (src) { // courtesy of David Walsh
function mixin (dest, source, copyFunc) {
var name, s, i, empty = {};
for(name in source) {
// the (!(name in empty) || empty[name] !== s) condition avoids copying properties in 'source'
// inherited from Object.prototype. For example, if dest has a custom toString() method,
// don't overwrite it with the toString() method that source inherited from Object.prototype
s = source[name];
if(!(name in dest) || (dest[name] !== s && (!(name in empty) || empty[name] !== s))) {
dest[name] = copyFunc ? copyFunc(s) : s;
}
}
return dest;
}
if(!src || typeof src != 'object' || Object.prototype.toString.call(src) === '[object Function]') {
// null, undefined, any non-object, or function
return src; // anything
}
if(src.nodeType && 'cloneNode' in src) {
// DOM Node
return src.cloneNode(true); // Node
}
if(src instanceof Date) {
// Date
return new Date(src.getTime()); // Date
}
if(src instanceof RegExp) {
// RegExp
return new RegExp(src); // RegExp
}
var r, i, l;
if(src instanceof Array) {
// array
r = [];
i = 0;
l = src.length;
for(i; i < l; ++i) {
if(i in src) {
r.push(gs.clone(src[i]));
}
}
// we don't clone functions for performance reasons
// }else if(d.isFunction(src)) {
// // function
// r = function() { return src.apply(this, arguments); };
} else {
// generic objects
r = src.constructor ? new src.constructor() : {};
}
return mixin(r, src, gs.clone);
}, //end clone
to3 : function (n) { return parseFloat(n.toFixed(3)) }
};
gs.setDims();
gs.init();
if (window.gs) {
window.gangnamStyle = gs;
} else {
window.gs = gs;
}
}());
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>d3 Gangnam Style!</title>
<style>
.cover-box {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.psy-svg-box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
/* svg styles */
line {
stroke: goldenrod;
stroke-width: 1.5px;
}
circle.node {
cursor: pointer;
fill: #000;
stroke: none;
}
</style>
</head>
<body>
<div class="cover-box">
<div class="psy-svg-box">
<svg></svg>
</div>
</div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.min.js'></script>
<script src="gangnam.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment