Related
I would like to add icons or images to nodes. That is, the nodes people with an icon or image, the nodes, cell phones with an icon image and so on.
So far my code is as follows. It should be noted that the data is obtained directly from my base in NE4OJ
Cada color representa una propiedad, Quisera añadir un icono o imagen a cada nodo
var graphWidth = 1024;
var graphHeight = 450;
var neo4jAPIURL = 'http://localhost:7474';
var neo4jLogin = 'USER';
var neo4jPassword = 'PASSWORD';
var circleSize = 30;
var textPosOffsetY = 5;
var arrowWidth = 5;
var arrowHeight = 5;
var collideForceSize = circleSize * 1.5;
var linkForceSize = 150;
var iconPosOffset = {'lock': [-40, -50], 'cross': [18, -50]};
var linkTypeMapping = {'OUT_ADD': '+', 'OUT_SUB': '-', 'IN_AND': 'AND', 'IN_OR': 'OR'};
var lockIconSVG = 'm18,8l-1,0l0,-2c0,-2.76 -3.28865,-5.03754 -5,-5c-1.71135,0.03754 -5.12064,0.07507 -5,4l1.9,0c0,-1.71 1.39,-2.1 3.1,-2.1c1.71,0 3.1,1.39 3.1,3.1l0,2l-9.1,0c-1.1,0 -2,0.9 -2,2l0,10c0,1.1 0.9,2 2,2l12,0c1.1,0 2,-0.9 2,-2l0,-10c0,-1.1 -0.9,-2 -2,-2zm0,12l-12,0l0,-10l12,0l0,10z';
var crossIconSVG = 'M14.59 8L12 10.59 9.41 8 8 9.41 10.59 12 8 14.59 9.41 16 12 13.41 14.59 16 16 14.59 13.41 12 16 9.41 14.59 8zM12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z';
var boxSVG = 'M0 0 L23 0 L23 23 L0 23 Z';
//######################### variable #########################
var nodeItemMap = {};
var linkItemMap = {};
var d3Simulation = null;
var circles;
var circleText;
var lines;
var lineText;
var iconLock;
var iconCross;
var itemColorMap = {};
var colorScale = d3.scaleOrdinal(d3.schemeSet2);
var drag_handler = d3.drag()
.on('start', drag_start)
.on('drag', drag_move)
.on('end', drag_end);
var zoom_handler = d3.zoom()
.filter(function() {
//Only enable wheel zoom and mousedown to pan
return (d3.event.type == 'wheel' | d3.event.type == 'mousedown');
})
.on('zoom', zoom_actions);
function unfreezeItms() {
var nodeItmArray = d3Simulation.nodes();
if (nodeItmArray != null) {
nodeItmArray.forEach(function(nodeItm) {
if (nodeItm.fx != null) {
nodeItm.fx = null;
nodeItm.fy = null;
}
});
}
}
function drag_start(d) {
//if (!d3.event.active && d3Simulation != null)
d3Simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function drag_move(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function drag_end(d) {
if (!d3.event.active && d3Simulation != null)
d3Simulation.alphaTarget(0);
//d.fx = null;
//d.fy = null;
}
function zoom_actions(){
d3.select('#resultSvg').select('g').attr('transform', d3.event.transform);
}
function initGraph() {
var svg = d3.select('#resultSvg');
var zoomGLayer = svg.append('g');
var centerX = graphWidth / 2;
var centerY = graphHeight / 2;
svg.attr('width', graphWidth)
.attr('height', graphHeight);
/*
var defs = svg.append('defs');
Not use marker as IE does not support it and so embed the arrow in the path directly
// define arrow markers for graph links
defs.append('marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 10)
.attr('markerWidth', 5)
.attr('markerHeight', 5)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5');
*/
zoomGLayer.append('g').attr('id', 'circle-group').attr('transform', 'translate(' + centerX + ',' + centerY + ')');
zoomGLayer.append('g').attr('id', 'text-group').attr('transform', 'translate(' + centerX + ',' + centerY + ')');
zoomGLayer.append('g').attr('id', 'path-group').attr('transform', 'translate(' + centerX + ',' + centerY + ')');
zoomGLayer.append('g').attr('id', 'path-label-group').attr('transform', 'translate(' + centerX + ',' + centerY + ')');
zoomGLayer.append('g').attr('id', 'control-icon-group').attr('transform', 'translate(' + centerX + ',' + centerY + ')');
zoom_handler(svg);
}
function stopSimulation() {
if (d3Simulation != null) {
d3Simulation.stop()
.on('tick', null);
d3Simulation = null;
}
}
function tick() {
lines.attr('d', drawLine);
lineText.attr('transform', transformPathLabel);
circles.attr('transform', transform);
circleText.attr('transform', transform);
iconLock.attr('transform', function(d) {return transformIcon(d, 'lock');});
iconCross.attr('transform', function(d) {return transformIcon(d, 'cross');});
}
function transformIcon(d, type) {
var sourceX = d.x + iconPosOffset[type][0];
var sourceY = d.y + iconPosOffset[type][1];
return 'translate(' + sourceX + ',' + sourceY + ')';
}
function transformPathLabel(d) {
var sourceX = d.source.x + ((d.target.x - d.source.x) / 2);
var sourceY = d.source.y + ((d.target.y - d.source.y) / 2);
return 'translate(' + sourceX + ',' + sourceY + ')';
}
function transform(d) {
return 'translate(' + d.x + ',' + d.y + ')';
}
function drawLine(d) {
var deltaX, deltaY, dist, cosTheta, sinTheta, sourceX, sourceY, targetX, targetY;
deltaX = d.target.x - d.source.x;
deltaY = d.target.y - d.source.y;
dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
cosTheta = deltaX / dist;
sinTheta = deltaY / dist;
sourceX = d.source.x + (circleSize * cosTheta);
sourceY = d.source.y + (circleSize * sinTheta);
targetX = d.target.x - (circleSize * cosTheta);
targetY = d.target.y - (circleSize * sinTheta);
//Not use marker as IE does not support it and so embed the arrow in the path directly
var arrowLeftX, arrowLeftY, arrowRightX, arrowRightY;
arrowLeftX = targetX - (arrowHeight * sinTheta) - (arrowWidth * cosTheta);
arrowLeftY = targetY + (arrowHeight * cosTheta) - (arrowWidth * sinTheta);
arrowRightX = targetX + (arrowHeight * sinTheta) - (arrowWidth * cosTheta);
arrowRightY = targetY - (arrowHeight * cosTheta) - (arrowWidth * sinTheta);
return 'M' + sourceX + ' ' + sourceY + ' L' + targetX + ' ' + targetY
+ ' M' + targetX + ' ' + targetY + ' L' + arrowLeftX + ' ' + arrowLeftY
+ ' L' + arrowRightX + ' ' + arrowRightY + ' Z';
}
function clearProperties() {
$('#propertiesBox').empty();
}
function showProperties(d) {
clearProperties();
var propertiesText = 'id: ' + d.id;
//For nodes
//if (d.labels != null)
// propertiesText += ', labels: ' + d.labels.join(', ');
//For links
//if (d.type != null)
// propertiesText += ', type: ' + d.type;
$.map(d.properties, function(value, key) {
propertiesText += ', ' + key + ': ' + value;
});
$('#propertiesBox').append($('<p></p>').text(propertiesText));
}
function replaceLinkTypeName(d) {
var linkTypeName = linkTypeMapping[d.type];
if (linkTypeName == null)
return d.type;
return linkTypeName;
}
function generateCircleClasses(d) {
if (d.properties != null && d.properties.cyclic == '1')
return 'Cyclic ' + d.labels.join(' ');
return d.labels.join(' ');
}
function removeNode(d) {
delete nodeItemMap[d.id];
$.map(linkItemMap, function(value, key) {
if (value.startNode == d.id || value.endNode == d.id)
delete linkItemMap[key];
});
}
function updateGraph() {
var d3LinkForce = d3.forceLink()
.distance(linkForceSize)
.links(mapToArray(linkItemMap))
.id(function(d) {return d.id;});
d3Simulation = d3.forceSimulation()
//.force('chargeForce', d3.forceManyBody())//.strength(-300)
.force('collideForce', d3.forceCollide(collideForceSize))
.nodes(mapToArray(nodeItemMap))
.force('linkForce', d3LinkForce);
circles = d3.select('#circle-group').selectAll('circle')
.data(d3Simulation.nodes(), function(d) {return d.id;});
circleText = d3.select('#text-group').selectAll('text')
.data(d3Simulation.nodes(), function(d) {return d.id;});
lines = d3.select('#path-group').selectAll('path')
.data(d3LinkForce.links(), function(d) {return d.id;});
lineText = d3.select('#path-label-group').selectAll('text')
.data(d3LinkForce.links(), function(d) {return d.id;});
iconLock = d3.select('#control-icon-group').selectAll('g.lockIcon')
.data([], function(d) {return d.id;});
iconCross = d3.select('#control-icon-group').selectAll('g.crossIcon')
.data([], function(d) {return d.id;});
iconLock.exit().remove();
iconCross.exit().remove();
circles.exit().remove();
circles = circles.enter().append('circle')
.attr('r', circleSize)
.attr('fill', getItemColor)
.attr('title', function(d) {return d.labels.join('-');})
.attr('class', function(d) {return generateCircleClasses(d);})
.call(drag_handler)
.on('mouseover', function(d) {
d3.select(this)
showProperties(d);
})
.on('dblclick', function(d) {
d.fx = d.x;
d.fy = d.y;
submitQuery(d.id);
})
.on('click', function(d) {
iconLock = d3.select('#control-icon-group').selectAll('g.lockIcon')
.data([d], function(d) {return d.id;});
iconCross = d3.select('#control-icon-group').selectAll('g.crossIcon')
.data([d], function(d) {return d.id;});
iconLock.exit().remove();
iconLock.remove();
iconCross.exit().remove();
iconCross.remove();
var iconLockEnter = iconLock.enter().append('g')
.attr('class', 'lockIcon')
.attr('transform', function(d) {
return transformIcon(d, 'lock');
})
.on('click', function(d) {
d.fx = null;
d.fy = null;
iconLock.remove();
iconCross.remove();
});
iconLockEnter.append('path').attr('class', 'overlay').attr('d', boxSVG);
iconLockEnter.append('path').attr('d', lockIconSVG);
var iconCrossEnter = iconCross.enter().append('g')
.attr('class', 'crossIcon')
.attr('transform', function(d) {
return transformIcon(d, 'cross');
})
.on('click', function(d) {
removeNode(d);
updateGraph();
});
iconCrossEnter.append('path').attr('class', 'overlay').attr('d', boxSVG);
iconCrossEnter.append('path').attr('d', crossIconSVG);
iconLock = iconLockEnter
.merge(iconLock);
iconCross = iconCrossEnter
.merge(iconCross);
})
.merge(circles);
circleText.exit().remove();
circleText = circleText.enter().append('text')
.attr('y', textPosOffsetY)
.attr('text-anchor', 'middle')
.text(function(d) {
return [
d.properties.name
];})
.merge(circleText);
lines.exit().remove();
lines = lines.enter().append('path')
//.attr('marker-end', 'url(#end-arrow)')
.attr('title', function(d) {return d.type;})
.attr('class', function(d) {return d.type;})
.on('mouseover', function(d) {
showProperties(d);
})
.merge(lines);
lineText.exit().remove();
lineText = lineText.enter().append('text')
.attr('y', textPosOffsetY)
.attr('text-anchor', 'middle')
.text(function(d) {return replaceLinkTypeName(d);})
.merge(lineText);
d3Simulation
.on('tick', tick);
}
function submitQuery(nodeID) {
removeAlert();
var queryStr = null;
if (nodeID == null || !nodeID) {
queryStr = $.trim($('#queryText').val());
if (queryStr == '') {
promptAlert($('#graphContainer'), 'Error: el texto de consulta no puede estar vacío !', true);
return;
}
if ($('#chkboxCypherQry:checked').val() != 1)
queryStr = 'match (n) where n.Cedula =~ \'(?i).*' + queryStr + '.*\' return n';
} else
queryStr = 'match (n)-[j]-(k) where id(n) = ' + nodeID + ' return n,j,k';
stopSimulation();
if (nodeID == null || !nodeID) {
nodeItemMap = {};
linkItemMap = {};
}
var jqxhr = $.post(neo4jAPIURL, '{"statements":[{"statement":"' + queryStr + '", "resultDataContents":["graph"]}]}',
function(data) {
//console.log(JSON.stringify(data));
if (data.errors != null && data.errors.length > 0) {
promptAlert($('#graphContainer'), 'Error: ' + data.errors[0].message + '(' + data.errors[0].code + ')', true);
return;
}
if (data.results != null && data.results.length > 0 && data.results[0].data != null && data.results[0].data.length > 0) {
var neo4jDataItmArray = data.results[0].data;
neo4jDataItmArray.forEach(function(dataItem) {
//Node
if (dataItem.graph.nodes != null && dataItem.graph.nodes.length > 0) {
var neo4jNodeItmArray = dataItem.graph.nodes;
neo4jNodeItmArray.forEach(function(nodeItm) {
if (!(nodeItm.id in nodeItemMap))
nodeItemMap[nodeItm.id] = nodeItm;
});
}
//Link
if (dataItem.graph.relationships != null && dataItem.graph.relationships.length > 0) {
var neo4jLinkItmArray = dataItem.graph.relationships;
neo4jLinkItmArray.forEach(function(linkItm) {
if (!(linkItm.id in linkItemMap)) {
linkItm.source = linkItm.startNode;
linkItm.target = linkItm.endNode;
linkItemMap[linkItm.id] = linkItm;
}
});
}
});
console.log('nodeItemMap.size:' + Object.keys(nodeItemMap).length);
console.log('linkItemMap.size:' + Object.keys(linkItemMap).length);
updateGraph();
return;
}
//also update graph when empty
updateGraph();
promptAlert($('#graphContainer'), 'No encontrado!', false);
}, 'json');
jqxhr.fail(function(data) {
promptAlert($('#graphContainer'), 'Error: se envió el texto de la consulta pero se recibió un mensaje de error (' + data + ')', true);
});
}
//Page Init
$(function() {
setupNeo4jLoginForAjax(neo4jLogin, neo4jPassword);
initGraph();
$('#queryText').keyup(function(e) {
if(e.which == 13) {
submitQuery();
}
});
$('#btnSend').click(function() {submitQuery()});
$('#chkboxCypherQry').change(function() {
if (this.checked)
$('#queryText').prop('placeholder', 'Cypher');
else
$('#queryText').prop('placeholder', 'Node Name');
});
});
</script>
</body>
</html>
So d3.js i am using to display a nodes diagram. right now parent start form left and children on right. is there any way to invert that direction so that children on left and parent on right.
Below is the function render tree which will display tree node. I call renderTree for example like vm.renderTree(vm.tree, "#tree-container");
vm.renderTree = function (treeData, treeId) {
var totalNodes = 0;
var maxLabelLength = 0;
var selectedNode = null;
var draggingNode = null;
var panSpeed = 200;
var panBoundary = 20;
var i = 0;
var duration = 750;
var root;
var viewerWidth = $(document).width();
var viewerHeight = $(document).height();
vm.tree = d3.layout.tree()
.size([viewerHeight, viewerWidth]);
var diagonal = d3.svg.diagonal()
.projection(function (d) {
return [d.y, d.x];
});
function visit(parent, visitFn, childrenFn) {
if (!parent)
return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
visit(treeData, function (d) {
totalNodes++;
if (typeof d.lename !== "undefined") {
if (treeId == "#tree-container-legalTree") {
maxLabelLength = Math.max(d.lename.length, maxLabelLength);
}
else {
maxLabelLength = Math.max(d.name.length, maxLabelLength);
}
}
}, function (d) {
return d.children && d.children.length > 0 ? d.children : null;
});
var sortTree = function () {
vm.tree.sort(function (a, b) {
if (typeof d !== "undefined") {
if (treeId == "#tree-container-legalTree") {
return b.lename.toLowerCase() < a.lename.toLowerCase() ? 1 : -1;
}
else {
return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
}
}
});
};
sortTree();
var pan = function (domNode, direction) {
var speed = panSpeed;
if (panTimer) {
clearTimeout(panTimer);
var translateCoords = d3.transform(svgGroup.attr("transform"));
if (direction == 'left' || direction == 'right') {
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
translateY = translateCoords.translate[1];
}
else if (direction == 'up' || direction == 'down') {
translateX = translateCoords.translate[0];
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
}
scaleX = translateCoords.scale[0];
scaleY = translateCoords.scale[1];
scale = zoomListener.scale();
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
zoomListener.scale(zoomListener.scale());
zoomListener.translate([translateX, translateY]);
panTimer = setTimeout(function () {
pan(domNode, speed, direction);
}, 50);
}
};
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
var initiateDrag = function (d, domNode) {
draggingNode = d;
d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none');
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show');
d3.select(domNode).attr('class', 'node activeDrag');
svgGroup.selectAll("g.node").sort(function (a, b) {
if (a.id != draggingNode.id)
return 1;
else
return -1;
});
if (nodes.length > 1) {
links = vm.tree.links(nodes);
nodePaths = svgGroup.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
}).remove();
nodesExit = svgGroup.selectAll("g.node")
.data(nodes, function (d) {
return d.id;
}).filter(function (d, i) {
if (d.id == draggingNode.id) {
return false;
}
return true;
}).remove();
}
parentLink = vm.tree.links(vm.tree.nodes(draggingNode.parent));
svgGroup.selectAll('path.link').filter(function (d, i) {
if (d.target.id == draggingNode.id) {
return true;
}
return false;
}).remove();
dragStarted = null;
};
var baseSvg = d3.select(treeId).append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
dragListener = d3.behavior.drag()
.on("dragstart", function (d) {
if (d == root) {
return;
}
dragStarted = true;
nodes = vm.tree.nodes(d);
d3.event.sourceEvent.stopPropagation();
})
.on("drag", function (d) {
if (d == root) {
return;
}
if (dragStarted) {
domNode = vm;
initiateDrag(d, domNode);
}
var relCoords = d3.mouse($('svg').get(0));
if (relCoords[0] < panBoundary) {
panTimer = true;
pan(vm, 'left');
}
else if (relCoords[0] > ($('svg').width() - panBoundary)) {
panTimer = true;
pan(vm, 'right');
}
else if (relCoords[1] < panBoundary) {
panTimer = true;
pan(vm, 'up');
}
else if (relCoords[1] > ($('svg').height() - panBoundary)) {
panTimer = true;
pan(vm, 'down');
}
else {
try {
clearTimeout(panTimer);
}
catch (e) {
}
}
d.x0 += d3.event.dy;
d.y0 += d3.event.dx;
var node = d3.select(vm);
node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")");
updateTempConnector();
}).on("dragend", function (d) {
if (d == root) {
return;
}
domNode = vm;
if (selectedNode) {
var index = draggingNode.parent.children.indexOf(draggingNode);
if (index > -1) {
draggingNode.parent.children.splice(index, 1);
}
if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
if (typeof selectedNode.children !== 'undefined') {
selectedNode.children.push(draggingNode);
}
else {
selectedNode._children.push(draggingNode);
}
}
else {
selectedNode.children = [];
selectedNode.children.push(draggingNode);
}
expand(selectedNode);
sortTree();
endDrag();
}
else {
endDrag();
}
});
function endDrag() {
selectedNode = null;
d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
d3.select(domNode).attr('class', 'node');
d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
updateTempConnector();
if (draggingNode !== null) {
update(root);
centerNode(draggingNode);
draggingNode = null;
}
}
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
function expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(expand);
d._children = null;
}
}
var overCircle = function (d) {
selectedNode = d;
updateTempConnector();
};
var outCircle = function (d) {
selectedNode = null;
updateTempConnector();
};
var updateTempConnector = function () {
var data = [];
if (draggingNode !== null && selectedNode !== null) {
data = [{
source: {
x: selectedNode.y0,
y: selectedNode.x0
},
target: {
x: draggingNode.y0,
y: draggingNode.x0
}
}];
}
var link = svgGroup.selectAll(".templink").data(data);
link.enter().append("path")
.attr("class", "templink")
.attr("d", d3.svg.diagonal())
.attr('pointer-events', 'none');
link.attr("d", d3.svg.diagonal());
link.exit().remove();
};
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = 150;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
function toggleChildren(d) {
if (d.children) {
d._children = d.children;
d.children = null;
}
else if (d._children) {
d.children = d._children;
d._children = null;
}
return d;
}
function click(d) {
if (d3.event.defaultPrevented)
return;
d = toggleChildren(d);
update(d);
centerNode(d);
}
var update = function (source) {
var levelWidth = [1];
var childCount = function (level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1)
levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function (d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newHeight = d3.max(levelWidth) * 25;
vm.tree = vm.tree.size([newHeight, viewerWidth]);
var nodes = vm.tree.nodes(root).reverse(), links = vm.tree.links(nodes);
nodes.forEach(function (d) {
d.y = (d.depth * (maxLabelLength * 10));
});
node = svgGroup.selectAll("g.node")
.data(nodes, function (d) {
return d.id || (d.id = ++i);
});
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
nodeEnter.append("circle")
.attr('class', 'nodeCircle')
.attr("r", 0)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr("x", function (d) {
return d.children || d._children ? -10 : 10;
})
.attr("dy", ".35em")
.attr('class', 'nodeText')
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
if (vm.showName == "LE Name") {
if (treeId == "#tree-container-legalTree") {
return d.lename;
}
return d.name;
}
})
.style("fill-opacity", 0);
nodeEnter.append("circle")
.attr('class', 'ghostCircle')
.attr("r", 30)
.attr("opacity", 0.2)
.style("fill", "red")
.attr('pointer-events', 'mouseover')
.on("mouseover", function (node) {
overCircle(node);
})
.on("mouseout", function (node) {
outCircle(node);
});
node.select('text')
.attr("x", function (d) {
return d.children || d._children ? -10 : 10;
})
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) {
if (vm.showName == "LE Name") {
if (treeId == "#tree-container-legalTree") {
return d.lename;
}
return d.name;
}
});
node.select("circle.nodeCircle")
.attr("r", 4.5)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
nodeUpdate.select("text")
.style("fill-opacity", 1);
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
var link = svgGroup.selectAll("path.link")
.data(links, function (d) {
return d.target.id;
});
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function (d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
link.transition()
.duration(duration)
.attr("d", diagonal);
link.exit().transition()
.duration(duration)
.attr("d", function (d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
};
var svgGroup = baseSvg.append("g");
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
update(root);
centerNode(root);
};
and below is example of treeData
{
"id": 1,
"code": "a",
"name": "b",
"type": "t",
"leId": 2,
"leName": "d",
"children": [
{
"id": 2,
"code": "e",
"name": "f",
"type": "g",
"leId": 4,
"lename": "e",
"childrenCount": 0
}
],
"childrenCount": 1
}
Here is a snippet of a tree with inverse direction.
Replace d.y with height - d.y, swap positionning of text (start / end) and modify call of diagonal for links:
var data = [
{ "name" : "Level 2: A", "parent":"Top Level" },
{ "name" : "Top Level", "parent":"null" },
{ "name" : "Son of A", "parent":"Level 2: A" },
{ "name" : "Daughter of A", "parent":"Level 2: A" },
{ "name" : "Level 2: B", "parent":"Top Level" }
];
// *********** Convert flat data into a nice tree ***************
// create a name: node map
var dataMap = data.reduce(function(map, node) {
map[node.name] = node;
return map;
}, {});
// create the tree array
var treeData = [];
data.forEach(function(node) {
// add to parent
var parent = dataMap[node.parent];
if (parent) {
// create child array if it doesn't exist
(parent.children || (parent.children = []))
// add node to child array
.push(node);
} else {
// parent is null or missing
treeData.push(node);
}
});
// ************** Generate the tree diagram *****************
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var i = 0;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
update(root);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Declare the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter the nodes.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + (height - d.y) + "," + d.x + ")"; });
nodeEnter.append("circle")
.attr("r", 10)
.style("fill", "#fff");
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? 13 : -13; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "start" : "end"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1);
// Declare the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter the links.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", d => {
console.log(d);
const source = {x: d.source.x, y: height - d.source.y};
const target = {x: d.target.x, y: height - d.target.y};
return diagonal({source, target});
//diagonal({x: d.x, y: height - d.y})
});
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text { font: 12px sans-serif; }
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.7/d3.min.js"></script>
I have a visual of a two ringed pie chart.
What I'm building is the following:
- if a sector's radians are too small for the specified text then the text is hidden. This is done and seems to work ok
- if text is hidden then a label should appear outside the pie. This is also done when the visual is initially rendered.
When the radio button is pressed the labels outside the pie should transition accordingly. I've attempted the transition and slowed it to 3500 and these labels are just slowly transitioning off the screen.
How do I fix this transition?
The snippet I've added to try to create the transition starts at line 241:
var arcs2 = svg.data([json]).selectAll(".arcG");
arcs2.data(partition.nodes)
.transition()
.duration(3500)
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle");
svg.selectAll(".theTxtsOuter")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
});
This is a plunk of the working(!) visual:
https://plnkr.co/edit/jYVPCL?p=preview
Here is the complete javascript used by the pie:
function pieChart(dataFile) {
var plot;
var vis;
var width = 400,
height = 400,
radius = Math.min(width, height) / 2.1,
color = d3.scale.ordinal()
.range(["#338ABA", "#016da9", "#4c96d5"])
.domain([0, 2]);
var labelr = radius + 5 // radius for label anchor
var div = d3.select("body")
.append("div")
.attr("class", "toolTip");
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.outerRadius(function(d) {
return (d.y + d.dy) / (radius);
})
.innerRadius(function(d) {
return d.y / (radius);
});
//check if the svg already exists
plot = d3.select("#svgPIEChart");
if (plot.empty()) {
vis = d3.select("#pieChart")
.append("svg")
.attr({
id: "svgPIEChart"
});
} else {
vis = d3.select("#svgPIEChart");
vis.selectAll("*").remove();
}
//group of the svg element
var svg = vis
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")"
});
//svg element
vis.attr({
//set the width and height of our visualization (these will be attributes of the <svg> tag
width: width,
height: height
});
d3.text(dataFile, function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
// it seems d3.layout.partition() can be either squares or arcs
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) {
return d.SalesRev;
});
var path = svg.data([json]).selectAll(".theArc")
.data(partition.nodes)
.enter()
.append("path")
.attr("class", "theArc")
.attr("id", function(d, i) {
return "theArc_" + i;
}) //Give each slice a unique ID
.attr("display", function(d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.style("stroke", "#fff")
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.attr("fill-rule", "evenodd")
.style("opacity", 0.01)
.style("stroke-opacity", 0.01)
.each(stash);
path.transition()
.duration(PIEOBJ.transTime)
.style("opacity", 1)
.style("stroke-opacity", 1)
path
.on("mouseout", mouseout)
.on("mousemove", function(d) {
div.style("left", d3.event.pageX + 10 + "px");
div.style("top", d3.event.pageY - 25 + "px");
div.style("display", "inline-block");
div.html(d.name + "<br>" + PIEOBJ.formatShrtInt(d.SalesRev));
})
var txts = svg.data([json]).selectAll(".theTxts")
.data(partition.nodes)
.enter()
.append("text");
txts
.attr("class", "theTxts")
.attr("dx", 10) //Move the text from the start angle of the arc
.attr("dy", 15) //Move the text down
.style("opacity", 0)
txts
.transition()
.duration(PIEOBJ.transTime)
.style("opacity", 1);
var txtPths = txts.append("textPath")
// .attr("xlink:href", function(d, i) {
.attr("href", function(d, i) {
return "#theArc_" + i;
})
.text(function(d) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return;
} else {
return d.name;
}
});
/* ------- TEXT LABELS OUTSIDE THE PIE-------*/
//var arcs = svg.selectAll(".theArc");
var arcs = svg.data([json]).selectAll(".arcG")
.data(partition.nodes)
.enter()
.append("g")
.attr("class", "arcG");
arcs.append("text")
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
console.log(c, h);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
})
.attr("class", "theTxtsOuter");
/* ----------------------------------------*/
d3.selectAll("input").on("change", function change() {
function createValueFunc(val) {
// currentMeasure = val;
return function(d) {
return d[val];
};
}
value = createValueFunc(this.value);
PIEOBJ.currentMeasure = this.value;
var path2 = svg.data([json]).selectAll(".theArc");
path2
.data(partition.value(value).nodes)
.transition()
.duration(1500)
.attrTween("d", arcTween)
.each("start", function() {
d3.select(this)
.on("mouseout", null) //CLEARING the listeners
.on("mousemove", null);
})
.each("end", function() {
d3.select(this)
.on("mouseout", mouseout) //attaching the listeners
.on("mousemove", function(d) {
div.style("left", d3.event.pageX + 10 + "px");
div.style("top", d3.event.pageY - 25 + "px");
div.style("display", "inline-block");
div.html(d.name + "<br>" + PIEOBJ.formatShrtInt(value(d)));
});
});
svg.selectAll("textPath")
.text(function(d) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return;
} else {
return d.name;
}
});
var arcs2 = svg.data([json]).selectAll(".arcG");
arcs2.data(partition.nodes)
.transition()
.duration(3500)
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle");
svg.selectAll(".theTxtsOuter")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
});
// the following deletes what was originally created and then recreates the text
// svg.selectAll("#titleX").remove();
});
function mouseout() {
div.style("display", "none"); //<< gets rid of the tooltip <<
}
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({
x: a.x0,
dx: a.dx0
}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
});
}
// // Take a 2-column CSV and transform it into a hierarchical structure suitable
// // for a partition layout.
function buildHierarchy(csv) {
var root = {
"name": "root",
"children": []
};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
// var APD = +csv[i][1];
var SalesRev = +csv[i][1];
var Amount = +csv[i][2];
if (isNaN(SalesRev)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode.children;
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k].name == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {
"name": nodeName,
"children": []
};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {
"name": nodeName,
// "APD": APD,
"SalesRev": SalesRev,
"Amount": Amount
};
children.push(childNode);
}
}
}
root.children.forEach(function(v) {
v.SalesRev = 0;
v.Amount = 0;
v.children.forEach(function(a) {
v.SalesRev += a.SalesRev;
v.Amount += a.Amount;
});
});
return root;
}
When you initially position them you are transforming the text elements. When you transition them you are positioning the outer g elements. These causes conflicting transforms. Use:
arcs2.data(partition.nodes)
.select('text') //<-- apply on child text
.transition()
.duration(3500)
.attr("transform", function(d) {
...
});
Updated plunker.
I've created a force-layout using D3 (see image below). However, for some reason it does not work in Firefox, whereas it works perfectly fine in Chrome. There's no errors in the Firefox debugger, but it only shows me a single line in the right side of the browser (as if the force layout never updates). I'm debugging it using a local server and browsing at http://localhost:8888/.
I've been looking at different posts regarding compatibility on stackoverflow, but I can't seem to find anything related to my code. If someone could give me a header on what to debug first, that would be great!
Edit: I've included links to the data as well as the csv-files in plain text at the bottom of my post. Data and code: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0
Here's the output from the Firefox console:
mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create d3.js:553:4
SyntaxError: An invalid or illegal string was specified d3.js:562:0
Chrome:
Firefox:
Index.html
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.legend {
font-size: 10px;
}
rect {
stroke-width: 2;
}
.node circle {
stroke: white;
stroke-width: 2px;
opacity: 1.0;
}
line {
stroke-width: 4px;
stroke-opacity: 1.0;
//stroke: "black";
}
body {
/* Scaling for different browsers */
-ms-transform: scale(1,1);
-webkit-transform: scale(1,1);
transform: scale(1,1);
}
svg{
position:absolute;
top:50%;
left:0px;
}
</style>
<body>
<script type="text/javascript" src="d3.js"></script>
<script type="text/javascript" src="papaparse.js"></script>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="networkview.js"></script>
</body>
networkview.js
var line_diff = 0.5; // increase from zero if you want space between the call/text lines
var mark_offset = 10; // how many percent of the mark lines in each end are not used for the relationship between incoming/outgoing?
var mark_size = 5; // size of the mark on the line
var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;
var text_links_data, call_links_data;
// colors for the different parts of the visualization
recordTypes.push({
text : "call",
color : "#438DCA"
});
recordTypes.push({
text : "text",
color : "#70C05A"
});
recordTypes.push({
text : "balance",
color : "#245A76"
});
// Function for grabbing a specific property from an array
pluck = function (ary, prop) {
return ary.map(function (x) {
return x[prop]
});
}
// Sums an array
sum = function (ary) {
return ary.reduce(function (a, b) {
return a + b
}, 0);
}
maxArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.max(a, b)
}, -Infinity);
}
minArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.min(a, b)
}, Infinity);
}
var data_links;
var data_nodes;
var results = Papa.parse("links.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_links = results.data;
dataLoaded();
}
});
var results = Papa.parse("nodes.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_nodes = results.data;
data_nodes.forEach(function (d, i) {
d.size = (i == 0)? 200 : 30
d.fill = (d.no_network_info == 1)? "#dfdfdf": "#a8a8a8"
});
dataLoaded();
}
});
function node_radius(d) {
return Math.pow(40.0 * ((d.index == 0) ? 200 : 30), 1 / 3);
}
function node_radius_data(d) {
return Math.pow(40.0 * d.size, 1 / 3);
}
function dataLoaded() {
if (typeof data_nodes === "undefined" || typeof data_links === "undefined") {
//console.log("Still loading")
} else {
CreateVisualizationFromData();
}
}
function isConnectedToOtherThanMain(a) {
var connected = false;
for (i = 1; i < data_nodes.length; i++) {
if (isConnected(a, data_nodes[i]) && a.index != i) {
connected = true;
}
}
return connected;
}
function isConnected(a, b) {
return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
}
function isConnectedAsSource(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function isConnectedAsTarget(a, b) {
return linkedByIndex[b.index + "," + a.index];
}
function isEqual(a, b) {
return a.index == b.index;
}
function tick() {
if (call_links_data.length > 0) {
callLink
.attr("x1", function (d) {
return d.source.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 0)[0];
})
.attr("y1", function (d) {
return d.source.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 0)[1];
})
.attr("x2", function (d) {
return d.target.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 1)[0];
})
.attr("y2", function (d) {
return d.target.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 1)[1];
});
callLink.each(function (d) {
applyGradient(this, "call", d)
});
}
if (text_links_data.length > 0) {
textLink
.attr("x1", function (d) {
return d.source.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 0)[0];
})
.attr("y1", function (d) {
return d.source.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 0)[1];
})
.attr("x2", function (d) {
return d.target.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 1)[0];
})
.attr("y2", function (d) {
return d.target.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 1)[1];
});
textLink.each(function (d) {
applyGradient(this, "text", d)
});
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
if (force.alpha() < 0.05)
drawLegend();
}
function getRandomInt() {
return Math.floor(Math.random() * (100000 - 0));
}
function applyGradient(line, interaction_type, d) {
var self = d3.select(line);
var current_gradient = self.style("stroke")
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
var new_gradient_id = "line-gradient" + getRandomInt();
var from = d.source.size < d.target.size ? d.source : d.target;
var to = d.source.size < d.target.size ? d.target : d.source;
var mid_offset = 0;
var standardColor = "";
if (interaction_type == "call") {
mid_offset = d.inc_calls / (d.inc_calls + d.out_calls);
standardColor = "#438DCA";
} else {
mid_offset = d.inc_texts / (d.inc_texts + d.out_texts);
standardColor = "#70C05A";
}
/* recordTypes_ID = pluck(recordTypes, 'text');
whichRecordType = recordTypes_ID.indexOf(interaction_type);
standardColor = recordTypes[whichRecordType].color;
*/
mid_offset = mid_offset * 100;
mid_offset = mid_offset * 0.6 + 20; // scale so it doesn't hit the ends
lineLengthCalculation = function (x, y, x0, y0) {
return Math.sqrt((x -= x0) * x + (y -= y0) * y);
};
lineLength = lineLengthCalculation(from.px, from.py, to.px, to.py);
if (lineLength >= 0.1) {
mark_size_percent = (mark_size / lineLength) * 100;
defs.append("linearGradient")
.attr("id", new_gradient_id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", from.px)
.attr("y1", from.py)
.attr("x2", to.px)
.attr("y2", to.py)
.selectAll("stop")
.data([{
offset : "0%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : "#245A76",
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : "#245A76",
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : "100%",
color : standardColor,
opacity : "1"
}
])
.enter().append("stop")
.attr("offset", function (d) {
return d.offset;
})
.attr("stop-color", function (d) {
return d.color;
})
.attr("stop-opacity", function (d) {
return d.opacity;
});
self.style("stroke", "url(#" + new_gradient_id + ")")
defs.select(current_gradient).remove();
}
}
var linkedByIndex;
var width = $(window).width();
var height = $(window).height();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force;
var callLink;
var textLink;
var link;
var node;
var defs;
var total_interactions = 0;
var max_interactions = 0;
function CreateVisualizationFromData() {
for (i = 0; i < data_links.length; i++) {
total_interactions += data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts;
max_interactions = Math.max(max_interactions, data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts)
}
linkedByIndex = {};
data_links.forEach(function (d) {
linkedByIndex[d.source + "," + d.target] = true;
//linkedByIndex[d.source.index + "," + d.target.index] = true;
});
//console.log(total_interactions);
//console.log(max_interactions);
function chargeForNode(d, i) {
// main node
if (i == 0) {
return -25000;
}
// contains other links
else if (isConnectedToOtherThanMain(d)) {
return -2000;
} else {
return -1200;
}
}
// initial placement of nodes prevents overlaps
central_x = width / 2
central_y = height / 2
data_nodes.forEach(function(d, i) {
if (i != 0) {
connected = isConnectedToOtherThanMain(d);
data_nodes[i].x = connected? central_x + 10000: central_x -10000;
data_nodes[i].y = connected? central_y: central_y;
}
else {data_nodes[i].x = central_x; data_nodes[i].y = central_y;}})
force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.charge(function (d, i) {
return chargeForNode(d, i)
})
.friction(0.6) // 0.6
.gravity(0.4) // 0.6
.size([width, height])
.start();
call_links_data = data_links.filter(function(d) {
return (d.inc_calls + d.out_calls > 0)});
text_links_data = data_links.filter(function(d) {
return (d.inc_texts + d.out_texts > 0)});
callLink = svg.selectAll(".call-line")
.data(call_links_data)
.enter().append("line");
textLink = svg.selectAll(".text-line")
.data(text_links_data)
.enter().append("line");
link = svg.selectAll("line");
node = svg.selectAll(".node")
.data(data_nodes)
.enter().append("g")
.attr("class", "node");
defs = svg.append("defs");
node
.append("circle")
.attr("r", node_radius)
.style("fill", function (d) {
return (d.index == 0)? "#ffffff" : d.fill;
})
.style("stroke", function (d) {
return (d.index == 0)? "#8C8C8C" : "#ffffff";
})
svg
.append("marker")
.attr("id", "arrowhead")
.attr("refX", 6 + 7)
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0,0 V 4 L6,2 Z");
if (text_links_data.length > 0) {
textLink
.style("stroke-width", function stroke(d) {
return text_width(d)
})
.each(function (d) {
applyGradient(this, "text", d)
});
}
if (call_links_data.length > 0) {
callLink
.style("stroke-width", function stroke(d) {
return call_width(d)
})
.each(function (d) {
applyGradient(this, "call", d)
});
}
force
.on("tick", tick);
}
function drawLegend() {
var node_px = pluck(data_nodes, 'px');
var node_py = pluck(data_nodes, 'py');
var nodeLayoutRight = Math.max(maxArray(node_px));
var nodeLayoutBottom = Math.max(maxArray(node_py));
legend = svg.selectAll('.legend')
.data(recordTypes)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var rect_height = legendRectSize + legendSpacing;
var offset = rect_height * (recordTypes.length-1);
var horz = nodeLayoutRight + 15; /* - 2*legendRectSize; */
var vert = nodeLayoutBottom + (i * rect_height) - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', function (d) {
return d.color
})
.style('stroke', function (d) {
return d.color
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing + 3)
.text(function (d) {
return d.text;
})
.style('fill', '#757575');
}
var line_width_factor = 10.0 // width for the widest line
function call_width(d) {
return (d.inc_calls + d.out_calls) / max_interactions * line_width_factor;
}
function text_width(d) {
return (d.inc_texts + d.out_texts) / max_interactions * line_width_factor;
}
function total_width(d) {
return (d.inc_calls + d.out_calls + d.inc_texts + d.out_texts) / max_interactions * line_width_factor + line_diff;
}
function line_perpendicular_shift(d, direction) {
theta = getAngle(d);
theta_perpendicular = theta + (Math.PI / 2) * direction;
lineWidthOfOppositeLine = direction == 1 ? text_width(d) : call_width(d);
shift = lineWidthOfOppositeLine / 2;
delta_x = (shift + line_diff) * Math.cos(theta_perpendicular)
delta_y = (shift + line_diff) * Math.sin(theta_perpendicular)
return [delta_x, delta_y]
}
function line_radius_shift_to_edge(d, which_node) { // which_node = 0 if source, = 1 if target
theta = getAngle(d);
theta = (which_node == 0) ? theta : theta + Math.PI; // reverse angle if target node
radius = (which_node == 0) ? node_radius(d.source) : node_radius(d.target) // d.source and d.target refer directly to the nodes (not indices)
radius -= 2; // add stroke width
delta_x = radius * Math.cos(theta)
delta_y = radius * Math.sin(theta)
return [delta_x, delta_y]
}
function getAngle(d) {
rel_x = d.target.x - d.source.x;
rel_y = d.target.y - d.source.y;
return theta = Math.atan2(rel_y, rel_x);
}
Links.csv
source,target,inc_calls,out_calls,inc_texts,out_texts
0,1,1.0,0.0,1.0,0.0
0,2,0.0,0.0,1.0,3.0
0,3,3.0,9.0,5.0,7.0
0,4,2.0,12.0,9.0,14.0
0,5,5.0,9.0,9.0,13.0
0,6,5.0,17.0,2.0,25.0
0,7,6.0,13.0,7.0,16.0
0,8,7.0,7.0,8.0,8.0
0,9,3.0,10.0,8.0,20.0
0,10,5.0,10.0,6.0,23.0
0,11,8.0,10.0,13.0,15.0
0,12,9.0,18.0,9.0,22.0
0,13,1.0,2.0,2.0,2.0
0,14,11.0,13.0,7.0,15.0
0,15,5.0,18.0,9.0,22.0
0,16,8.0,15.0,13.0,20.0
0,17,4.0,10.0,9.0,26.0
0,18,9.0,18.0,8.0,33.0
0,19,12.0,11.0,4.0,15.0
0,20,4.0,15.0,9.0,25.0
0,21,4.0,17.0,10.0,19.0
0,22,4.0,16.0,12.0,29.0
0,23,6.0,9.0,12.0,20.0
0,24,2.0,2.0,1.0,3.0
0,25,3.0,8.0,10.0,16.0
0,26,3.0,10.0,11.0,22.0
0,27,6.0,14.0,9.0,11.0
0,28,2.0,7.0,8.0,15.0
0,29,2.0,11.0,8.0,15.0
0,30,1.0,8.0,9.0,6.0
0,31,3.0,6.0,7.0,7.0
0,32,4.0,9.0,3.0,12.0
0,33,4.0,4.0,7.0,12.0
0,34,4.0,4.0,5.0,9.0
0,35,2.0,3.0,0.0,7.0
0,36,3.0,7.0,5.0,9.0
0,37,1.0,7.0,5.0,3.0
0,38,1.0,13.0,1.0,2.0
0,39,2.0,7.0,3.0,4.0
0,40,1.0,3.0,2.0,6.0
0,41,0.0,1.0,2.0,1.0
0,42,0.0,0.0,2.0,0.0
0,43,0.0,3.0,1.0,5.0
0,44,0.0,1.0,0.0,2.0
0,45,4.0,1.0,1.0,10.0
0,46,2.0,7.0,3.0,5.0
0,47,5.0,7.0,3.0,5.0
0,48,2.0,5.0,4.0,10.0
0,49,3.0,3.0,5.0,13.0
1,15,10.0,30.0,13.0,37.0
2,8,16.0,9.0,24.0,15.0
2,43,4.0,10.0,9.0,16.0
5,48,3.0,5.0,0.0,4.0
6,37,11.0,25.0,15.0,34.0
8,48,12.0,4.0,7.0,2.0
9,42,25.0,9.0,29.0,15.0
9,45,11.0,3.0,16.0,5.0
12,24,4.0,15.0,13.0,16.0
14,31,18.0,9.0,29.0,12.0
14,33,5.0,10.0,4.0,9.0
15,28,8.0,5.0,16.0,5.0
16,36,14.0,11.0,10.0,19.0
23,38,3.0,11.0,6.0,10.0
26,42,9.0,23.0,17.0,21.0
27,46,12.0,12.0,15.0,21.0
29,39,8.0,15.0,9.0,20.0
29,47,8.0,27.0,19.0,24.0
33,46,6.0,4.0,13.0,13.0
37,43,10.0,12.0,6.0,21.0
Nodes.csv
no_network_info
0
0
0
1
1
0
0
0
0
0
0
1
0
1
0
0
0
1
0
1
1
0
0
0
0
1
0
0
0
0
1
0
1
0
1
1
0
0
0
0
1
1
0
0
1
0
0
0
0
0
The syntax error is with this line:
defs.select(current_gradient).remove();
But above that is the real problem. Replace:
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
With:
if (current_gradient.match("http")) {
var parts = current_gradient.split("/");
current_gradient = parts[-1];
} else {
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
}
When you originally set the current_gradient in Chrome, it is set to "url(somevalue)" whereas in Firefox is it set to "url(fullpath/somevalue)". So you need to remove all the path info, not just the "url()" bit. Splitting the slash and taking the last value from the split is probably the easiest way to do that.
I've created a force-layout using D3 (see image below). However, it runs very slowly in Firefox, whereas it works perfectly fine in Chrome. I'm debugging it using a local server and browsing at http://localhost:8888/. It's might be due to the following message in the Firefox console, but accordingly to the comments that's unlikely. Can someone pinpoint the performance issue and give me a hint on how to resolve it?
mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create
Data and code in zip: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0
Visualization:
Index.html
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.legend {
font-size: 10px;
}
rect {
stroke-width: 2;
}
.node circle {
stroke: white;
stroke-width: 2px;
opacity: 1.0;
}
line {
stroke-width: 4px;
stroke-opacity: 1.0;
//stroke: "black";
}
body {
/* Scaling for different browsers */
-ms-transform: scale(1,1);
-webkit-transform: scale(1,1);
transform: scale(1,1);
}
svg{
position:absolute;
top:50%;
left:0px;
}
</style>
<body>
<script type="text/javascript" src="d3.js"></script>
<script type="text/javascript" src="papaparse.js"></script>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="networkview.js"></script>
</body>
networkview.js
var line_diff = 0.5; // increase from zero if you want space between the call/text lines
var mark_offset = 10; // how many percent of the mark lines in each end are not used for the relationship between incoming/outgoing?
var mark_size = 5; // size of the mark on the line
var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;
var text_links_data, call_links_data;
// colors for the different parts of the visualization
recordTypes.push({
text : "call",
color : "#438DCA"
});
recordTypes.push({
text : "text",
color : "#70C05A"
});
recordTypes.push({
text : "balance",
color : "#245A76"
});
// Function for grabbing a specific property from an array
pluck = function (ary, prop) {
return ary.map(function (x) {
return x[prop]
});
}
// Sums an array
sum = function (ary) {
return ary.reduce(function (a, b) {
return a + b
}, 0);
}
maxArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.max(a, b)
}, -Infinity);
}
minArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.min(a, b)
}, Infinity);
}
var data_links;
var data_nodes;
var results = Papa.parse("links.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_links = results.data;
dataLoaded();
}
});
var results = Papa.parse("nodes.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_nodes = results.data;
data_nodes.forEach(function (d, i) {
d.size = (i == 0)? 200 : 30
d.fill = (d.no_network_info == 1)? "#dfdfdf": "#a8a8a8"
});
dataLoaded();
}
});
function node_radius(d) {
return Math.pow(40.0 * ((d.index == 0) ? 200 : 30), 1 / 3);
}
function node_radius_data(d) {
return Math.pow(40.0 * d.size, 1 / 3);
}
function dataLoaded() {
if (typeof data_nodes === "undefined" || typeof data_links === "undefined") {
//console.log("Still loading")
} else {
CreateVisualizationFromData();
}
}
function isConnectedToOtherThanMain(a) {
var connected = false;
for (i = 1; i < data_nodes.length; i++) {
if (isConnected(a, data_nodes[i]) && a.index != i) {
connected = true;
}
}
return connected;
}
function isConnected(a, b) {
return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
}
function isConnectedAsSource(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function isConnectedAsTarget(a, b) {
return linkedByIndex[b.index + "," + a.index];
}
function isEqual(a, b) {
return a.index == b.index;
}
function tick() {
if (call_links_data.length > 0) {
callLink
.attr("x1", function (d) {
return d.source.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 0)[0];
})
.attr("y1", function (d) {
return d.source.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 0)[1];
})
.attr("x2", function (d) {
return d.target.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 1)[0];
})
.attr("y2", function (d) {
return d.target.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 1)[1];
});
callLink.each(function (d) {
applyGradient(this, "call", d)
});
}
if (text_links_data.length > 0) {
textLink
.attr("x1", function (d) {
return d.source.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 0)[0];
})
.attr("y1", function (d) {
return d.source.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 0)[1];
})
.attr("x2", function (d) {
return d.target.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 1)[0];
})
.attr("y2", function (d) {
return d.target.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 1)[1];
});
textLink.each(function (d) {
applyGradient(this, "text", d)
});
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
if (force.alpha() < 0.05)
drawLegend();
}
function getRandomInt() {
return Math.floor(Math.random() * (100000 - 0));
}
function applyGradient(line, interaction_type, d) {
var self = d3.select(line);
var current_gradient = self.style("stroke")
//current_gradient = current_gradient.substring(4, current_gradient.length - 1);
if (current_gradient.match("http")) {
var parts = current_gradient.split("/");
current_gradient = parts[-1];
} else {
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
}
var new_gradient_id = "line-gradient" + getRandomInt();
var from = d.source.size < d.target.size ? d.source : d.target;
var to = d.source.size < d.target.size ? d.target : d.source;
var mid_offset = 0;
var standardColor = "";
if (interaction_type == "call") {
mid_offset = d.inc_calls / (d.inc_calls + d.out_calls);
standardColor = "#438DCA";
} else {
mid_offset = d.inc_texts / (d.inc_texts + d.out_texts);
standardColor = "#70C05A";
}
/* recordTypes_ID = pluck(recordTypes, 'text');
whichRecordType = recordTypes_ID.indexOf(interaction_type);
standardColor = recordTypes[whichRecordType].color;
*/
mid_offset = mid_offset * 100;
mid_offset = mid_offset * 0.6 + 20; // scale so it doesn't hit the ends
lineLengthCalculation = function (x, y, x0, y0) {
return Math.sqrt((x -= x0) * x + (y -= y0) * y);
};
lineLength = lineLengthCalculation(from.px, from.py, to.px, to.py);
if (lineLength >= 0.1) {
mark_size_percent = (mark_size / lineLength) * 100;
defs.append("linearGradient")
.attr("id", new_gradient_id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", from.px)
.attr("y1", from.py)
.attr("x2", to.px)
.attr("y2", to.py)
.selectAll("stop")
.data([{
offset : "0%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
color : "#245A76",
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : "#245A76",
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
color : standardColor,
opacity : "1"
}, {
offset : "100%",
color : standardColor,
opacity : "1"
}
])
.enter().append("stop")
.attr("offset", function (d) {
return d.offset;
})
.attr("stop-color", function (d) {
return d.color;
})
.attr("stop-opacity", function (d) {
return d.opacity;
});
self.style("stroke", "url(#" + new_gradient_id + ")")
defs.select(current_gradient).remove();
}
}
var linkedByIndex;
var width = $(window).width();
var height = $(window).height();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force;
var callLink;
var textLink;
var link;
var node;
var defs;
var total_interactions = 0;
var max_interactions = 0;
function CreateVisualizationFromData() {
for (i = 0; i < data_links.length; i++) {
total_interactions += data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts;
max_interactions = Math.max(max_interactions, data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts)
}
linkedByIndex = {};
data_links.forEach(function (d) {
linkedByIndex[d.source + "," + d.target] = true;
//linkedByIndex[d.source.index + "," + d.target.index] = true;
});
//console.log(total_interactions);
//console.log(max_interactions);
function chargeForNode(d, i) {
// main node
if (i == 0) {
return -25000;
}
// contains other links
else if (isConnectedToOtherThanMain(d)) {
return -2000;
} else {
return -1200;
}
}
// initial placement of nodes prevents overlaps
central_x = width / 2
central_y = height / 2
data_nodes.forEach(function(d, i) {
if (i != 0) {
connected = isConnectedToOtherThanMain(d);
data_nodes[i].x = connected? central_x + 10000: central_x -10000;
data_nodes[i].y = connected? central_y: central_y;
}
else {data_nodes[i].x = central_x; data_nodes[i].y = central_y;}})
force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.charge(function (d, i) {
return chargeForNode(d, i)
})
.friction(0.6) // 0.6
.gravity(0.4) // 0.6
.size([width, height])
.start();
call_links_data = data_links.filter(function(d) {
return (d.inc_calls + d.out_calls > 0)});
text_links_data = data_links.filter(function(d) {
return (d.inc_texts + d.out_texts > 0)});
callLink = svg.selectAll(".call-line")
.data(call_links_data)
.enter().append("line");
textLink = svg.selectAll(".text-line")
.data(text_links_data)
.enter().append("line");
link = svg.selectAll("line");
node = svg.selectAll(".node")
.data(data_nodes)
.enter().append("g")
.attr("class", "node");
defs = svg.append("defs");
node
.append("circle")
.attr("r", node_radius)
.style("fill", function (d) {
return (d.index == 0)? "#ffffff" : d.fill;
})
.style("stroke", function (d) {
return (d.index == 0)? "#8C8C8C" : "#ffffff";
})
svg
.append("marker")
.attr("id", "arrowhead")
.attr("refX", 6 + 7)
.attr("refY", 2)
.attr("markerWidth", 6)
.attr("markerHeight", 4)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0,0 V 4 L6,2 Z");
if (text_links_data.length > 0) {
textLink
.style("stroke-width", function stroke(d) {
return text_width(d)
})
.each(function (d) {
applyGradient(this, "text", d)
});
}
if (call_links_data.length > 0) {
callLink
.style("stroke-width", function stroke(d) {
return call_width(d)
})
.each(function (d) {
applyGradient(this, "call", d)
});
}
force
.on("tick", tick);
}
function drawLegend() {
var node_px = pluck(data_nodes, 'px');
var node_py = pluck(data_nodes, 'py');
var nodeLayoutRight = Math.max(maxArray(node_px));
var nodeLayoutBottom = Math.max(maxArray(node_py));
legend = svg.selectAll('.legend')
.data(recordTypes)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var rect_height = legendRectSize + legendSpacing;
var offset = rect_height * (recordTypes.length-1);
var horz = nodeLayoutRight + 15; /* - 2*legendRectSize; */
var vert = nodeLayoutBottom + (i * rect_height) - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', function (d) {
return d.color
})
.style('stroke', function (d) {
return d.color
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing + 3)
.text(function (d) {
return d.text;
})
.style('fill', '#757575');
}
var line_width_factor = 10.0 // width for the widest line
function call_width(d) {
return (d.inc_calls + d.out_calls) / max_interactions * line_width_factor;
}
function text_width(d) {
return (d.inc_texts + d.out_texts) / max_interactions * line_width_factor;
}
function total_width(d) {
return (d.inc_calls + d.out_calls + d.inc_texts + d.out_texts) / max_interactions * line_width_factor + line_diff;
}
function line_perpendicular_shift(d, direction) {
theta = getAngle(d);
theta_perpendicular = theta + (Math.PI / 2) * direction;
lineWidthOfOppositeLine = direction == 1 ? text_width(d) : call_width(d);
shift = lineWidthOfOppositeLine / 2;
delta_x = (shift + line_diff) * Math.cos(theta_perpendicular)
delta_y = (shift + line_diff) * Math.sin(theta_perpendicular)
return [delta_x, delta_y]
}
function line_radius_shift_to_edge(d, which_node) { // which_node = 0 if source, = 1 if target
theta = getAngle(d);
theta = (which_node == 0) ? theta : theta + Math.PI; // reverse angle if target node
radius = (which_node == 0) ? node_radius(d.source) : node_radius(d.target) // d.source and d.target refer directly to the nodes (not indices)
radius -= 2; // add stroke width
delta_x = radius * Math.cos(theta)
delta_y = radius * Math.sin(theta)
return [delta_x, delta_y]
}
function getAngle(d) {
rel_x = d.target.x - d.source.x;
rel_y = d.target.y - d.source.y;
return theta = Math.atan2(rel_y, rel_x);
}
Links.csv
source,target,inc_calls,out_calls,inc_texts,out_texts
0,1,1.0,0.0,1.0,0.0
0,2,0.0,0.0,1.0,3.0
0,3,3.0,9.0,5.0,7.0
0,4,2.0,12.0,9.0,14.0
0,5,5.0,9.0,9.0,13.0
0,6,5.0,17.0,2.0,25.0
0,7,6.0,13.0,7.0,16.0
0,8,7.0,7.0,8.0,8.0
0,9,3.0,10.0,8.0,20.0
0,10,5.0,10.0,6.0,23.0
0,11,8.0,10.0,13.0,15.0
0,12,9.0,18.0,9.0,22.0
0,13,1.0,2.0,2.0,2.0
0,14,11.0,13.0,7.0,15.0
0,15,5.0,18.0,9.0,22.0
0,16,8.0,15.0,13.0,20.0
0,17,4.0,10.0,9.0,26.0
0,18,9.0,18.0,8.0,33.0
0,19,12.0,11.0,4.0,15.0
0,20,4.0,15.0,9.0,25.0
0,21,4.0,17.0,10.0,19.0
0,22,4.0,16.0,12.0,29.0
0,23,6.0,9.0,12.0,20.0
0,24,2.0,2.0,1.0,3.0
0,25,3.0,8.0,10.0,16.0
0,26,3.0,10.0,11.0,22.0
0,27,6.0,14.0,9.0,11.0
0,28,2.0,7.0,8.0,15.0
0,29,2.0,11.0,8.0,15.0
0,30,1.0,8.0,9.0,6.0
0,31,3.0,6.0,7.0,7.0
0,32,4.0,9.0,3.0,12.0
0,33,4.0,4.0,7.0,12.0
0,34,4.0,4.0,5.0,9.0
0,35,2.0,3.0,0.0,7.0
0,36,3.0,7.0,5.0,9.0
0,37,1.0,7.0,5.0,3.0
0,38,1.0,13.0,1.0,2.0
0,39,2.0,7.0,3.0,4.0
0,40,1.0,3.0,2.0,6.0
0,41,0.0,1.0,2.0,1.0
0,42,0.0,0.0,2.0,0.0
0,43,0.0,3.0,1.0,5.0
0,44,0.0,1.0,0.0,2.0
0,45,4.0,1.0,1.0,10.0
0,46,2.0,7.0,3.0,5.0
0,47,5.0,7.0,3.0,5.0
0,48,2.0,5.0,4.0,10.0
0,49,3.0,3.0,5.0,13.0
1,15,10.0,30.0,13.0,37.0
2,8,16.0,9.0,24.0,15.0
2,43,4.0,10.0,9.0,16.0
5,48,3.0,5.0,0.0,4.0
6,37,11.0,25.0,15.0,34.0
8,48,12.0,4.0,7.0,2.0
9,42,25.0,9.0,29.0,15.0
9,45,11.0,3.0,16.0,5.0
12,24,4.0,15.0,13.0,16.0
14,31,18.0,9.0,29.0,12.0
14,33,5.0,10.0,4.0,9.0
15,28,8.0,5.0,16.0,5.0
16,36,14.0,11.0,10.0,19.0
23,38,3.0,11.0,6.0,10.0
26,42,9.0,23.0,17.0,21.0
27,46,12.0,12.0,15.0,21.0
29,39,8.0,15.0,9.0,20.0
29,47,8.0,27.0,19.0,24.0
33,46,6.0,4.0,13.0,13.0
37,43,10.0,12.0,6.0,21.0
Nodes.csv
no_network_info
0
0
0
1
1
0
0
0
0
0
0
1
0
1
0
0
0
1
0
1
1
0
0
0
0
1
0
0
0
0
1
0
1
0
1
1
0
0
0
0
1
1
0
0
1
0
0
0
0
0
EDIT
The root cause of the problem was document bloat caused by failing to remove outdated linearGradient tags in the defs section of the
HTML. This was only happening in Firefox because of what it returns
in response to getPropertyValue in it's CSSStyleDeclaration
interface (which is called by d3 in selection.style()). The value
returned is of the form
"url("http://localhost:88888/index.html#line-gradientXXXXXX")
transparent", compared to "url(#line-gradientXXXXXX)" in the other
browsers. Since the id was not properly extracted by the OP,
linearGradient tags ear-marked for deletion were not found and not
deleted, causing them to grow in number. The problem is avoided by
using unique indexing, already available in the data, to label the
linearGradient tags.
As per my comments above, I managed to solve the Firefox problem by making the following changes:
Eliminate redundant calculations in the forEach sections in tick and applyGradient.
Using well-formed d3 to manage the defs. It was probably fine how it was, it just took me a while to realise how it was done but, I changed it to standard d3 patterns which will manage updating and changing data properly. This line is particularly sensitive...
var new_gradient_id = "line-gradient" + getRandomInt();
this works better...
var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
Applied standard d3 patterns to managing the callLink and textLink sections in CreateVisualizationFromData. Using these patterns it updates properly and manages changing data.
After making these changes, the speed problems in Firefox disapeared and it is now the same in all three major browsers in terms of speed. It looks better in Chrome though. Some experimenting would be in order to determine exactly which changes are critical, but there was definitely a problem with deleting the linearGradient tags. These were not being properly deleted in FF and massively bloating the DOM. I think this is probably what was causing the problem.
The other changes I made were just stylistic to make it easier for me to understand.
Amended code:
HTML
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/*div {
outline: 1px solid black;*/
}
.legend {
font-size: 10px;
}
rect {
stroke-width: 2;
}
.node circle {
stroke: white;
stroke-width: 2px;
opacity: 1.0;
}
line {
stroke-width: 4px;
stroke-opacity: 1.0;
//stroke: "black";
}
body {
/* Scaling for different browsers */
-ms-transform: scale(1,1);
-webkit-transform: scale(1,1);
transform: scale(1,1);
}
svg{
position:absolute;
top:50%;
left:0px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div style="margin: 50px 0 10px 50px; display: inline-block">click to start/stop</div>
<!--<script src="d3/d3 CB.js"></script>-->
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="papaparse.js"></script>
<script type="text/javascript" src="networkview CB.js"></script>
</body>
JS
var line_diff = 0.5; // increase from zero if you want space between the call/text lines
var mark_offset = 10; // how many percent of the mark lines in each end are not used for the relationship between incoming/outgoing?
var mark_size = 5; // size of the mark on the line
var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;
var text_links_data, call_links_data;
// colors for the different parts of the visualization
recordTypes.push({
text : "call",
color : "#438DCA"
});
recordTypes.push({
text : "text",
color : "#70C05A"
});
recordTypes.push({
text : "balance",
color : "#245A76"
});
// Function for grabbing a specific property from an array
pluck = function (ary, prop) {
return ary.map(function (x) {
return x[prop]
});
}
// Sums an array
sum = function (ary) {
return ary.reduce(function (a, b) {
return a + b
}, 0);
}
maxArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.max(a, b)
}, -Infinity);
}
minArray = function (ary) {
return ary.reduce(function (a, b) {
return Math.min(a, b)
}, Infinity);
}
var data_links;
var data_nodes;
var results = Papa.parse("links.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_links = results.data;
for (i = 0; i < data_links.length; i++) {
total_interactions += data_links[i].inc_calls
+ data_links[i].out_calls
+ data_links[i].inc_texts
+ data_links[i].out_texts;
max_interactions = Math.max(max_interactions,
data_links[i].inc_calls
+ data_links[i].out_calls
+ data_links[i].inc_texts
+ data_links[i].out_texts)
}
//console.log(total_interactions);
//console.log(max_interactions);
linkedByIndex = {};
data_links.forEach(function (d) {
linkedByIndex[d.source + "," + d.target] = true;
//linkedByIndex[d.source.index + "," + d.target.index] = true;
});
dataLoaded();
}
});
var results = Papa.parse("nodes.csv", {
header : true,
download : true,
dynamicTyping : true,
delimiter : ",",
skipEmptyLines : true,
complete : function (results) {
data_nodes = results.data;
data_nodes.forEach(function (d, i) {
d.size = (i == 0)? 200 : 30
d.fill = (d.no_network_info == 1)? "#dfdfdf": "#a8a8a8"
});
dataLoaded();
}
});
function node_radius(d) {
return Math.pow(40.0 * ((d.index == 0) ? 200 : 30), 1 / 3);
}
function node_radius_data(d) {
return Math.pow(40.0 * d.size, 1 / 3);
}
function dataLoaded() {
if (typeof data_nodes === "undefined" || typeof data_links === "undefined") {
console.log("Still loading " + (typeof data_nodes === "undefined" ? 'data_links' : 'data_nodes'))
} else {
CreateVisualizationFromData();
}
}
function isConnectedToOtherThanMain(a) {
var connected = false;
for (i = 1; i < data_nodes.length; i++) {
if (isConnected(a, data_nodes[i]) && a.index != i) {
connected = true;
}
}
return connected;
}
function isConnected(a, b) {
return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
}
function isConnectedAsSource(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function isConnectedAsTarget(a, b) {
return linkedByIndex[b.index + "," + a.index];
}
function isEqual(a, b) {
return a.index == b.index;
}
var log = d3.select('body').append('div').attr('id', 'log').style({margin: '50px 0 10px 3px', display: 'inline-block'});
log.update = function (alpha) {
this.text('alpha: ' + d3.format(".3f")(alpha))
}
function tick(e) {
log.update(e.alpha)
if (call_links_data.length > 0) {
callLink
//CB eliminate redundant calculations
.each(function (d) {
d.lpf1 = line_perpendicular_shift(d, 1)
d.lrste = []
d.lrste.push(line_radius_shift_to_edge(d, 0))
d.lrste.push(line_radius_shift_to_edge(d, 1))
})
//CB
.attr("x1", function (d) {
return d.source.x - d.lpf1[0] + d.lrste[0][0];
})
.attr("y1", function (d) {
return d.source.y - d.lpf1[1] + d.lrste[0][1];
})
.attr("x2", function (d) {
return d.target.x - d.lpf1[0] + d.lrste[1][0];
})
.attr("y2", function (d) {
return d.target.y - d.lpf1[1] + d.lrste[1][1];
});
callLink.each(function (d, i) {
applyGradient(this, "call", d, i)
});
}
if (text_links_data.length > 0) {
textLink
//CB
.each(function (d) {
d.lpfNeg1 = line_perpendicular_shift(d, -1);
d.lrste = [];
d.lrste.push(line_radius_shift_to_edge(d, 0));
d.lrste.push(line_radius_shift_to_edge(d, 1));
})
//CB
.attr("x1", function (d) {
return d.source.x - d.lpfNeg1[0] + d.lrste[0][0];
})
.attr("y1", function (d) {
return d.source.y - d.lpfNeg1[1] + d.lrste[0][1];
})
.attr("x2", function (d) {
return d.target.x - d.lpfNeg1[0] + d.lrste[1][0];
})
.attr("y2", function (d) {
return d.target.y - d.lpfNeg1[1] + d.lrste[1][1];
});
textLink.each(function (d, i) {
applyGradient(this, "text", d, i)
});
node
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
if (force.alpha() < 0.05)
drawLegend();
}
function getRandomInt() {
return Math.floor(Math.random() * (100000 - 0));
}
function applyGradient(line, interaction_type, d, i) {
var self = d3.select(line);
var current_gradient = self.style("stroke");
//current_gradient = current_gradient.substring(4, current_gradient.length - 1);
if (current_gradient.match("http")) {
var parts = current_gradient.split("/");
current_gradient = parts[-1];
} else {
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
}
var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index; // + getRandomInt();
var from = d.source.size < d.target.size ? d.source : d.target;
var to = d.source.size < d.target.size ? d.target : d.source;
var mid_offset = 0;
var standardColor = "";
if (interaction_type == "call") {
mid_offset = d.inc_calls / (d.inc_calls + d.out_calls);
standardColor = "#438DCA";
} else {
mid_offset = d.inc_texts / (d.inc_texts + d.out_texts);
standardColor = "#70C05A";
}
/* recordTypes_ID = pluck(recordTypes, 'text');
whichRecordType = recordTypes_ID.indexOf(interaction_type);
standardColor = recordTypes[whichRecordType].color;
*/
mid_offset = mid_offset * 100;
mid_offset = mid_offset * 0.6 + 20; // scale so it doesn't hit the ends
lineLengthCalculation = function (x, y, x0, y0) {
return Math.sqrt((x -= x0) * x + (y -= y0) * y);
};
lineLength = lineLengthCalculation(from.px, from.py, to.px, to.py);
if (lineLength >= 0.1) {
var mark_size_percent = (mark_size / lineLength) * 100,
_offsetDiff = Math.round(mid_offset - mark_size_percent / 2) + "%",
_offsetSum = Math.round(mid_offset + mark_size_percent / 2) + "%",
defsUpdate = defs.selectAll("#" + new_gradient_id)
.data([{
x1: from.px,
y1: from.py,
x2: to.px,
y2: to.py
}]),
defsEnter = defsUpdate.enter().append("linearGradient")
.attr("id", new_gradient_id)
.attr("gradientUnits", "userSpaceOnUse"),
defsUpdateEnter = defsUpdate
.attr("x1", function (d) { return d.x1 })
.attr("y1", function (d) { return d.y1 })
.attr("x2", function (d) { return d.x2 })
.attr("y2", function (d) { return d.y2 }),
stopsUpdate = defsUpdateEnter.selectAll("stop")
.data([{
offset: "0%",
color: standardColor,
opacity: "1"
}, {
offset: _offsetDiff,
color: standardColor,
opacity: "1"
}, {
offset: _offsetDiff,
color: standardColor,
opacity: "1"
}, {
offset: _offsetDiff,
color: "#245A76",
opacity: "1"
}, {
offset: _offsetSum,
color: "#245A76",
opacity: "1"
}, {
offset: _offsetSum,
color: standardColor,
opacity: "1"
}, {
offset: _offsetSum,
color: standardColor,
opacity: "1"
}, {
offset: "100%",
color: standardColor,
opacity: "1"
}
]),
stopsEnter = stopsUpdate.enter().append("stop")
stopsUpdateEnter = stopsUpdate
.attr("offset", function (d) {
return d.offset;
})
.attr("stop-color", function (d) {
return d.color;
})
.attr("stop-opacity", function (d) {
return d.opacity;
})
self.style("stroke", "url(#" + new_gradient_id + ")")
//current_gradient && defs.select(current_gradient).remove(); /*CB Edit*/
}
} /*applyGradient*/
var linkedByIndex;
var width = $(window).width();
var height = $(window).height();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force;
var callLink;
var textLink;
var link;
var node;
var defs;
var marker;
var total_interactions = 0;
var max_interactions = 0;
function CreateVisualizationFromData() {
function chargeForNode(d, i) {
// main node
if (i == 0) {
return -25000;
}
// contains other links
else if (isConnectedToOtherThanMain(d)) {
return -2000;
} else {
return -1200;
}
}
// initial placement of nodes prevents overlaps
var xOffset = 10000,
yOffset = -10000,
central_x = width / 2,
central_y = height / 2;
data_nodes.forEach(function(d, i) {
if (i != 0) {
connected = isConnectedToOtherThanMain(d);
data_nodes[i].x = connected ? central_x + xOffset : central_x - xOffset;
data_nodes[i].y = connected ? central_y + yOffset : central_y - yOffset;
}
else {data_nodes[i].x = central_x; data_nodes[i].y = central_y;}})
force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.charge(function (d, i) {
return chargeForNode(d, i)
})
.friction(0.6) // 0.6
.gravity(0.4) // 0.6
.size([width, height])
.start() //initialise alpha
.stop();
log.update(force.alpha());
call_links_data = data_links.filter(function(d) {
return (d.inc_calls + d.out_calls > 0)});
text_links_data = data_links.filter(function(d) {
return (d.inc_texts + d.out_texts > 0)});
//UPDATE
callLink = svg.selectAll(".call-line")
.data(call_links_data)
//ENTER
callLink.enter().append("line")
.attr('class', 'call-line');
//EXIT
callLink.exit().remove;
//UPDATE
textLink = svg.selectAll(".text-line")
.data(text_links_data)
//ENTER
textLink.enter().append("line")
.attr('class', 'text-line');
//EXIT
textLink.exit().remove;
//UPDATE
node = svg.selectAll(".node")
.data(data_nodes)
//CB the g elements are not needed because there is only one element
//in each node...
//ENTER
node.enter().append("g")
.attr("class", "node")
.append("circle")
.attr("r", node_radius)
.style("fill", function (d) {
return (d.index == 0) ? "#ffffff" : d.fill;
})
.style("stroke", function (d) {
return (d.index == 0) ? "#8C8C8C" : "#ffffff";
});
//EXIT
node.exit().remove;
defs = !(defs && defs.length) ? svg.append("defs") : defs;
marker = svg.selectAll('marker')
.data([{refX: 6+7, refY: 2, markerWidth: 6, markerHeight: 4}])
.enter().append("marker")
.attr("id", "arrowhead")
.attr("refX", function (d) { return d.refX })
.attr("refY", function (d) { return d.refY })
.attr("markerWidth", function (d) { return d.markerWidth })
.attr("markerHeight", function (d) { return d.markerHeight })
.attr("orient", "auto")
.append("path")
.attr("d", "M 0,0 V 4 L6,2 Z");
if (text_links_data.length > 0) {
//UPDATE + ENTER
textLink
.style("stroke-width", function stroke(d) {
return text_width(d)
})
.each(function (d, i) {
applyGradient(this, "text", d, i)
});
}
if (call_links_data.length > 0) {
//UPDATE + ENTER
callLink
.style("stroke-width", function stroke(d) {
return call_width(d)
})
.each(function (d, i) {
applyGradient(this, "call", d, i)
});
}
force
.on("tick", tick);
}
d3.select(document).on('click', (function () {
var _disp = d3.dispatch('stop_start')
return function (e) {
if (!_disp.on('stop_start') || _disp.on('stop_start') === force.stop) {
if (!_disp.on('stop_start')) {
_disp.on('stop_start', force.start)
} else {
_disp.on('stop_start', function () {
CreateVisualizationFromData();
force.start()
//force.alpha(0.5)
})
}
} else {
_disp.on('stop_start', force.stop)
}
_disp.stop_start()
}
})())
function drawLegend() {
var node_px = pluck(data_nodes, 'px');
var node_py = pluck(data_nodes, 'py');
var nodeLayoutRight = Math.max(maxArray(node_px));
var nodeLayoutBottom = Math.max(maxArray(node_py));
legend = svg.selectAll('.legend')
.data(recordTypes)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function (d, i) {
var rect_height = legendRectSize + legendSpacing;
var offset = rect_height * (recordTypes.length-1);
var horz = nodeLayoutRight + 15; /* - 2*legendRectSize; */
var vert = nodeLayoutBottom + (i * rect_height) - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', function (d) {
return d.color
})
.style('stroke', function (d) {
return d.color
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing + 3)
.text(function (d) {
return d.text;
})
.style('fill', '#757575');
}
var line_width_factor = 10.0 // width for the widest line
function call_width(d) {
return (d.inc_calls + d.out_calls) / max_interactions * line_width_factor;
}
function text_width(d) {
return (d.inc_texts + d.out_texts) / max_interactions * line_width_factor;
}
function total_width(d) {
return (d.inc_calls + d.out_calls + d.inc_texts + d.out_texts) / max_interactions * line_width_factor + line_diff;
}
function line_perpendicular_shift(d, direction) {
theta = getAngle(d);
theta_perpendicular = theta + (Math.PI / 2) * direction;
lineWidthOfOppositeLine = direction == 1 ? text_width(d) : call_width(d);
shift = lineWidthOfOppositeLine / 2;
delta_x = (shift + line_diff) * Math.cos(theta_perpendicular)
delta_y = (shift + line_diff) * Math.sin(theta_perpendicular)
return [delta_x, delta_y]
}
function line_radius_shift_to_edge(d, which_node) { // which_node = 0 if source, = 1 if target
theta = getAngle(d);
theta = (which_node == 0) ? theta : theta + Math.PI; // reverse angle if target node
radius = (which_node == 0) ? node_radius(d.source) : node_radius(d.target) // d.source and d.target refer directly to the nodes (not indices)
radius -= 2; // add stroke width
delta_x = radius * Math.cos(theta)
delta_y = radius * Math.sin(theta)
return [delta_x, delta_y]
}
function getAngle(d) {
rel_x = d.target.x - d.source.x;
rel_y = d.target.y - d.source.y;
return theta = Math.atan2(rel_y, rel_x);
}