Skip to content

Instantly share code, notes, and snippets.

@shadiakiki1986
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shadiakiki1986/64bb3f8ef2fd09adc950 to your computer and use it in GitHub Desktop.
Save shadiakiki1986/64bb3f8ef2fd09adc950 to your computer and use it in GitHub Desktop.
Animated graph of financial market participants (based on d3.js, json data, and fed with dummy data)

The graph is controlled by data in the text boxes above as described, and the clock on the right. The "pause" button plays the clock through time. The "step" only makes a 1-hour step. Make sure to update the trade timestamps to be after the current time of the clock (displayed right above it). The same applies for the asset performances. If you can't see the "step" or "pause" links on the right of the clock, please use the "W" cursor to reduce the width of the graph's canvas. You can click and drag on the canvas to pan, and you can zoom in and out with the mouse scroll wheel.

The cyan discs are traders, and the blue discs are assets. The blue lines are long positions, and the pink ones are short positions. An asset disc encircled in blue indicates a positive performance for the time step, whereas a pink circle indicates a negative performance.

The text in the textareas are a json description of each box's content. The json tags I use are:

  • s: security ID
  • q: quantity held
  • c: trader ID
  • sf: security name
  • cf: trader name
  • ts: timestamp of trade
  • pf: security performance in %

For further reading, you will find that I include inline links to all references that were useful to me at a particular part of my code.

// Modified version of analogClock()
// http://www.kirupa.com/html5/create_an_analog_clock_using_the_canvas.htm
//
// It has a fast-forward.
// The arms are: day of month, hour, minute (instead of hour, minute, second)
// usage:
// ac = new analogClock();
// ac.startTimer(10); // 10x time
function analogClock2 () {
var begin;
var speed,display;
var now;
var nowStart;
var funcCall;
var pause,pauseStart,pauseLength;
var dt2;
var stepLength;
function setPause(a) {
pause=a;
if(a) {
pauseStart=new Date();
} else {
pauseLength=new Date()-pauseStart+pauseLength;
}
}
this.togglePause = function() { setPause(!pause); }
this.setPause = setPause;
this.getPause = function() { return pause; }
this.getNow = function() { return now; }
this.initTimer = function startTimer(ff,ds,bt,ftf) {
// ff: fast forward speed
// ds: display speed
// ftf: function to fire every ds
begin = new Date();
speed = ff;
display=ds;
funcCall = ftf;
nowStart=bt;
pause=true;
pauseStart=new Date();
pauseLength=0;
updateDt2();
stepLength=0;
}
function stepTimer() {
displayTime();
funcCall();
}
this.stepTimer = stepTimer;
this.getDt2=function() { return dt2; }
function updateDt2() { dt2=new Date()-begin-pauseLength; }
this.incrementStepLength=function(a) { stepLength=stepLength+a; }
this.startTimer = function startTimer() {
setInterval(function() {
if(!pause) {
updateDt2();
stepTimer();
}
}, 1000/display);
stepTimer();
}
function displayTime() {
now = new Date(nowStart.getTime() + speed*(dt2+stepLength));
var d = now.getDate();
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
// digital clock //
//var timeString = d +" / " + formatHour(h) + ":" + padZero(m) + ":" + padZero(s) + " " + getTimePeriod(h);
//document.querySelector("#current-time").innerHTML = timeString;
document.querySelector("#current-time").innerHTML = now.toString();
// --- Analog clock ---//
var canvas = document.querySelector("#clock");
var context = canvas.getContext("2d");
// You can change this to make the clock as big or small as you want.
// Just remember to adjust the canvas size if necessary.
var clockRadius = 100;
// Make sure the clock is centered in the canvas
var clockX = canvas.width / 2;
var clockY = canvas.height / 2;
// Make sure TAU is defined (it's not by default)
Math.TAU = 2 * Math.PI;
function drawArm(progress, armThickness, armLength, armColor) {
var armRadians = (Math.TAU * progress) - (Math.TAU/4);
var targetX = clockX + Math.cos(armRadians) * (armLength * clockRadius);
var targetY = clockY + Math.sin(armRadians) * (armLength * clockRadius);
context.lineWidth = armThickness;
context.strokeStyle = armColor;
context.beginPath();
context.moveTo(clockX, clockY); // Start at the center
context.lineTo(targetX, targetY); // Draw a line outwards
context.stroke();
}
context.clearRect(0, 0, canvas.width, canvas.height);
drawArm(d / 31, 8, 0.75, '#00FF00'); // Day
drawArm(h / 24, 10, 0.50, '#000000'); // Hour
if(speed<3600) drawArm(m / 60, 4, 0.75, '#000000'); // Minute
if(speed<60) drawArm(s / 60, 2, 1.00, '#FF0000'); // Second
}
this.displayTime = displayTime;
function padZero(num) {
if (num < 10) {
return "0" + String(num);
}
else {
return String(num);
}
}
function formatHour(h) {
var hour = h % 12;
if (hour == 0) {
hour = 12;
}
return String(hour)
}
function getTimePeriod(h) {
return (h < 12) ? "AM" : "PM";
}
} // end of analogClock class
<!-- http://christophergandrud.github.io/d3Network/ -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script type="text/javascript" src="visTds.js"></script>
<script type="text/javascript" src="analogClock2.js"></script>
<link rel="stylesheet" type="text/css" href="visTds.css">
<link rel="stylesheet" type="text/css" href="analogClock.css">
</head>
<body>
<table>
<tr><th>Initial positions</th><th>Internal</th><th>Trades</th><th>Asset performances</th></tr>
<tr>
<td><textarea id="ta" onblur="mg.upd2()" cols=15 rows=1></textarea></td>
<td><textarea id="ta2" readonly cols=15 rows=1></textarea></td>
<td><textarea id="ta3" onblur="mg.upd4()" cols=15 rows=1></textarea></td>
<td><textarea id="ta4" onblur="mg.upd6()" cols=15 rows=1></textarea></td>
</tr>
<tr><td colspan="4">
&nbsp; H: <input type="range" min=200 max=2400 step=50 id="height" value=500 onchange='mg.updateDims()'></input>
&nbsp; W: <input type="range" min=200 max=2400 step=50 id="width" value=800 onchange='mg.updateDims()'></input>
</td></tr>
</table>
<br/>
<table border=1><tr>
<td><div id="d"></div></td>
<td>
<div id="current-time">12:00:00 AM</div>
<canvas id="clock" width="200" height="200">
If you can see this message, your browser does not support canvas
and needs an upate. Sorry. :(
</canvas>
</div></td>
<td>
<u><a id="step">Step</a></u>,
<u><a id="pause">Pause</a></u>,
<u><a id="download">Download</a></u>
</td>
</tr></table>
<div id="dtt" class="tooltip" style="opacity:0"></div>
<script>
$("document").ready(function() {
$("#ta").val('[\n'+
'{"s":3,"q":-35,"c":1, "sf":"balbla", "cf":"yoho" },\n'+
'{"s":2,"q":5,"c":2, "sf":"balbla", "cf":"yoho" },\n'+
'{"s":2,"q":-15,"c":1, "sf":"balbla", "cf":"yoho" },\n'+
'{"s":1,"q":-5,"c":3, "sf":"balbla", "cf":"yoho" },\n'+
'{"s":1,"q":23,"c":1, "sf":"balbla", "cf":"yoho"},\n'+
'{"s":1,"q":-12,"c":4, "sf":"balbla", "cf":"yoho"}\n'+
']');
$("#ta3").val('[\n'+
'{"id":4,"s":4,"q":7,"c":6, "sf":"balbla", "cf":"yoho", "ts":"2014-06-25T19:12:34"},\n'+
'{"id":3,"s":1,"q":7,"c":5, "sf":"balbla", "cf":"yoho", "ts":"2014-06-25T21:12:34"},\n'+
'{"id":2,"s":1,"q":7,"c":1, "sf":"balbla", "cf":"yoho", "ts":"2014-06-26T21:15:34"},\n'+
'{"id":1,"s":1,"q":-11,"c":4, "sf":"balbla", "cf":"yoho", "ts":"2014-06-27T03:12:34"}\n'+
']');
$("#ta4").val('[\n'+
'{"s":1,"pf":7, "ts":"2014-06-30T15:12:34"},\n'+ // pf is performance in percentage
'{"s":2,"pf":-9, "ts":"2014-06-30T16:12:34"}\n'+
']');
mg=new myGraph();
mg.setTdsMaxtime(new Date());// set tdsMaxtime
mg.upd2();
ac = new analogClock2();
ac.initTimer(60*60,1,new Date(),function() {
mg.upd5(mg.getTdsMaxtime(),ac.getNow());
mg.upd6(mg.getTdsMaxtime(),ac.getNow());
mg.setTdsMaxtime(ac.getNow());
});
ac.displayTime();
ac.startTimer();
$("#pause").click(function() { ac.togglePause(); });
$("#step").click(function() { if(ac.getPause()) { ac.incrementStepLength(1000); ac.stepTimer(); } }); // only works if paused
$("#download").click(function() {
d3.select(this)
.attr("href", 'data:application/octet-stream;base64,' + btoa(d3.select("#d").html()))
.attr("download", "visTds.svg")
});
}); // document.ready
</script>
</body>
</html>
div.tooltip {
position: absolute;
text-align: left;
/* width: 180px;
height: 80px;
*/ padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
.link {
opacity: 0.6;
stroke-width: 1.5px;
}
.node circle {
stroke: #fff;
opacity: 0.6;
stroke-width: 1.5px;
}
text {
font: 7px serif;
opacity: 0.6;
pointer-events: none;
}
// JS class: http://www.phpied.com/3-ways-to-define-a-javascript-class/
// d3.js graph: http://bl.ocks.org/mbostock/4062045
// d3.js force layout: https://github.com/mbostock/d3/wiki/Force-Layout
function myGraph () {
var force;
var links = [];
var nodes={};
var width = $("#width").val();
var height = $("#height").val();
var link,node;
var vis,div;
var tdsIncorporated=[];
var tdsMaxtime;
this.getTdsMaxtime = function() { return tdsMaxtime; }
this.setTdsMaxtime = function(a) { tdsMaxtime=a; }
this.stopGraph = function() { force.stop(); }
this.startGraph = function() { force.start(); }
this.updateDims = function() {
// http://stackoverflow.com/questions/16265123/resize-svg-when-window-is-resized-in-d3-js
// The answer by gavs
vis=d3.select("svg");
vis.attr('width', $("#width").val());
vis.attr('height', $("#height").val());
force.size([$("#width").val(),$("#height").val()])
// do not call resume so as not to push the graph // .resume()
;
}
var dateFn = function(d) { return d.name; } // I want the key to be the node name // http://pothibo.com/2013/09/d3-js-how-to-handle-dynamic-json-data/
var dateFn2 = function(d) { return d.source.name+"-"+d.target.name; } // I want the key to be the combination of source-target node names
function colorq(d) {return d.q==null?"#666":(parseInt(d.q)>0?"#3182bd":"#ff82bd");}
function upd0l(vis) {
var link = vis.selectAll(".link")
.data(force.links(),dateFn2);
link.style("stroke",colorq ); // moving this from after the "enter" means that this applies to update entries // http://stackoverflow.com/questions/13129913/select-only-updating-elements-with-d3-js
link.enter().append("line")
.attr("class", "link")
.attr("source", function(d) { return d.source.name; })
.attr("target", function(d) { return d.target.name; })
.style("stroke",colorq )
// add tool tip, ref: http://bl.ocks.org/d3noob/9692795
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.q + "<br/>")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
})
;
link.exit().remove();
}
function upd0n(vis) {
var node = vis.selectAll(".node")
.data(force.nodes(),dateFn);
// draw security performances
// http://bl.ocks.org/d3noob/5141528
node.select("circle")
.style("stroke",function(d){
return d.perf==null?"#fff":(parseInt(d.perf)>0?"#3182bd":"#ff82bd");
})
;
var g=node.enter()
.append("g")
.attr("class", "node")
// add tool tip, ref: http://bl.ocks.org/d3noob/9692795
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(d.fn + "<br/>")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
})
.call(force.drag)
;
g.append("circle")
.attr("r", 4)
.style("fill",function(d){return d.name.substr(0,1)=="s"?"#32ffbd":"#3282bd"; })
;
/* Commenting out just for printing
g.append("text")
.attr("x", 6)
.attr("dy", ".35em")
.style("fill", "#3182bd")
.text(function(d) { return d.name; })
;
*/
node.exit().remove();
}
// Compute the nodes from the links: http://www.d3noob.org/2013/03/d3js-force-directed-graph-example-basic.html
// Modiified from original because the original only supports one-time run, not a 2nd, 3rd, ... iteration
// Note the x and y => all securities and clients are born at the center of the graph
function implyNodes(link) {
var nns={name: link.source, fn: link.fns, x: width/2, y: height/2, perf:null};
var nnt={name: link.target, fn: link.fnt, x: width/2, y: height/2, perf:null};
if(typeof link.source=="string") {
link.source = nodes[link.source] || (nodes[link.source] = nns);
} else {
if(!nodes.hasOwnProperty(link.source.name)) nodes[link.source] = nns;
}
if(typeof link.target=="string") {
link.target = nodes[link.target] || (nodes[link.target] = nnt);
} else {
if(!nodes.hasOwnProperty(link.target.name)) nodes[link.target] = nnt;
}
link.value = +link.value;
return link;
}
function redraw() {
d3.select("svg").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
function tick() {
d3.select("svg").selectAll(".link")
.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; });
d3.select("svg").selectAll(".node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
}
function upd21() {
// https://github.com/mbostock/d3/wiki/Force-Layout
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(function(d) { return 25; }) // \todo d.q==null?50:8+Math.abs(d.q); })
.charge(-100)
.friction(0.7)
.on("tick", tick)
.start();
$("#d").empty();
div = d3.select("#dtt");// the tool tip
var svg = d3.select("#d").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().on("zoom", redraw))
;
upd0l(svg);
upd0n(svg);
// http://stackoverflow.com/questions/14567809/how-to-add-an-image-to-an-svg-container-using-d3-js?rq=1
d3.select("svg")
.append("svg:image")
.attr('x',0)
.attr('y',0)
.attr('width', 40)
.attr('height', 40)
.attr("xlink:href","ffa.jpg")
;
}
this.upd2 = function upd2() {
// original data
console.log($("#ta").val());
pos = JSON.parse($("#ta").val());
// modify pos to look like the following
// var links = [ { "source" : "A", "target" : "B" }, { "source" : "A", "target" : "C" }, { "source" : "A", "target" : "D" }, { "source" : "A", "target" : "J" }, { "source" : "B", "target" : "E" }, { "source" : "B", "target" : "F" }, { "source" : "C", "target" : "G" }, { "source" : "C", "target" : "H" }, { "source" : "D", "target" : "I" } ] ;
// push securities
//secIds=[];
//pos.forEach(function(v){
// if(secIds.indexOf(v.s)<0) { secIds.push(v.s); }
//});
//secIds.forEach(function(v){links.push( {"target":"s"+v, "source":"ffa","color":"#fff","q":null})});
// push clients per position
pos.forEach(function(v){ links.push({
"target":"c"+v.c,
"source":"s"+v.s,
//"color":"#fff",
"q":v.q,//parseInt(v.q)/100});}); // note the parseInt \todo
"fnt":v.cf, // full name target
"fns":v.sf // full name source
});});
// push client base
//clIds=[];
//pos.forEach(function(v){
// if(clIds.indexOf(v.c)<0) links.push( {"target":"c"+v.c, "source":"cb","color":"#fff","q":null});
//});
// Compute the distinct nodes from the links.
// Compute the distinct nodes from the links.
links.forEach(function(link) { return implyNodes(link); })
// save to textarea to view
$("#ta2").val(JSON.stringify(links));
upd21();
}
// Instant animation of additions to the graph
// http://bl.ocks.org/ericcoopey/6c602d7cb14b25c179a4
this.upd3 = function upd3() {
tds = JSON.parse($("#ta3").val()); // get all trades
tds2=tds.filter(function(i) {return tdsIncorporated.indexOf(i.id) < 0;}); // check for new trades only
upd31(tds2);
}
function upd31 (tds2) {
tds2.forEach(function(t) {
var found=false;
// if link is existing, modify qty
links.forEach(function(d,i) {
if(!found) {
found=d.source.name=="s"+t.s && d.target.name=="c"+t.c
if(found) {
//console.log("Found: "+d.source.name+","+d.target.name+"="+t.s+","+t.c+": "+d.q+","+t.q+";"+(parseInt(d.q)+parseInt(t.q))+";"+(parseInt(d.q)+parseInt(t.q)==0)+";"+((parseInt(d.q)+parseInt(t.q))==0));
if((parseInt(d.q)+parseInt(t.q))==0) {
links.splice(i,1); // remove link
// if node at source is only linked with this link, then remove it
if(!links.some(function(link) { return link.source.name==d.source.name; })) {
// delete nodes[d.source.name];
}
// ditto for target
if(!links.some(function(link) { return link.target.name==d.target.name; })) {
// delete nodes[d.target.name];
}
} else d.q=parseInt(d.q)+parseInt(t.q);
}
}
});
// if link is not already existing, add new link
if(!found) {
console.log("Not Found: "+t.s+","+t.c+","+t.q+"("+(parseInt(t.q)>0)+")"+","+t.sf+","+t.cf); // new position
var linkNew = {
"target":"c"+t.c,
"source":"s"+t.s,
//"color":"#fff",
"q":t.q,
"fns":t.sf,
"fnt":t.cf
};
links.push(linkNew);
implyNodes(linkNew);
}
// now that we're done, mark the trade as "incorporated"
tdsIncorporated.push(t.id);
});
// if there was any new trade...
if(tds2.length>0) {
vis=d3.select("svg");
force.links(links).nodes(d3.values(nodes)).start();
upd0l(vis);
upd0n(vis);
}
}
// step-by-step animation of additions to the graph at a speed of one step per second
this.upd4 = function upd4() {
tds = JSON.parse($("#ta3").val()); // get all trades
tds2=tds.filter(function(i) {return tdsIncorporated.indexOf(i.id) < 0;}); // check for new trades only
tds2.forEach(function(t,i) {
var tds3 = []; tds3.push(t);
setTimeout(upd31,1000*i,tds3);
});
}
// animate only the set of trades between 2 timestamps
this.upd5 = function upd5(t1,t2) {
tds = JSON.parse($("#ta3").val()); // get all trades
tds=tds.filter(function(i) {return tdsIncorporated.indexOf(i.id) < 0;}); // check for new trades only
tds=tds.filter(function(i) {i.ts = new Date(i.ts);return i.ts>t1 & i.ts<t2;});// check only those between timestamps t1 and t2
upd31(tds);
}
// visualize performance of securities relative to client positions (long/short)
this.upd6=function upd6(t1,t2) {
// reset performances
var isSome=false;
for(n in nodes) {
isSome = isSome || nodes[n].perf!=null;
nodes[n].perf = null;
}
if(isSome) { upd0n(d3.select("svg")); }
//get performances
pfs=JSON.parse($("#ta4").val());
pfs=pfs.filter(function(i) {i.ts = new Date(i.ts);return i.ts>t1 & i.ts<t2;});// check only those between timestamps t1 and t2
// color borders per performance
pfs.forEach(function(pf) {
force.nodes()
.filter(function(n) { return n.name==("s"+pf.s); })
.forEach(function(n) { n.perf = pf.pf;})
;
// console.log(force.nodes());
});
// if there was any new performance...
if(pfs.length>0) {
vis=d3.select("svg");
force.links(links).nodes(d3.values(nodes)).start();
upd0l(vis);
upd0n(vis);
}
}
} // end of my class
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment