|
// 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 |