Created
May 9, 2019 06:31
-
-
Save RasmusFonseca/44e637afb444ddb4dfb8d1c23bd5bcd1 to your computer and use it in GitHub Desktop.
Flareplot main script with tracks moved toward center
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
/** | |
* Creates a flareplot svg in the container matched by the `containerSelector`, sets its width to `width` pixels, | |
* and associates it with the contents of `json` | |
* @param width | |
* @param inputGraph | |
* @param containerSelector | |
* @returns {{getNumFrames, setFrame, framesIntersect, framesSum, setTrack, setTree, getTreeNames, getTrackNames, addNodeToggleListener, addNodeHoverListener, addEdgeToggleListener, addEdgeHoverListener, graph}} | |
*/ | |
function createFlareplot(width, inputGraph, containerSelector){ | |
var w = width; | |
if (width === "auto") { | |
const containerStyle = window.getComputedStyle(d3.select(containerSelector).node()); | |
const containerPadding = parseFloat(containerStyle.paddingLeft) + parseFloat(containerStyle.paddingRight); | |
w = parseFloat(d3.select(containerSelector).node().clientWidth) - containerPadding; | |
} | |
var h = w; | |
var rx = w * 0.5; | |
var ry = w * 0.5; | |
var rotate = 0; | |
var discRad = 55; | |
if( typeof inputGraph == "string" ){ | |
inputGraph = JSON.parse(inputGraph); | |
} | |
var svg; | |
var div; | |
var bundle; | |
var line; | |
var nodes; | |
var splines; | |
var links; | |
var graph; | |
var selectedTree = 0; | |
var selectedTrack = 0; | |
var toggledNodes = {}; | |
var visibleEdges = []; | |
var splineDico; | |
return (function() { | |
function create_bundle() { | |
cluster = d3.layout.cluster() | |
.size([360, ry - discRad]) | |
.sort(function(a, b) { | |
if (!a.sortKey && !b.sortKey) { | |
var aRes = a.key.match(/\d+$/); | |
var bRes = b.key.match(/\d+$/); | |
if (aRes && bRes) { | |
return d3.ascending(parseInt(aRes[0]), parseInt(bRes[0])); | |
} | |
} | |
let aKey = a.sortKey?a.sortKey:a.key; | |
let bKey = b.sortKey?b.sortKey:b.key; | |
return d3.ascending(aKey, bKey); | |
}); | |
graph = preprocessGraph(inputGraph); | |
nodes = cluster.nodes(graph.trees[selectedTree].tree[""]); | |
bundle = d3.layout.bundle(); | |
//links = graph.trees[selectedTree].frames; | |
//splines = bundle(links[0]); | |
//splineDico = buildSplineIndex(splines); | |
links = graph.trees[selectedTree].allEdges; | |
splines = bundle(links); | |
splineDico = buildSplineIndex(splines); | |
line = d3.svg.line.radial() | |
.interpolate("bundle") | |
.tension(0.85) | |
.radius(function(d) { return d.y; }) | |
.angle(function(d) { return d.x / 180 * Math.PI; }); | |
d3.select(containerSelector).style("position","relative"); | |
div = d3.select(containerSelector).insert("div") | |
.style("width", w + "px") | |
.style("height", h + "px") | |
.style("-webkit-backface-visibility", "hidden"); | |
svg = div.append("svg:svg") | |
.attr("width", w) | |
.attr("height", h) | |
.append("svg:g") | |
.attr("transform", "translate(" + rx + "," + ry + ")"); | |
//// Find the width of the node-name track. Temporarily add all text, go through them and get max-width | |
//var tmpTexts = svg.selectAll("g.node") | |
// .data(nodes.filter(function(n) { return !n.children; }), function(d) { return d.key; }) | |
// .enter().append("svg:g") | |
// .attr("class", "node") | |
// .attr("id", function(d) { return "node-" + d.key; }) | |
// .append("text") | |
// .text(function(d) { return d.key; }); | |
//var maxTextWidth = d3.max(svg.selectAll("text")[0], function(t){ return t.getBBox().width; }); | |
//svg.selectAll("g.node").remove(); | |
var path = svg.selectAll("path.link") | |
.data(links, function(d,i){ | |
var key = "source-" + d.source.key + "target-" + d.target.key; | |
return key; | |
}) | |
.enter().append("svg:path") | |
.attr("class", function(d) { | |
var ret = "link source-" + d.source.key + " target-" + d.target.key; | |
if( d.source.key in toggledNodes || d.target.key in toggledNodes) { | |
ret += " toggled"; | |
} | |
return ret; | |
}) | |
.style("stroke-width",function(){ | |
return 0; | |
}) | |
.style("stroke",function(d){ return d.color; }) | |
.style("fill","none") | |
.attr("d", function(d, i) { return line(splines[i]); }) | |
.on("mouseover", function(d){ fireEdgeHoverListeners(d); }) | |
.on("click", function(d){ fireEdgeToggleListeners(d); }); | |
svg.selectAll("g.node") | |
.data(nodes.filter(function(n) { return !n.children; }), function(d) { return d.key; }) | |
.enter().append("svg:g") | |
.attr("class", "node") | |
.attr("id", function(d) { return "node-" + d.key; }) | |
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) | |
.append("text") | |
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; }) | |
.attr("dy", ".31em") | |
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) | |
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }) | |
.text(function(d) { return d.name; }) | |
.on("mouseover", mouseoverNode) | |
.on("mouseout", mouseoutNode) | |
.on("click", function(d){ toggleNode(d); }); | |
var arcW = 250.0/(graph.nodeNames.length)*Math.PI/360; | |
var arc = d3.svg.arc() | |
.innerRadius(ry-55) | |
.outerRadius(function(d,i){ | |
var sz = d.size; | |
if(!sz) { sz = 0.0; } | |
var or = ry-55+sz*15; | |
return or; | |
}) | |
.startAngle(-arcW) | |
.endAngle(arcW); | |
svg.selectAll("g.trackElement") | |
.data(graph.tracks[selectedTrack].trackProperties, function(d){ return d.nodeName; }) | |
.enter().append("svg:g") | |
.attr("class", "trackElement") | |
.attr("id", function(d) { return "trackElement-" + d.nodeName; }) | |
.append("path") | |
.attr("transform", function(d) { | |
var x = graph.trees[selectedTree].tree[d.nodeName].x; | |
return "rotate("+x+")" ; | |
}) | |
.style("fill", function(d){ return d.color; }) | |
.attr("d", arc) | |
.on("mouseover", function(d){ | |
//Locate corresponding node | |
nodes.filter(function(n){ return n.name==d.nodeName; }) | |
.forEach(function(n){ mouseoverNode(n); }); | |
}) | |
.on("mouseout", function(d){ | |
//Locate corresponding node | |
nodes.filter(function(n){ return n.name==d.nodeName; }) | |
.forEach(function(n){ mouseoutNode(n); }); | |
}) | |
.on("click", function(d){ | |
//Locate corresponding node | |
nodes.filter(function(n){ return n.name==d.nodeName; }) | |
.forEach(function(n){ toggleNode(n); }); | |
}); | |
setFrame(0); | |
} | |
/** | |
* Preprocess the graph and return its reference but with additional fields: | |
* - nodeMap: a map associating each node name (string) with a node reference | |
* - nodeNames: list of string-representation of nodes in no particular order | |
* - edges: list of (not necessarily distinct) interactions with interaction timepoints (frames) | |
* - tracks: ... | |
* - trees: ... | |
*/ | |
function preprocessGraph(graph) { | |
"use strict"; | |
// ======= Create defaults, trees, and tracks if they don't exist ========== | |
if (!graph.defaults) { | |
graph.defaults = {}; | |
} | |
if (!graph.trees) { | |
graph.trees = [{"treeLabel": "default", "treeProperties": []}]; | |
} | |
if (!graph.tracks) { | |
graph.tracks = [{"trackLabel": "default", "trackProperties": []}]; | |
} | |
// For backwards compatibility: Create treeProperties from treePaths if they are present | |
graph.trees.forEach(function (t) { | |
if (!t.treeProperties){ | |
t.treeProperties = []; | |
} | |
if (t.treePaths) { | |
t.treePaths.forEach(function (p) { | |
t.treeProperties.push({"path": p}); | |
}); | |
} | |
}); | |
// =========== Construct `nodeNames` list =========== \\ | |
graph.nodeNames = []; | |
//Fill nodeNames from edges | |
graph.edges.forEach(function (e) { | |
if (graph.nodeNames.indexOf(e.name1) == -1) { | |
graph.nodeNames.push(e.name1); | |
} | |
if (graph.nodeNames.indexOf(e.name2) == -1) { | |
graph.nodeNames.push(e.name2); | |
} | |
}); | |
//Fill nodeNames from trees | |
graph.trees.forEach(function (t) { | |
t.treeProperties.forEach(function (p) { | |
var name = p.path.substring(p.path.lastIndexOf(".") + 1); | |
if (!(graph.nodeNames.indexOf(name) > -1)) { | |
graph.nodeNames.push(name); | |
} | |
}); | |
}); | |
//Fill nodeNames from tracks | |
graph.tracks.forEach(function (t) { | |
t.trackProperties.forEach(function (c) { | |
var name = c.nodeName; | |
if (!(graph.nodeNames.indexOf(name) > -1)) { | |
graph.nodeNames.push(name); | |
} | |
}); | |
}); | |
// =========== Parse `edges` section ========== \\ | |
//Go through edges and ensure that default widths have been assigned and frames are sorted | |
graph.edges.forEach(function (e) { | |
e.width = e.width || graph.defaults.edgeWidth || 1; | |
// Sort e.frames, but only if its out of order | |
let last = -100; | |
for (let i=0;i<e.frames.length; i++){ | |
let cur = e.frames[i]; | |
if (last > cur){ | |
e.frames.sort(); | |
break; | |
} | |
last = cur; | |
} | |
// e.frames.sort(); | |
}); | |
// =========== Parse `trees` section ========== \\ | |
function addToMap(nodeMap, fullName) { | |
var i = fullName.lastIndexOf("."); | |
var name = fullName.substring(i + 1); | |
var node = nodeMap[name]; | |
if (!node) { | |
node = {name: name, children: []}; | |
nodeMap[name] = node; | |
if (name.length) { | |
node.parent = addToMap(nodeMap, fullName.substring(0, i)); | |
node.parent.children.push(node); | |
node.key = name.replace(/:/g, "_"); | |
} | |
} | |
return node; | |
} | |
graph.trees.forEach(function (t) { | |
var addedNames = []; | |
//Ensure that each tree-object has a `tree` attribute with the hierarchy | |
t.tree = {}; | |
t.treeProperties.forEach(function (p) { | |
var n = addToMap(t.tree, p.path); | |
//addedNames.push(p.path.substring(p.path.lastIndexOf(".") + 1)); | |
addedNames.push(n.key); | |
if (p.key){ | |
n.sortKey = p.key; | |
} | |
}); | |
//Ensure that even nodes not mentioned in the treePaths are added to the tree | |
graph.nodeNames.forEach(function (p) { | |
if (addedNames.indexOf(p) === -1) { | |
addToMap(t.tree, p); | |
} | |
}); | |
}); | |
//Go through graph.edges and convert name1, name2, and frames to target and source object arrays. | |
graph.trees.forEach(function (t) { | |
t.frames = []; | |
var summaryEdges = {}; | |
t.allEdges = []; | |
graph.edges.forEach(function (e) { | |
//Set source and target of edge | |
var edge = { | |
source: t.tree[e.name1], | |
target: t.tree[e.name2], | |
key: "" + t.tree[e.name1].key + "-" + t.tree[e.name2].key, | |
color: e.color || graph.defaults.edgeColor || "rgba(100,100,100)", | |
opacity: e.opacity || graph.defaults.edgeOpacity || 1, | |
width: e.width || graph.defaults.edgeWidth || 1 | |
}; | |
var edgeKey = edge.key; | |
if (!summaryEdges[edgeKey]) { | |
summaryEdges[edgeKey] = { | |
source: edge.source, | |
target: edge.target, | |
key: edge.key, | |
color: edge.color, | |
opacity: edge.opacity, | |
width: edge.width | |
}; | |
t.allEdges.push({ | |
source: edge.source, | |
target: edge.target, | |
key: edge.key, | |
color: edge.color, | |
opacity: edge.opacity, | |
width: edge.width | |
}); | |
} else { | |
summaryEdges[edgeKey].width += edge.width; | |
} | |
//edge.source = t.tree[edge.name1]; console.assert(edge.source); | |
//edge.target = t.tree[edge.name2]; console.assert(edge.target); | |
//edge.key = ""+i; | |
//Add interaction frames | |
e.frames.forEach(function (f) { | |
while (t.frames.length <= f) { | |
t.frames.push([]); | |
} | |
t.frames[f].push(edge); | |
}); | |
}); | |
t.summaryEdges = summaryEdges; | |
}); | |
// =========== Parse `tracks` section ========== \\ | |
graph.tracks.forEach(function (track) { | |
//Ensure that nodes not mentioned in the trackProperties are created with default values | |
var remainingNodeNames = []; | |
graph.nodeNames.forEach(function (n) { | |
remainingNodeNames.push(n); | |
}); | |
track.trackProperties.forEach(function (p) { | |
//Remove p.name from remainingNodeNames | |
var idx = remainingNodeNames.indexOf(p.nodeName); | |
if (idx > -1) { | |
remainingNodeNames.splice(idx, 1); | |
} | |
}); | |
remainingNodeNames.forEach(function (n) { | |
var color = graph.defaults.trackColor || "white"; | |
var size = graph.defaults.trackSize || 0; | |
track.trackProperties.push({"nodeName": n, "color": color, "size": size}); | |
}); | |
}); | |
// =========== Parse `defaults` section ========== \\ | |
//From https://stackoverflow.com/questions/566203/changing-css-values-with-javascript | |
function insertCSSrule(selector, property, value) { | |
for (var i=0; i<document.styleSheets.length;i++) {//Loop through all styles | |
try { | |
document.styleSheets[i].insertRule(selector+ ' {'+property+':'+value+'}', document.styleSheets[i].cssRules.length); | |
} catch(err) {//IE | |
try { | |
document.styleSheets[i].addRule(selector, property+':'+value); | |
} catch(err) {} | |
} | |
} | |
} | |
if (graph.defaults.edgeOpacity) { | |
insertCSSrule(".link", "stroke-opacity", graph.defaults.edgeOpacity); | |
insertCSSrule(".link", "opacity", graph.defaults.edgeOpacity); | |
} | |
return graph; | |
} | |
/** | |
* | |
* @param clusterDefinition {} | |
* | |
* keys are the key of the cluster | |
* values are array that correspond to node keys | |
* | |
*/ | |
function assignCluster(clusterDefinition, oldCluster, graph) { | |
var nodesMap = clusterDefinition.tree; | |
var root = nodesMap[""]; | |
var rootNodes = root.children; | |
// recursively copy x and y propery from the old cluster | |
rootNodes.forEach(copyAndGoThruChildren); | |
function copyAndGoThruChildren(node) { | |
var newNode; | |
var nodeKey = node.key; | |
if (nodesMap[nodeKey]) { | |
newNode = nodesMap[nodeKey]; | |
var oldNode = oldCluster.tree[nodeKey]; | |
if (oldNode) { | |
newNode.oldX = oldNode.x; | |
newNode.oldY = oldNode.y; | |
} | |
} else | |
{ | |
// it could happen that an new node come (in case of intermediate level) | |
newNode = nodesMap[nodeKey]; | |
newNode.clusterName = nodeKey; | |
} | |
if (newNode.children && newNode.children.length > 0) { | |
newNode.children.forEach(copyAndGoThruChildren); | |
} | |
} | |
} | |
/** | |
* Find the total number of frames in the graph. | |
* @returns {number} | |
*/ | |
function getNumFrames(){ | |
var maxFrame = -1; | |
graph.edges.forEach(function(e){ | |
// maxFrame = Math.max(maxFrame, Math.max.apply(Math, e.frames)); | |
maxFrame = Math.max(maxFrame, e.frames[e.frames.length - 1]); | |
}); | |
return maxFrame+1; | |
} | |
/** | |
* Change the state of the flareplot so it reflects the interactions in the indicated frame. | |
* @param frameNum a number indicating the frame to set. | |
*/ | |
function setFrame(frameNum){ | |
frameNum = parseInt(frameNum); | |
rangeSum(frameNum,frameNum+1); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the intersection over a range. | |
* @param rangeStart first frame to include (must be less than `rangeEnd`) | |
* @param rangeEnd first frame after `rangeStart` that should not be included | |
*/ | |
function rangeIntersect(rangeStart, rangeEnd){ | |
//splines = bundle(links); | |
//splineDico = buildSplineIndex(splines); | |
var path = svg.selectAll("path.link"); | |
path.style("stroke-width", | |
function(d,i){ | |
var count = graph.edges[i].frames.rangeCount(rangeStart, rangeEnd-1); | |
return count==(rangeEnd-rangeStart)?(2 * graph.edges[i].width):0; | |
}) | |
.attr("class", function(d) { | |
var ret = "link source-" + d.source.key + " target-" + d.target.key; | |
if( d.source.key in toggledNodes || d.target.key in toggledNodes) { | |
ret += " toggled"; | |
} | |
return ret; | |
}) | |
//.attr("class", function(d) { return "link source-" + d.source.key + " target-" + d.target.key; }) | |
.style("stroke",function(d){ return d.color; }) | |
.attr("d", function(d, i) { return line(splines[i]); }); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the sum over a range. | |
* @param rangeStart first frame to include (must be less than `rangeEnd`) | |
* @param rangeEnd first frame after `rangeStart` that should not be included | |
*/ | |
function rangeSum(rangeStart, rangeEnd){ | |
//splines = bundle(links); | |
//splineDico = buildSplineIndex(splines); | |
var path = svg.selectAll("path.link"); | |
var widthScale = d3.scale.linear() | |
.domain([1,Math.max(1,rangeEnd-rangeStart)]) | |
.range([2,10]); | |
visibleEdges = []; | |
path.style("stroke-width", | |
function(d,i){ | |
var count = graph.edges[i].frames.rangeCount(rangeStart, rangeEnd-1); | |
if (count>0){ | |
var e = {edge:graph.edges[i], weight:count/(rangeEnd-rangeStart)}; | |
e.toggled = e.edge.name1 in toggledNodes || e.edge.name2 in toggledNodes; | |
visibleEdges.push(e); | |
return widthScale(count) * graph.edges[i].width; | |
} else { | |
return 0; | |
} | |
}) | |
.attr("class", function(d) { | |
var ret = "link source-" + d.source.key + " target-" + d.target.key; | |
if( d.source.key in toggledNodes || d.target.key in toggledNodes) { | |
ret += " toggled"; | |
} | |
return ret; | |
}) | |
//.attr("class", function(d) { return "link source-" + d.source.key + " target-" + d.target.key; }) | |
.style("stroke",function(d){ return d.color; }) | |
.attr("d", function(d, i) { return line(splines[i]); }); | |
// fireFrameListeners(visibleEdges); | |
fireFrameListeners({type:"sum", start:rangeStart, end:rangeEnd}); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the intersection over a subset. | |
* @param subset a list of numbers indicating which frames to include | |
*/ | |
function subsetIntersect(subset, subtract){ | |
//splines = bundle(links); | |
//splineDico = buildSplineIndex(splines); | |
var path = svg.selectAll("path.link"); | |
visibleEdges = []; | |
path.style("stroke-width", | |
function(d,i) { | |
for (var c = 0; c < subset.length; c++) { | |
var frame = subset[c]; | |
var iud = graph.edges[i].frames.indexUpDown(frame); | |
if (iud[0] != iud[1]) return 0; | |
} | |
if (subtract) { | |
for (var c = 0; c < subtract.length; c++) { | |
var frame = subtract[c]; | |
var iud = graph.edges[i].frames.indexUpDown(frame); | |
if (iud[0] == iud[1]) return 0; | |
} | |
} | |
var e = {edge:graph.edges[i], weight:1}; | |
e.toggled = e.edge.name1 in toggledNodes || e.edge.name2 in toggledNodes; | |
visibleEdges.push(e); | |
return 2 * e.weight; | |
}) | |
.attr("class", function(d) { | |
var ret = "link source-" + d.source.key + " target-" + d.target.key; | |
if( d.source.key in toggledNodes || d.target.key in toggledNodes) | |
ret+=" toggled"; | |
return ret; | |
}) | |
.style("stroke",function(d){ return d.color; }) | |
.attr("d", function(d, i) { return line(splines[i]); }); | |
fireFrameListeners({type:"intersect", intersected:subset, excluded:subtract}); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the sum over a subset. | |
* @param subset a list of numbers indicating which frames to include | |
*/ | |
function subsetSum(subset){ | |
//splines = bundle(links); | |
//splineDico = buildSplineIndex(splines); | |
var path = svg.selectAll("path.link"); | |
visibleEdges = []; | |
var widthScale = d3.scale.linear() | |
.domain([1,subset.length]) | |
.range([2,10]); | |
path.style("stroke-width", | |
function(d,i){ | |
var count = 0; | |
subset.forEach(function(f){ | |
var iud = graph.edges[i].frames.indexUpDown(f); | |
if( iud[0] == iud[1] ){ count++; } | |
}); | |
var e = {edge:graph.edges[i], weight:count/subset.length}; | |
e.toggled = e.edge.name1 in toggledNodes || e.edge.name2 in toggledNodes; | |
visibleEdges.push(e); | |
return count==0?0:(widthScale(count) * e.weight); | |
}) | |
.attr("class", function(d) { | |
var ret = "link source-" + d.source.key + " target-" + d.target.key; | |
if( d.source.key in toggledNodes || d.target.key in toggledNodes) { | |
ret += " toggled"; | |
} | |
return ret; | |
}) | |
//.attr("class", function(d) { return "link source-" + d.source.key + " target-" + d.target.key; }) | |
.style("stroke",function(d){ return d.color; }) | |
.attr("d", function(d, i) { return line(splines[i]); }); | |
fireFrameListeners({type:"setsum", set:subset}); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the intersection over the specified selection. If | |
* `selection` and `optionalSelection` are both numbers then the intersection will be taken over the range of | |
* frames spanned by the two. If `selection` is an array of numbers the intersection will be taken over the | |
* frames in the array. If the arguments don't satisfy these requirements an error is thrown. | |
* @param selection | |
* @param optionalSelection | |
* @returns {*} | |
*/ | |
function framesIntersect(selection, optionalSelection) { | |
if (typeof selection === "number" && typeof optionalSelection === "number") { | |
return rangeIntersect(selection, optionalSelection); | |
} | |
if (Object.prototype.toString.call(selection) === "[object Array]" && optionalSelection === undefined) { | |
return subsetIntersect(selection); | |
} | |
throw "framesIntersect must take either two integers (range), or an array (subset) as argument"; | |
} | |
function framesIntersectSubtract(intersectSelection, subtractSelection) { | |
return subsetIntersect(intersectSelection, subtractSelection); | |
} | |
/** | |
* Update the state of the flareplot so it reflects the sum over the specified selection. If `selection` and | |
* `optionalSelection` are both numbers then the intersection will be taken over the range of frames spanned by | |
* the two. If `selection` is an array of numbers the intersection will be taken over the frames in the array. | |
* If the arguments don't satisfy these requirements an error is thrown. | |
* @param selection | |
* @param optionalSelection | |
* @returns {*} | |
*/ | |
function framesSum(selection, optionalSelection) { | |
if (typeof selection === "number" && typeof optionalSelection === "number") { | |
return rangeSum(selection, optionalSelection); | |
} | |
if (Object.prototype.toString.call(selection) === "[object Array]" && optionalSelection === undefined) { | |
return subsetSum(selection); | |
} | |
throw "framesSum must take either two integers (range), or an array (subset) as argument"; | |
} | |
function toggleNode(node){ | |
var svgNodeElement = svg.selectAll("g.node#node-"+node.key).node(); | |
var toggled = !d3.select(svgNodeElement).classed("toggledNode"); | |
d3.select(svgNodeElement) | |
.classed("toggledNode", function(){return toggled; }); | |
var name = node.key.substring(node.key.lastIndexOf(".")+1); | |
if(!toggled) | |
delete toggledNodes[name]; | |
else | |
toggledNodes[name] = ""; | |
path = svg.selectAll("path.link") | |
.classed("toggled", function(d) { | |
return ( d.source.key in toggledNodes || d.target.key in toggledNodes) | |
}); | |
visibleEdges.forEach(function(e){ | |
if(e.edge.name1==name || e.edge.name2==name){ | |
e.toggled = toggled; | |
} | |
}); | |
fireNodeToggleListeners({"name": node.name, "key": node.key}); | |
} | |
function mouseoverNode(d) { | |
svg.selectAll("path.link.target-" + d.key) | |
.classed("target", true) | |
.each(updateNodes("source", true)); | |
svg.selectAll("path.link.source-" + d.key) | |
.classed("source", true) | |
.each(updateNodes("target", true)); | |
} | |
function mouseoutNode(d) { | |
svg.selectAll("path.link.source-" + d.key) | |
.classed("source", false) | |
.each(updateNodes("target", false)); | |
svg.selectAll("path.link.target-" + d.key) | |
.classed("target", false) | |
.each(updateNodes("source", false)); | |
} | |
function updateNodes(name, value) { | |
return function(d) { | |
//if (value) this.parentNode.appendChild(this); | |
svg.select("#node-" + d[name].key).classed(name, value); | |
}; | |
} | |
function getTreeNames(){ | |
var ret = []; | |
for (var t=0; t<graph.trees.length; t++ ){ | |
ret.push(graph.trees[t].treeLabel); | |
} | |
return ret; | |
} | |
function setTree(treeIdx){ | |
var oldTreeIdx = selectedTree; | |
selectedTree = treeIdx; | |
assignCluster(graph.trees[selectedTree], graph.trees[oldTreeIdx], graph); | |
var recomposedSplines = []; | |
nodes = cluster.nodes(graph.trees[selectedTree].tree[""]); | |
links = Object.values(graph.trees[selectedTree].summaryEdges); | |
svg.selectAll("g.node") | |
.data(nodes.filter(function(n) { return !n.children; }), function(d) { return d.key}) | |
.transition().duration(500) | |
//.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; }) | |
.attrTween("transform", function(d) { | |
var oldMatrix = "rotate(" + (d.oldX - 90) + ")translate(" + d.y + ")"; | |
var newMatrix = "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; | |
return d3.interpolateString(oldMatrix, newMatrix); | |
}) | |
.select("text") | |
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; }) | |
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) | |
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; }); | |
var arcW = 250.0/(graph.nodeNames.length)*Math.PI/360; | |
var arc = d3.svg.arc() | |
.innerRadius(ry-15) | |
.outerRadius(function(d){ | |
var sz = d.size; | |
if(!sz) sz = 0.0; | |
return ry-15+sz*15; | |
}) | |
.startAngle(-arcW) | |
.endAngle(arcW); | |
svg.selectAll("g.trackElement") | |
.select("path") | |
.transition().duration(500) | |
.attrTween("transform", function(d) { | |
var node = graph.trees[selectedTree].tree[d.nodeName]; | |
var oldMatrix = "rotate(" + (node.oldX) + ")"; | |
var newMatrix = "rotate(" + (node.x) + ")"; | |
return d3.interpolateString(oldMatrix, newMatrix); | |
}) | |
.style("fill", function(d){ return d.color; }) | |
.attr("d", arc); | |
// transition the splines | |
var newSplines = bundle(Object.values((graph.trees[selectedTree].summaryEdges))); | |
var newSplinesDico = buildSplineIndex(newSplines); | |
var done = false; | |
var path = svg.selectAll("path.link").data(links, function(d){ | |
return d.key; | |
}); | |
// i dont understand how d3 orders the spline array, so we need | |
path.transition().attrTween("d", | |
function(d, i, a) { | |
//if (i != 2) return; | |
// make a copy of the targeted Spline, and put all x to the value of OldX.. | |
var oldSpline = []; | |
var key = d.key; | |
var oldSplineIdx = splineDico[key]; | |
var newSplineIdx = newSplinesDico[key]; | |
if (oldSplineIdx === void 0 || newSplineIdx === void 0) { | |
console.log("Not found Spline with key", key); | |
return; | |
} | |
for (var j = 0; j < splines[oldSplineIdx].length; j++) { | |
var s = Object.assign({}, splines[oldSplineIdx][j]); | |
oldSpline.push(s); | |
} | |
oldSpline = oldSpline.map(function(s) { | |
return {x: s.x, y: s.y}; | |
}); | |
var simpleSpline = newSplines[newSplineIdx].map(function(s) { | |
return {x: s.x, y:s.y, key:s.key} | |
}); | |
// now if oldspine is missing controlpoints | |
var delta = simpleSpline.length - oldSpline.length; | |
if (oldSpline.length < simpleSpline.length) { | |
//positive delta | |
var recomposedOldSpline = []; | |
// we make the assumption that we start with 3 control points | |
// but they may be more complicated situations | |
// if delta = 2 0 - 0, 1-0, 2-1, 3-2, 4-2 (3 to 5 ) | |
// if delta = 4 0-0 1-0 2-0, 3-1, 4-2, 5-2, 6-2 ( 3 to 7 ) | |
// if delta = 2 ( 5 to 7) what happens ? | |
// if delta = 4 ( 5 to 9) what happens ? | |
for (i = 0, currentIndex = 0; i < simpleSpline.length; i++) { | |
recomposedOldSpline[i] = oldSpline[currentIndex]; | |
if (i <= delta/2 || currentIndex >= oldSpline.length - 1) { } else { | |
currentIndex++; | |
} | |
} | |
} else if (delta < 0) { // (5 < 3) | |
// newer spline has less target point than older spline | |
var recomposedNewSpline = []; | |
// -2, 5 to 3 => 0 -0, 1-0, 2-1, 3-2,4-2 (simplespline 3, oldSpine = 5) | |
// -4 ,7 to 3 => 0-0, 1-0, 2-0, 3-1, 4-2 5-2 6-2 | |
delta = Math.abs(delta); | |
for (i = 0, currentIndex = 0; i < oldSpline.length; i++) { | |
recomposedNewSpline[i] = simpleSpline[currentIndex]; | |
if (i <= Math.floor(delta / 2) || currentIndex >= simpleSpline.length - 1) {} else { | |
currentIndex++; | |
} | |
} | |
simpleSpline = recomposedNewSpline; | |
recomposedOldSpline = oldSpline; | |
} else | |
{ | |
recomposedOldSpline = oldSpline; | |
} | |
recomposedSplines.push(simpleSpline); | |
var interpolate = d3.interpolate(recomposedOldSpline, simpleSpline); | |
// we can update the splines at the next loop, or it will mess D3 | |
setTimeout(function(){ | |
if (!done){ | |
done = true; | |
splines = recomposedSplines; | |
splineDico = buildSplineIndex(recomposedSplines); | |
// we do not want to rebind data here | |
} | |
}, 500); | |
return function(t) { | |
return line(interpolate(t)) | |
}; | |
}) | |
.duration(500); | |
} | |
/** | |
* For all splines in the array, create an index that match the key of | |
* the link and the index in the spline array. | |
*/ | |
function buildSplineIndex(splines) { | |
var linkKeyToSplineIdx = {}; | |
splines.forEach(function(spline, idx){ | |
var source = spline[0].key; | |
var target = spline[spline.length-1].key; | |
var key = source + "-" + target; | |
linkKeyToSplineIdx[key] = idx; | |
}); | |
return linkKeyToSplineIdx; | |
} | |
function getTrackNames(){ | |
var ret = []; | |
for(var t=0;t<graph.tracks.length;t++){ | |
ret.push(graph.tracks[t].trackLabel); | |
} | |
return ret; | |
} | |
function setTrack(trackIdx){ | |
selectedTrack = trackIdx; | |
var arcW = 250.0/(graph.nodeNames.length)*Math.PI/360; | |
var arc = d3.svg.arc() | |
.innerRadius(ry-15) | |
.outerRadius(function(d){ | |
var sz = d.size; | |
if(!sz) sz = 0.0; | |
return ry-15+sz*15; | |
}) | |
.startAngle(-arcW) | |
.endAngle(arcW); | |
svg.selectAll("g.trackElement") | |
.data(graph.tracks[selectedTrack].trackProperties, function(d){ return d.nodeName; }) | |
.select("path") | |
.transition() | |
.style("fill", function(d){ return d.color; }) | |
.attr("d", arc); | |
} | |
function setTension(tension){ | |
line.tension(tension); | |
var path = svg.selectAll("path.link") | |
.attr("d", function(d, i) { return line(splines[i]); }) | |
} | |
function getEdges(){ | |
return visibleEdges; | |
} | |
var nodeToggleListeners = []; | |
var nodeHoverListeners = []; | |
var edgeToggleListeners = []; | |
var edgeHoverListeners = []; | |
var frameListeners = []; | |
function addNodeToggleListener(l){ nodeToggleListeners.push(l); } | |
function addNodeHoverListener(l){ nodeHoverListeners.push(l); } | |
function addEdgeToggleListener(l){ edgeToggleListeners.push(l); } | |
function addEdgeHoverListener(l){ edgeHoverListeners.push(l); } | |
function addFrameListener(l){ frameListeners.push(l); } | |
function fireNodeToggleListeners(n){ nodeToggleListeners.forEach(function(l){l(n);}); } | |
function fireNodeHoverListeners(n){ nodeHoverListeners.forEach(function(l){l(n);}); } | |
function fireEdgeToggleListeners(n){ edgeToggleListeners.forEach(function(l){l(n);}); } | |
function fireEdgeHoverListeners(n){ edgeHoverListeners.forEach(function(l){l(n);}); } | |
function fireFrameListeners(f){ frameListeners.forEach(function(l){l(f);}); } | |
create_bundle(); | |
return { | |
getNumFrames: getNumFrames, | |
setFrame: setFrame, | |
getEdges: getEdges, | |
framesIntersect: framesIntersect, | |
framesSum: framesSum, | |
framesIntersectSubtract: framesIntersectSubtract, | |
setTrack: setTrack, | |
setTree: setTree, | |
getTreeNames: getTreeNames, | |
getTrackNames: getTrackNames, | |
setTension: setTension, | |
addNodeToggleListener: addNodeToggleListener, | |
addNodeHoverListener: addNodeHoverListener, | |
addEdgeToggleListener: addEdgeToggleListener, | |
addEdgeHoverListener: addEdgeHoverListener, | |
addFrameListener: addFrameListener, | |
graph: graph//, for debugging purposes | |
} | |
}) (); | |
} | |
function upload_button(el, callback) { | |
var uploader = document.getElementById(el); | |
var reader = new FileReader(); | |
reader.onload = function(e) { | |
var contents = e.target.result; | |
callback(contents); | |
}; | |
uploader.addEventListener("change", handleFiles, false); | |
function handleFiles() { | |
d3.select("#table").text("loading..."); | |
var file = this.files[0]; | |
reader.readAsText(file); | |
} | |
} | |
/** | |
* Gets the index of the value just above and just below `key` in a sorted array. | |
* If the exact element was found, the two indices are identical. | |
*/ | |
function indexUpDown(key) { | |
"use strict"; | |
var minIdx = 0; | |
var maxIdx = this.length - 1; | |
var curIdx, curElm, resIdx; | |
while (minIdx <= maxIdx) { | |
resIdx = curIdx = (minIdx + maxIdx) / 2 | 0; | |
curElm = this[curIdx]; | |
if (curElm < key) minIdx = curIdx + 1; | |
else if (curElm > key) maxIdx = curIdx - 1; | |
else return [curIdx,curIdx]; | |
} | |
return [minIdx,maxIdx]; | |
} | |
/** Get the number of entries whose value are greater than or equal to `start` | |
* and lower than or equal to `end` in a sorted array*/ | |
function rangeCount(start, end){ | |
var startIdx = this.indexUpDown(start)[0]; | |
var endIdx = this.indexUpDown(end)[1]; | |
return endIdx-startIdx+1; | |
} | |
Array.prototype.indexUpDown = indexUpDown; | |
Array.prototype.rangeCount = rangeCount; | |
// var list = [1,2,5,10,15,16]; | |
// function testRange(l,s,e, expected){ | |
// var res = l.rangeCount(s,e); | |
// console.log("["+l+"].count("+s+","+e+") -> "+res+" expects "+expected+(res==expected?" PASS":" FAILED")); | |
// } | |
// | |
// testRange(list, 0, 0, 0); | |
// testRange(list, 0, 1, 1); | |
// testRange(list, -10, -1, 0); | |
// testRange(list, 1, 1, 1); | |
// testRange(list, 1, 2, 2); | |
// testRange(list, 2, 2, 1); | |
// testRange(list, 2, 4, 1); | |
// testRange(list, 2, 5, 2); | |
// testRange(list, 16, 16, 1); | |
// testRange(list, 16, 20, 1); | |
// testRange(list, 17, 17, 0); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment