Related
I have a force directed graph using version 5 of d3.js and would like to include arrow heads for each link. I've included the code below and posted a jsfiddle. I'm seeking guidance on why the arrow heads (id=end-arrow) are not showing up in the graph. I'm referencing the end-arrow as an attribute in the declaration of link: .attr("marker-end","url(#end-arrow)"), and I don't know how to troubleshoot this.
HTML:
<svg id="viz"></svg>
Javascript with d3.js version 5:
// based on https://bl.ocks.org/mapio/53fed7d84cd1812d6a6639ed7aa83868
var width = 600;
var height = 400;
var border = 1;
var bordercolor="black";
var color = d3.scaleOrdinal(d3.schemeCategory10); // coloring of nodes
var graph = {
"nodes": [
{"id": "4718871", "group": 2, "img": "https://derivationmap.net/static/multiplybothsidesby.png", "width": 298, "height": 30, "linear index": 2},
{"id": "2131616531", "group": 0, "img": "https://derivationmap.net/static/2131616531.png", "width": 103, "height": 30, "linear index": 0},
{"id": "9565166889", "group": 0, "img": "https://derivationmap.net/static/9565166889.png", "width": 24, "height": 23, "linear index": 0},
{"id": "9040079362", "group": 0, "img": "https://derivationmap.net/static/9040079362.png", "width": 18, "height": 30, "linear index": 0},
{"id": "9278347", "group": 1, "img": "https://derivationmap.net/static/declareinitialexpr.png", "width": 270, "height": 30, "linear index": 1},
{"id": "6286448", "group": 4, "img": "https://derivationmap.net/static/declarefinalexpr.png", "width": 255, "height": 30, "linear index": 4},
{"id": "2113211456", "group": 0, "img": "https://derivationmap.net/static/2113211456.png", "width": 121, "height": 34, "linear index": 0},
{"id": "2169431", "group": 3, "img": "https://derivationmap.net/static/dividebothsidesby.png", "width": 260, "height": 30, "linear index": 3},
{"id": "3131111133", "group": 0, "img": "https://derivationmap.net/static/3131111133.png", "width": 121, "height": 34, "linear index": 0}
],
"links": [
{"source": "2169431", "target": "2113211456", "value": 1},
{"source": "2113211456", "target": "6286448", "value": 1},
{"source": "9278347", "target": "3131111133", "value": 1},
{"source": "4718871", "target": "2131616531", "value": 1},
{"source": "9040079362", "target": "4718871", "value": 1},
{"source": "2131616531", "target": "2169431", "value": 1},
{"source": "3131111133", "target": "4718871", "value": 1},
{"source": "9565166889", "target": "2169431", "value": 1}
]
};
var label = {
"nodes": [],
"links": []
};
graph.nodes.forEach(function(d, i) {
label.nodes.push({node: d});
label.nodes.push({node: d});
label.links.push({
source: i * 2,
target: i * 2 + 1
});
});
var labelLayout = d3.forceSimulation(label.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(label.links).distance(0).strength(2));
var graphLayout = d3.forceSimulation(graph.nodes)
.force("charge", d3.forceManyBody().strength(-3000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(1))
.force("y", d3.forceY(height / 2).strength(1))
.force("link", d3.forceLink(graph.links).id(function(d) {return d.id; }).distance(50).strength(1))
.on("tick", ticked);
var adjlist = [];
graph.links.forEach(function(d) {
adjlist[d.source.index + "-" + d.target.index] = true;
adjlist[d.target.index + "-" + d.source.index] = true;
});
function neigh(a, b) {
return a == b || adjlist[a + "-" + b];
}
var svg = d3.select("#viz").attr("width", width).attr("height", height);
// define arrow markers for graph links
svg.append("svg:defs").append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 6)
.attr("markerWidth", 3)
.attr("markerHeight", 3)
.attr("orient", "auto")
.append("svg:line")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "black");
// http://bl.ocks.org/AndrewStaroscik/5222370
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
var container = svg.append("g");
svg.call(
d3.zoom()
.scaleExtent([.1, 4])
.on("zoom", function() { container.attr("transform", d3.event.transform); })
);
var link = container.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke", "#aaa")
.attr("stroke-width", "1px")
.attr("marker-end","url(#end-arrow)");
var node = container.append("g").attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) { return color(d.group); })
node.on("mouseover", focus).on("mouseout", unfocus);
node.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
var labelNode = container.append("g").attr("class", "labelNodes")
.selectAll("text")
.data(label.nodes)
.enter()
.append("image")
// alternative option, unverified: https://stackoverflow.com/questions/39908583/d3-js-labeling-nodes-with-image-in-force-layout
// I have no idea why the i%2 is needed; without it I get two images per node
// switching between i%2==1 and i%2==0 produces different image locations (?)
.attr("xlink:href", function(d, i) { return i % 2 == 1 ? "" : d.node.img; } )
.attr("x", 0)
.attr("y", 0)
// the following alter the image size
.attr("width", function(d, i) { return d.node.width/2; })
.attr("height", function(d, i) { return d.node.height/2; })
// .append("text")
// .text(function(d, i) { return i % 2 == 0 ? "" : d.node.id; })
// .style("fill", "#555")
// .style("font-family", "Arial")
// .style("font-size", 12)
.style("pointer-events", "none"); // to prevent mouseover/drag capture
node.on("mouseover", focus).on("mouseout", unfocus);
function ticked() {
node.call(updateNode);
link.call(updateLink);
labelLayout.alphaTarget(0.3).restart();
labelNode.each(function(d, i) {
if(i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 16;
this.setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
function fixna(x) {
if (isFinite(x)) return x;
return 0;
}
function focus(d) {
var index = d3.select(d3.event.target).datum().index;
node.style("opacity", function(o) {
return neigh(index, o.index) ? 1 : 0.1;
});
labelNode.attr("display", function(o) {
return neigh(index, o.node.index) ? "block": "none";
});
link.style("opacity", function(o) {
return o.source.index == index || o.target.index == index ? 1 : 0.1;
});
}
function unfocus() {
labelNode.attr("display", "block");
node.style("opacity", 1);
link.style("opacity", 1);
}
function updateLink(link) {
link.attr("x1", function(d) { return fixna(d.source.x); })
.attr("y1", function(d) { return fixna(d.source.y); })
.attr("x2", function(d) { return fixna(d.target.x); })
.attr("y2", function(d) { return fixna(d.target.y); });
}
function updateNode(node) {
node.attr("transform", function(d) {
return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
});
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d3.event.active) graphLayout.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) graphLayout.alphaTarget(0);
d.fx = null;
d.fy = null;
}
Based on feedback in the d3js Slack channel, there were two issues:
In the definition of the arrow, needed .append("svg:path")
With that fixed, the arrows were too small and were hidden behind the node circles. By making the arrows larger, they were visible.
I've updated http://bl.ocks.org/bhpayne/0a8ef2ae6d79aa185dcf2c3a385daf25 and the revised code is below:
HTML
<svg id="viz"></svg>
Javascript + d3js
// based on https://bl.ocks.org/mapio/53fed7d84cd1812d6a6639ed7aa83868
var width = 600;
var height = 400;
var border = 3;
var bordercolor = "black";
var color = d3.scaleOrdinal(d3.schemeCategory10); // coloring of nodes
var graph = {
"nodes": [{
"id": "4718871",
"group": 2,
"img": "https://derivationmap.net/static/multiplybothsidesby.png",
"width": 298,
"height": 30,
"linear index": 2
},
{
"id": "2131616531",
"group": 0,
"img": "https://derivationmap.net/static/2131616531.png",
"width": 103,
"height": 30,
"linear index": 0
},
{
"id": "9565166889",
"group": 0,
"img": "https://derivationmap.net/static/9565166889.png",
"width": 24,
"height": 23,
"linear index": 0
},
{
"id": "9040079362",
"group": 0,
"img": "https://derivationmap.net/static/9040079362.png",
"width": 18,
"height": 30,
"linear index": 0
},
{
"id": "9278347",
"group": 1,
"img": "https://derivationmap.net/static/declareinitialexpr.png",
"width": 270,
"height": 30,
"linear index": 1
},
{
"id": "6286448",
"group": 4,
"img": "https://derivationmap.net/static/declarefinalexpr.png",
"width": 255,
"height": 30,
"linear index": 4
},
{
"id": "2113211456",
"group": 0,
"img": "https://derivationmap.net/static/2113211456.png",
"width": 121,
"height": 34,
"linear index": 0
},
{
"id": "2169431",
"group": 3,
"img": "https://derivationmap.net/static/dividebothsidesby.png",
"width": 260,
"height": 30,
"linear index": 3
},
{
"id": "3131111133",
"group": 0,
"img": "https://derivationmap.net/static/3131111133.png",
"width": 121,
"height": 34,
"linear index": 0
}
],
"links": [{
"source": "2169431",
"target": "2113211456",
"value": 1
},
{
"source": "2113211456",
"target": "6286448",
"value": 1
},
{
"source": "9278347",
"target": "3131111133",
"value": 1
},
{
"source": "4718871",
"target": "2131616531",
"value": 1
},
{
"source": "9040079362",
"target": "4718871",
"value": 1
},
{
"source": "2131616531",
"target": "2169431",
"value": 1
},
{
"source": "3131111133",
"target": "4718871",
"value": 1
},
{
"source": "9565166889",
"target": "2169431",
"value": 1
}
]
};
var label = {
"nodes": [],
"links": []
};
graph.nodes.forEach(function(d, i) {
label.nodes.push({
node: d
});
label.nodes.push({
node: d
});
label.links.push({
source: i * 2,
target: i * 2 + 1
});
});
var labelLayout = d3.forceSimulation(label.nodes)
.force("charge", d3.forceManyBody().strength(-50))
.force("link", d3.forceLink(label.links).distance(0).strength(2));
var graphLayout = d3.forceSimulation(graph.nodes)
.force("charge", d3.forceManyBody().strength(-3000))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("x", d3.forceX(width / 2).strength(1))
.force("y", d3.forceY(height / 2).strength(1))
.force("link", d3.forceLink(graph.links).id(function(d) {
return d.id;
}).distance(50).strength(1))
.on("tick", ticked);
var adjlist = [];
graph.links.forEach(function(d) {
adjlist[d.source.index + "-" + d.target.index] = true;
adjlist[d.target.index + "-" + d.source.index] = true;
});
function neigh(a, b) {
return a == b || adjlist[a + "-" + b];
}
var svg = d3.select("#viz").attr("width", width).attr("height", height);
// define arrow markers for graph links
svg.append("svg:defs").append("svg:marker")
.attr("id", "end-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 10)
.attr("markerWidth", 20)
.attr("markerHeight", 20)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L20,0L0,5")
.attr("fill", "#000");
// http://bl.ocks.org/AndrewStaroscik/5222370
var borderPath = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.style("stroke", bordercolor)
.style("fill", "none")
.style("stroke-width", border);
var container = svg.append("g");
svg.call(
d3.zoom()
.scaleExtent([.1, 4])
.on("zoom", function() {
container.attr("transform", d3.event.transform);
})
);
var link = container.append("g").attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke", "#aaa")
.attr("marker-end", "url(#end-arrow)")
.attr("stroke-width", "1px");
var node = container.append("g").attr("class", "nodes")
.selectAll("g")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", 10)
.attr("fill", function(d) {
return color(d.group);
})
node.on("mouseover", focus).on("mouseout", unfocus);
node.call(
d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);
var labelNode = container.append("g").attr("class", "labelNodes")
.selectAll("text")
.data(label.nodes)
.enter()
.append("image")
// alternative option, unverified: https://stackoverflow.com/questions/39908583/d3-js-labeling-nodes-with-image-in-force-layout
// I have no idea why the i%2 is needed; without it I get two images per node
// switching between i%2==1 and i%2==0 produces different image locations (?)
.attr("xlink:href", function(d, i) {
return i % 2 == 1 ? "" : d.node.img;
})
.attr("x", 0)
.attr("y", 0)
// the following alter the image size
.attr("width", function(d, i) {
return d.node.width / 2;
})
.attr("height", function(d, i) {
return d.node.height / 2;
})
// .append("text")
// .text(function(d, i) { return i % 2 == 0 ? "" : d.node.id; })
// .style("fill", "#555")
// .style("font-family", "Arial")
// .style("font-size", 12)
.style("pointer-events", "none"); // to prevent mouseover/drag capture
node.on("mouseover", focus).on("mouseout", unfocus);
function ticked() {
node.call(updateNode);
link.call(updateLink);
labelLayout.alphaTarget(0.3).restart();
labelNode.each(function(d, i) {
if (i % 2 == 0) {
d.x = d.node.x;
d.y = d.node.y;
} else {
var b = this.getBBox();
var diffX = d.x - d.node.x;
var diffY = d.y - d.node.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
var shiftX = b.width * (diffX - dist) / (dist * 2);
shiftX = Math.max(-b.width, Math.min(0, shiftX));
var shiftY = 16;
this.setAttribute("transform", "translate(" + shiftX + "," + shiftY + ")");
}
});
labelNode.call(updateNode);
}
function fixna(x) {
if (isFinite(x)) return x;
return 0;
}
function focus(d) {
var index = d3.select(d3.event.target).datum().index;
node.style("opacity", function(o) {
return neigh(index, o.index) ? 1 : 0.1;
});
labelNode.attr("display", function(o) {
return neigh(index, o.node.index) ? "block" : "none";
});
link.style("opacity", function(o) {
return o.source.index == index || o.target.index == index ? 1 : 0.1;
});
}
function unfocus() {
labelNode.attr("display", "block");
node.style("opacity", 1);
link.style("opacity", 1);
}
function updateLink(link) {
link.attr("x1", function(d) {
return fixna(d.source.x);
})
.attr("y1", function(d) {
return fixna(d.source.y);
})
.attr("x2", function(d) {
return fixna(d.target.x);
})
.attr("y2", function(d) {
return fixna(d.target.y);
});
}
function updateNode(node) {
node.attr("transform", function(d) {
return "translate(" + fixna(d.x) + "," + fixna(d.y) + ")";
});
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
if (!d3.event.active) graphLayout.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) graphLayout.alphaTarget(0);
d.fx = null;
d.fy = null;
}
Problem: highlighted text(in the image below) is been over-written(therefore its looking bold) when the node is expanded.
Expected Output:
Collapsible d3 Treechart
var linktext = svg.selectAll("g.link")
.data(links, function (d) {
console.log("Text Data Id..."+d.id);
return d.id;
});
linktext.enter()
.insert("g")
.merge(linktext)
.attr("class", "link")
.attr("transform", function (d) {
return "translate(" + ((d.parent.y + d.y) / 2) + "," + ((d.parent.x +d.x) / 2) + ")"; })
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
debugger;
console.log("Text Data Rule..."+d.data.rule);
return d.data.rule;
});
linktext.exit().transition()
.remove();
Your problem is caused due to .merge function, this code will fix the multiple-element issue
var treeData =
{ "name": "E", "children": [{ "name": "A", "rule": "yes", "children": [{ "name": "B", "rule": "<=3.843750000000001", "children": [{ "name": "C", "rule": "yes" }, { "name": "D", "rule": "no", "children": [{ "name": "Sex", "rule": "yes", "children": [{ "name": "E", "rule": "Male", "children": [{ "name": "F", "rule": "<=62.5125" }, { "name": "G", "rule": ">62.5125", "children": [{ "name": "H", "rule": "yes" }, { "name": "K", "rule": "no" }] }] }, { "name": "I", "rule": "Female" }] }, { "name": "J", "rule": "no" }] }] }, { "name": "K", "rule": ">3.843750000000001", "children": [{ "name": "Q", "rule": "yes" }, { "name": "H", "rule": "no", "children": [{ "name": "S", "rule": "yes" }, { "name": "N", "rule": "no" }] }] }] }, { "name": "L", "rule": "no", "children": [{ "name": "S", "rule": "yes", "children": [{ "name": "T", "rule": "<=82.025", "children": [{ "name": "Y", "rule": "<=102.9125" }, { "name": "Malaise", "rule": ">102.9125", "children": [{ "name": "Age", "rule": "no", "children": [{ "name": "J", "rule": ">40.6625", "children": [{ "name": "M", "rule": "yes", "children": [{ "name": "N", "rule": "no", "children": [{ "name": "G", "rule": "yes" }, { "name": "E", "rule": "no" }] }] }] }] }] }] }] }, { "name": "survive", "rule": "no" }] }] };
// Set the dimensions and margins of the diagram
var margin = { top: 20, right: 90, bottom: 30, left: 90 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
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 + ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function (d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Collapse after the second level
root.children.forEach(collapse);
update(root);
// Collapse the node and all it's children
function collapse(d) {
if (d.children) {
d._children = d.children
d._children.forEach(collapse)
d.children = null
}
}
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
var nodes = treeData.descendants(),
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function (d) { d.y = d.depth * 180 });
// ****************** Nodes section ***************************
// Update the nodes...
var node = svg.selectAll('g.node')
.data(nodes, function (d) { return d.id || (d.id = ++i); });
// Enter any new modes at the parent's previous position.
var nodeEnter = node.enter().append('g')
.attr('class', 'node')
.attr("transform", function (d) {
return "translate(" + source.y0 + "," + source.x0 + ")";
})
.on('click', click);
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", ".35em")
.attr("x", function (d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function (d) {
return d.children || d._children ? "end" : "start";
})
.text(function (d) { return d.data.name; });
// UPDATE
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function (d) {
return d._children ? "lightsteelblue" : "#fff";
})
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle')
.attr('r', 1e-6);
// On exit reduce the opacity of text labels
nodeExit.select('text')
.style('fill-opacity', 1e-6);
// ****************** links section ***************************
// Update the links...
var link = svg.selectAll('path.link')
.data(links, function (d) { return d.id; });
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', function (d) {
var o = { x: source.x0, y: source.y0 }
return diagonal(o, o)
});
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function (d) { return diagonal(d, d.parent) });
// Remove any exiting links
link.exit().transition()
.duration(duration)
.attr('d', function (d) {
var o = { x: source.x, y: source.y }
return diagonal(o, o)
})
.remove();
var linktext = svg.selectAll("g.link")
.data(links, function (d) {
return d.id;
});
linktext.enter()
.insert("g")
//.merge(linktext)
.attr("class", "link")
.attr("transform", function (d) {
return "translate(" + ((d.parent.y + d.y) / 2) + "," + ((d.parent.x + d.x) / 2) + ")";
})
.append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function (d) {
return d.data.rule;
});
linktext.exit().remove();
// Store the old positions for transition.
nodes.forEach(function (d) {
d.x0 = d.x;
d.y0 = d.y;
});
// Creates a curved (diagonal) path from parent to the child nodes
function diagonal(s, d) {
path = `M ${s.y} ${s.x}
C ${(s.y + d.y) / 2} ${s.x},
${(s.y + d.y) / 2} ${d.x},
${d.y} ${d.x}`
return path
}
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
Also replace your css with the below to fix the link-text font:
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 1px;
}
I have a d3 force layout with a line connecting a character. My code currently looks something like this...
edges.selectAll("line")
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
<g class="edge"><line x1="183.7429436753079" y1="-3182.732396966405" x2="-224.94046319279028" y2="-2920.273406745797" style="stroke: rgb(255, 255, 255); stroke-width: 1px;"></line><text fill="white" x="-20.59875975874118" y="-3051.502901856101"></text></g>
<g class="node" transform="translate(4109.590685978889,2004.5469511133144)"><text fill="white"></text><text fill="white" y="15">Interior</text></g>
When I use this I see something like the following...
as you can see the relations all meet at the bottom, however, I want them to meet on the left. For this I figure I need to change to something like...
edges.selectAll("line")
.attr("x1", function (d) { return d.source.x+r; })
.attr("y1", function (d) { return d.source.y-r; })
.attr("x2", function (d) { return d.target.x+r; })
.attr("y2", function (d) { return d.target.y-r; });
Where r is the radius. Short of keeping an array with the widths of all nodes, is there a way to grab the source and target's width?
You can add the BBox onto the node data as a reference while adding the text elements to the document. If you add the nodes first, followed by the links, you can transfer information from the former to the later by adding a reference to the node elements onto the data elements. You can also add any other helpful positioning state that you might need on data array elements. After that you can access whatever you need in the force tick call-back via d.source and d.target.
Using font awesome fonts I noticed that the bounding box took at least one animation cycle to evolve to the correct shape so I had to add an animation timer to run a few (10) times after initial rendering to update the bounding box a few times and reposition the glyphs to be properly centered.
Edit
Made the bounding box adjustment permanent (not just run 10 times) to work around a bug in webkit whereby the glyph alignment breaks on zoom events. This however caused problems in moz so need to find another way to fix the zoom bug in webkit.
Note
Referencing the svg element that the data is bound to, from that data element, creates a circular reference. So special care needs to be taken to break the reference chain. In the example below, the BBox reference is deleted after the required state has been copied onto the data elements.
Working example
//debug panel/////////////////////////////////////////////////////////////////////////////
d3.select("#update").on("click", (function() {
var dataSet = false;
return function() {
refresh(dataSets[(dataSet = !dataSet, +dataSet)])
}
})());
var alpha = d3.select("#alpha").text("waiting..."),
cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true),
fdgInst = d3.select("#fdg");
elapsedTime = ElapsedTime("#panel", {margin: 0, padding: 0})
.message(function (id) {
return 'fps : ' + d3.format(" >8.3f")(1/this.aveLap())
});
elapsedTime.consoleOn = false;
//////////////////////////////////////////////////////////////////////////////////
var dataSets = [
{
"nodes": [
{"name": "node1", "content": "the first Node"},
{"name": "node2", "content": "node2"},
{"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
{"name": "node4", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}}
],
"edges": [
{"source": 2, "target": 0},
{"source": 2, "target": 1},
{"source": 2, "target": 3}
]
},
{
"nodes": [
{"name": "node1", "content": "node1"},
{"name": "node2", "content":{"fa": "fa/*-spin*/", text: "\uf1ce"}},
{"name": "node3", "content":{"fa": "fa/*-spin*/", text: "\uf013"}},
{"name": "node4", "content": "4"},
{"name": "node5", "content": "5"},
{"name": "node6", "content": "6"}
],
"edges": [
{"source": 2, "target": 0},
{"source": 2, "target": 1},
{"source": 2, "target": 3},
{"source": 2, "target": 4},
{"source": 2, "target": 5}
]
}
];
var refresh = (function(){
var instID = Date.now(),
height = 160,
width = 500,
force = d3.layout.force()
.size([width, height])
.charge(-1000)
.linkDistance(50)
.on("end", function(){cog.classed("fa-spin", false); elapsedTime.stop()})
.on("start", function(){cog.classed("fa-spin", true); elapsedTime.start()});
return function refresh(data) {
force
.nodes(data.nodes)
.links(data.edges)
.on("tick", (function(instID) {
return function(e) {
elapsedTime.mark();
alpha.text(d3.format(" >8.4f")(e.alpha));
fdgInst.text("fdg instance: " + instID);
lines.attr("x1", function(d) {
return d.source.x + d.source.cx + d.source.r;
}).attr("y1", function(d) {
return d.source.y + d.source.cy;
}).attr("x2", function(d) {
return d.target.x + d.target.cx;
}).attr("y2", function(d) {
return d.target.y + d.target.cy;
});
node.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")"
});
}
})(instID))
.start();
var svg = d3.select("body").selectAll("svg").data([data]);
svg.enter().append("svg")
.attr({height: height, width: width});
var lines = svg.selectAll(".links")
.data(linksData),
linesEnter = lines.enter()
.insert("line", d3.select("#nodes") ? "#nodes" : null)
.attr("class", "links")
.attr({stroke: "steelblue", "stroke-width": 3});
var nodes = svg.selectAll("#nodes").data(nodesData),
nodesEnter = nodes.enter().append("g")
.attr("id", "nodes"),
node = nodes.selectAll(".node")
.data(id),
newNode = node.enter().append("g")
.attr("class","node")
.call(force.drag);
newNode.append("text")
.attr({class: "content", fill: "steelblue"})
newNode.insert("circle", ".node .content");
var glyphs = node.select("text")
.each(function(d) {
var node = d3.select(this);
if(d.content.fa)
node.style({'font-family': 'FontAwesome', 'font-size': '32px', 'dominant-baseline': 'central'})
.classed(d.content.fa, true)
.text(d.content.text);
else
node.text(d.content)
.attr({"class": "content", style: null});
})
.call(getBB),
backGround = node.select("circle").each(function(d) {
d3.select(this).attr(makeCircleBB(d))
}).style({"fill": "red", opacity: 0.8});
(function(id){
//adjust the bounding box after the font loads
var count = 0;
d3.timer(function() {
console.log(id);
glyphs.call(getBB);
backGround.each(function(d) {
d3.select(this).attr(makeCircleBB(d))
});
return /*false || id != instID*/++count > 10; //needs to keep running due to webkit zoom bug
})
})(instID);
lines.exit().remove();
node.exit().remove();
function nodesData(d) {
return [d.nodes];
}
function linksData(d) {
return d.edges;
}
};
function getBB(selection) {
this.each(function(d) {
d.bb = this.getBBox();
})
}
function makeCircleBB(d, i, j) {
var bb = d.bb;
d.r = Math.max(bb.width, bb.height) / 2;
delete d.bb; //plug potential memory leak!
d.cy = bb.height / 2 + bb.y;
d.cx = bb.width / 2;
return {
r: d.r, cx: d.cx, cy: d.cy, height: bb.height, width: bb.width
}
}
function id(d) {
return d;
}
})();
refresh(dataSets[0]);
svg {
outline: 1px solid #282f51;
pointer-events: all;
overflow: visible;
}
g.outline {
outline: 1px solid red;
}
#panel div {
display: inline-block;
margin: 0 .25em 3px 0;
}
#panel div div {
white-space: pre;
margin: 0 .25em 3px 0;
}
div#inputDiv {
white-space: normal;
display: inline-block;
}
.node {
cursor: default;
}
.content {
transform-origin: 50% 50%;
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://rawgit.com/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-1.0.js"></script>
<div id="panel">
<div id="inputDiv">
<input id="update" type="button" value="update">
</div>
<div id="wrapAlpha">alpha:<div id="alpha"></div></div>
<div id="fdg"></div>
</div>
<div id="viz"></div>
I am using d3.js and would like the force layout to be NOT random every time I load up the page.
I have already read over a few questions and I set the node position but the layout still is random.
Just set static x and y positions in your node objects.
var graph = {
"nodes": [{
"name": "1",
"rating": 90,
"id": 2951,
x: 5, //Any random value
y: 10 //Any random value
}, {
"name": "2",
"rating": 80,
"id": 654654,
x: 15,
y: 20
------------------
-------------------
}],
"links": [{
"source": 5,
"target": 2,
"value": 6,
"label": "publishedOn"
}, {
"source": 1,
"target": 5,
"value": 6,
"label": "publishedOn"
},
------------------
-------------------
}
Here is the working code snippet.
var graph = {
"nodes": [{
"name": "1",
"rating": 90,
"id": 2951,
x: 5,
y: 10
}, {
"name": "2",
"rating": 80,
"id": 654654,
x: 15,
y: 20
}, {
"name": "3",
"rating": 80,
"id": 6546544,
x: 5,
y: 60
}, {
"name": "4",
"rating": 1,
"id": 68987978,
x: 55,
y: 17
}, {
"name": "5",
"rating": 1,
"id": 9878933,
x: 24,
y: 70
}, {
"name": "6",
"rating": 1,
"id": 6161,
x: 35,
y: 10
}],
"links": [{
"source": 5,
"target": 2,
"value": 6,
"label": "publishedOn"
}, {
"source": 1,
"target": 5,
"value": 6,
"label": "publishedOn"
}, {
"source": 4,
"target": 5,
"value": 4,
"label": "containsKeyword"
}, {
"source": 2,
"target": 3,
"value": 3,
"label": "containsKeyword"
}, {
"source": 3,
"target": 2,
"value": 4,
"label": "publishedBy"
}]
}
var margin = {
top: -5,
right: -5,
bottom: -5,
left: -5
};
var width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-200)
.linkDistance(50)
.size([width + margin.left + margin.right, height + margin.top + margin.bottom]);
var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var drag = d3.behavior.drag()
.origin(function(d) {
return d;
})
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
var svg = d3.select("#map").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.right + ")")
.call(zoom);
var rect = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all");
var container = svg.append("g");
//d3.json('http://blt909.free.fr/wd/map2.json', function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = container.append("g")
.attr("class", "links")
.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) {
return Math.sqrt(d.value);
});
var node = container.append("g")
.attr("class", "nodes")
.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.call(drag);
node.append("circle")
.attr("r", function(d) {
return d.weight * 2 + 12;
})
.style("fill", function(d) {
return color(1 / d.rating);
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
var linkedByIndex = {};
graph.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index];
}
node.on("mouseover", function(d) {
node.classed("node-active", function(o) {
thisOpacity = isConnected(d, o) ? true : false;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.classed("link-active", function(o) {
return o.source === d || o.target === d ? true : false;
});
d3.select(this).classed("node-active", true);
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", (d.weight * 2 + 12) * 1.5);
})
.on("mouseout", function(d) {
node.classed("node-active", false);
link.classed("link-active", false);
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", d.weight * 2 + 12);
});
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
force.start();
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.node-active{
stroke: #555;
stroke-width: 1.5px;
}
.link {
stroke: #555;
stroke-opacity: .3;
}
.link-active {
stroke-opacity: 1;
}
.overlay {
fill: none;
pointer-events: all;
}
#map{
border: 2px #555 dashed;
width:500px;
height:400px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
<div id="map"></div>
</body>
I'm trying to force nodes into different clusters in force layout based on a certain attribute in the data like "group." I'm adapting the code from Mike Bostock's multi foci force layout example (code, example) and I've been successful in adding in my own data but I haven't been able to specify how many clusters there are and how to assign a node to a cluster.
I'm relatively new to d3 and JavaScript and I haven't been able to find many examples of multi foci applications. Here's my d3 code, any help or input is appreciated:
var width = 960,
height = 500;
var fill = d3.scale.category10();
d3.json("test.json" , function(error, json){
var root = json.nodes[0];
root.radius = 0;
root.fixed = true;
var force = d3.layout.force()
.nodes(json.nodes)
.size([width, height])
.gravity(0.06)
.charge(function(d, i) { return i ? 0 : -2000; })
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var elem = svg.selectAll(".elem")
.data(json.nodes)
.enter()
.append("g")
.attr("class", "elem");
elem.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 40)
.style("fill", function(d, i) { return fill(i & 3); })
.style("stroke", function(d, i) { return d3.rgb(fill(i & 3)).darker(2); })
.call(force.drag)
.on("mousedown", function() { d3.event.stopPropagation(); });
elem.append("text")
.text(function(d){ return d.name; });
svg.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
d3.select("body")
.on("mousedown", mousedown);
I've specifically been trying to figure out how this tick function is working. I did some research and found that the "&" is a bitwise operator and I noticed that changing the number after it is what is changing how many clusters there are and which nodes are in each. But preferably I would like to be able to point to something like d.group here to specify the cluster.
function tick(e) {
// Push different nodes in different directions for clustering.
var k = 6 * e.alpha;
json.nodes.forEach(function(o, i) {
o.y += i & 3 ? k : -k;
o.x += i & 2 ? k : -k;
});
var q = d3.geom.quadtree(json.nodes),
i = 0,
n = json.nodes.length;
while (++i < n) q.visit(collide(json.nodes[i]));
svg.selectAll("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
svg.selectAll("text")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
function collide(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * .5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
svg.on("mousemove", function() {
var p1 = d3.mouse(this);
root.px = p1[0];
root.py = p1[1];
force.resume();
});
function mousedown() {
json.nodes.forEach(function(o, i) {
o.x += (Math.random() - .5) * 40;
o.y += (Math.random() - .5) * 40;
});
force.resume();
}
});
*Note that I have also implemented collision detection to repel nodes but I don't think this is affecting the clusters at the moment.
My data is currently stored in a json file called test.json:
{
"nodes":[
{
"name": "Null",
"radius": 40,
"color": "#ff0000",
"gravity": 0.05,
"group": 1
},
{
"name": "One",
"radius": 40,
"color": "#ffff00",
"gravity": 0.05,
"group": 1
},
{
"name": "Two",
"radius": 40,
"color": "#33cc33",
"gravity": 0.2,
"group": 1
},
{
"name": "Three",
"radius": 40,
"color": "#3399ff",
"gravity": 0.9,
"group": 1
},
{
"name": "Four",
"radius": 40,
"color": "#ffff00",
"gravity": 0.05,
"group": 6
},
{
"name": "Five",
"radius": 40,
"color": "#33cc33",
"gravity": 0.2,
"group": 6
},
{
"name": "Six",
"radius": 40,
"color": "#3399ff",
"gravity": 0.9,
"group": 6
}
]
}
All the clustering work takes place here:
// Push different nodes in different directions for clustering.
var k = 6 * e.alpha;
json.nodes.forEach(function(o, i) {
o.y += i & 3 ? k : -k;
o.x += i & 2 ? k : -k;
});
Admittedly, I don't get how it works in this particular example. It seems indirect and hard to understand. More generally, this is what you want to do in order to cluster:
force.on("tick", function(e) {
var k = e.alpha * .1;
nodes.forEach(function(node) {
var center = ...; // here you want to set center to the appropriate [x,y] coords
node.x += (center.x - node.x) * k;
node.y += (center.y - node.y) * k;
});
It's taken straight out of this example and you can view source to see the code.
In this code, it's easier to understand how, on tick, the nodes are pushed closer to a desired focal point. So now you need to come up with a way to map a node to a focal point based on its group param so that you fill in that var center = ...; line.
First, you need to get an inventory of all the groups in json.nodes. d3.nest() is good for that:
var groups = d3.nest()
.key(function(d) { return d.group; })
.map(json.nodes)
That will give you a mapping of groups to node. Since your example json has just 2 groups in it ("1" and "6"), it'll look like this:
{
"1": [
{
"name": "Null",
"radius": 40,
"color": "#ff0000",
"gravity": 0.05,
"group": 1
},
{
"name": "One",
"radius": 40,
"color": "#ffff00",
"gravity": 0.05,
"group": 1
},
{
"name": "Two",
"radius": 40,
"color": "#33cc33",
"gravity": 0.2,
"group": 1
},
{
"name": "Three",
"radius": 40,
"color": "#3399ff",
"gravity": 0.9,
"group": 1
}
],
"6": [
{
"name": "Four",
"radius": 40,
"color": "#ffff00",
"gravity": 0.05,
"group": 6
},
{
"name": "Five",
"radius": 40,
"color": "#33cc33",
"gravity": 0.2,
"group": 6
},
{
"name": "Six",
"radius": 40,
"color": "#3399ff",
"gravity": 0.9,
"group": 6
}
]
}
Then you can loop over groups and assign each group a center point as you'd like. How you do that depends on what you're trying to achieve. Perhaps you'd like to distribute the focal points in a circle around the center of the screen. It's up to you... But the goal is to end up with something where groups["1"].center equals something like {x:123, y:456}, because then you can plug that back into the tick handler from above:
force.on("tick", function(e) {
var k = e.alpha * .1;
nodes.forEach(function(node) {
var center = groups[node.group].center;
node.x += (center.x - node.x) * k;
node.y += (center.y - node.y) * k;
});
And (hopefully) voila!