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;
}
Related
Below example should update the nodes after select the groups at the bottom. but the select works fine, the nodes on the svg not updated!
force_json()
function force_json(){
var svg = d3.select('body').append('svg')
.attr('width',200).attr('height',100)
.style('border','1px solid red'),
width = +svg.attr("width"),
height = +svg.attr("height");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
var color = d3.scaleOrdinal(d3.schemeCategory10);
var jsonobj = {
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 2},
{"id": "Mlle.Baptistine", "group": 3},
{"id": "Mme.Magloire", "group": 1},
],
"links": [
{"source": "Napoleon", "target": "Myriel", "value": 10},
{"source": "Mlle.Baptistine", "target": "Myriel", "value": 8},
{"source": "Mme.Magloire", "target": "Myriel", "value": 10},
]
}
// d3.json("miserables.json",function(error, graph) {
// if (error) throw error;
// });
process_data(jsonobj)
function process_data(graph) {
var currNodes = graph.nodes
var currLinks = graph.links
var nodesByGroup = d3.group(graph.nodes,d => d.group)
var catMenu = d3.select("body").append('div')
catMenu
.append("select")
.selectAll("option")
.data(nodesByGroup)
.enter()
.append("option")
.attr("value", function(d,i) {
return d[0];
})
.text(function(d,i){
return d[0];
})
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(currLinks)
.enter().append("line")
.attr('stroke','#aaa')
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(currNodes)
.enter().append("circle")
.attr("r", 5)
.attr('pointer-events','all')
.attr('stroke','none')
.attr('stroke-wdith',40)
.attr("fill", function(d) { return color(d.group);})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(function(d) { return d.id; });
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id;})
simulation
.nodes(currNodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function ticked() {
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
catMenu.on('change', function(){
var selectedGroup = +d3.select(this)
.select("select")
.property("value");
currNodes = filterNodes(selectedGroup);
});
function filterNodes(group) {
var filteredNodes = nodesByGroup.get(group)
return filteredNodes;
}
}
function dragstarted(event,d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event,d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Re-render filtered nodes and links upon group selection:
force_json()
function force_json(){
var svg = d3.select('body').append('svg')
.attr('width',200).attr('height',200)
.style('border','1px solid red'),
width = +svg.attr("width"),
height = +svg.attr("height");
var color = d3.scaleOrdinal(d3.schemeCategory10);
var jsonobj = {
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 2},
{"id": "Mlle.Baptistine", "group": 3},
{"id": "Mme.Magloire", "group": 1},
{"id": "A", "group": 2},
{"id": "B", "group": 3},
{"id": "C", "group": 3}
],
"links": [
{"source": "Napoleon", "target": "Myriel", "value": 10},
{"source": "Napoleon", "target": "A", "value": 10},
{"source": "Mlle.Baptistine", "target": "Myriel", "value": 8},
{"source": "Mlle.Baptistine", "target": "B", "value": 8},
{"source": "Mme.Magloire", "target": "Myriel", "value": 10},
{"source": "A", "target": "B", "value": 10},
{"source": "C", "target": "B", "value": 10},
]
}
process_data(jsonobj)
function process_data(graph) {
var nodesByGroup = d3.group(graph.nodes,d => d.group)
var catMenu = d3.select("body").append('div')
catMenu
.append("select")
.selectAll("option")
.data(nodesByGroup)
.enter()
.append("option")
.attr("value", function(d,i) {
return d[0];
})
.text(function(d,i){
return d[0];
})
catMenu.select('select').append('option').text('all').attr("selected", "selected");
const updateGraph = (nodes, links) => {
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
svg.selectAll('g').remove();
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr('stroke','#aaa')
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 5)
.attr('pointer-events','all')
.attr('stroke','none')
.attr('stroke-wdith',40)
.attr("fill", function(d) { return color(d.group);})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
node.append("title")
.text(d => d.id);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) {return d.id;})
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
function ticked() {
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(event,d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event,d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event,d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
updateGraph(graph.nodes, graph.links);
catMenu.on('change', function(){
var selectedGroup = d3.select(this)
.select("select")
.property("value");
let nodes, links;
if (selectedGroup === 'all') {
nodes = graph.nodes;
links = graph.links;
} else {
const group = parseInt(selectedGroup);
nodes = graph.nodes.filter(n => n.group === group);
console.log(graph.links)
links = graph.links.filter(link => {
const source = nodes.find(n => n.id === link.source.id);
const target = nodes.find(n => n.id === link.target.id);
return source && target;
})
}
updateGraph(nodes, links);
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
I am trying to add new nodes in force directed, but when I call the function QuickSearch(value) then it duplicates the graph.
Can anybody advise me how to add a new node only.
// Graph variables
var w = window.innerWidth;
var h = window.innerHeight;
var svg = d3.select("#svgData"),
scheme = ['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'],
width = +svg.attr(w),
height = +svg.attr(h),
color = d3.scaleOrdinal(d3.schemeCategory20);
//color = d3.scaleOrdinal(scheme);
var info = {
"nodes": [
{"id": "1", "name": "1", "group": 1},
{"id": "2", "name": "2", "group": 1},
{"id": "3", "name": "3", "group": 1},
{"id": "4", "name": "4", "group": 1},
{"id": "5", "name": "5", "group": 1}
],
"links": [
{"source": "1", "target": "2", "value": 1},
{"source": "1", "target": "3", "value": 1},
{"source": "1", "target": "4", "value": 1},
{"source": "1", "target": "5", "value": 1}
]
}
var marker = d3.select("#svgData").append('defs')
.append('marker')
.attr("id", "Triangle")
.attr('viewBox', '-0 -5 10 10')
.attr("refX", 25)
.attr("refY", 0)
.attr("markerUnits", 'userSpaceOnUse')
.attr("orient", 'auto')
.attr("markerWidth", 13)
.attr("markerHeight", 13)
.attr('xoverflow', 'visible')
.append('path')
.attr("d", 'M 0,-5 L 10 ,0 L 0,5');
function QuickSearch(value) {
var new_node = {};
//console.log(info.nodes);
new_node = {"id": value, "name": value, "group": 1};
info.nodes.findIndex(x => x.id == new_node.id) == -1 ? info.nodes.push(new_node) : console.log("object already exists")
createGraph(info);
};
function createGraph(graph) {
// Zoom
var zoom = d3.zoom()
.scaleExtent([0, 10])
.on("zoom", zoomed);
d3.select("#svgData").call(zoom);
function zoomed() {
const currentTransform = d3.event.transform;
container.attr("transform", currentTransform);
}
// Simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).distance(200))
.force("charge", d3.forceManyBody().strength(10).distanceMax(1000))
.force("center", d3.forceCenter(w / 2, h / 2))
.force('collision', d3.forceCollide().radius(30))
var container = svg.append("g");
var link = container.append("g")
.attr("class", "links")
.selectAll("path")
.data(graph.links)
.enter().append("path")
.attr("marker-end", "url(#Triangle)");
var value = d3.select("g.links")
.selectAll("text")
.data(graph.links)
.enter().append("text")
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {
return d.value;
});
var node = container.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr('stroke-width', 3)
.attr('stroke', function(d) { return color(d.group); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", listInfo);
var lable = d3.select(".nodes")
.selectAll("text")
.data(graph.nodes)
.enter().append("text")
.text(function(d) { return d.name; });
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links)
function ticked() {
link
.attr("d", function(d) {
return "M" + d.source.x + "," + d.source.y
+ "C" + d.source.x + "," + (d.source.y + d.target.y) / 2
+ " " + d.target.x + "," + d.target.y
+ " " + d.target.x + "," + d.target.y;
})
.attr("stroke-dasharray", function() {
return this.getTotalLength() - 25;
});
value
.attr("x", function(d) { return (d.source.x + d.target.x)/2; })
.attr("y", function(d) { return (d.source.y + d.target.y)/2; });
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
lable
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y - 30; });
}
function slided(d) {
zoom.scaleTo(svg, d3.select(this).property("value"));
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
d3.select(this).classed("dragging", true);
d.fixed = true;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
//fix_nodes(d);
}
// Preventing other nodes from moving while dragging one node
function fix_nodes(this_node) {
node.each(function(d){
if (this_node != d){
d.fx = d.x;
d.fy = d.y;
}
});
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d3.select(this).classed("dragging", false);
//d.fixed = true;
}
var nodeText;
function listInfo(d) {
d3.select(this)
.select("text")
.text(function(d) { return d.name;})
var nodeText = d.name;
document.getElementById("nodeId").innerHTML = nodeText;
}
};
I tried several ways to use .exit().remove() option, but it doesn't work for me.
I am trying to transform dots along a circular path. Each transform has a different duration. On end the transition start again for all dots once the transform with the shortest speed duration ends. How do I re-start the transition for each dot independently?
const graphWidth = 500;
const graphHeight = 500;
const svg = d3.select("svg")
.attr("width", graphWidth)
.attr("height", graphHeight);
const testData = [{
"country": "Netherlands",
"speed": 4000
},
{
"country": "Belgium",
"speed": 2000
},
{
"country": "Austria",
"speed": 1000
},
{
"country": "Germany",
"speed": 5000
},
{
"country": "USA",
"speed": 4000
}
];
const dots = svg.selectAll('circle')
.data(testData)
.enter()
.append('circle')
.attr('transform', `translate(${graphWidth / 2}, ${graphHeight / 2})`)
.attr('cx', 223)
.attr('r', 5)
.attr('fill', 'yellow')
.attr('stroke', 'black');
function transition() {
svg.selectAll('circle').each(function(d) {
d3.select(this)
.transition()
.ease(d3.easeLinear)
.duration(d.speed)
.attrTween('transform', rotTween)
.on('end', transition);
})
}
transition();
function rotTween() {
var i = d3.interpolate(0, 360);
return function(t) {
return `translate(${graphWidth / 2}, ${graphHeight / 2}) rotate(${i(t)}, 0, 0)`;
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
The easiest way is to change your transition function to handle just one transition of one circle, by taking the .each() out of the function:
const graphWidth = 500;
const graphHeight = 500;
const svg = d3.select("svg")
.attr("width", graphWidth)
.attr("height", graphHeight);
const testData = [{
"country": "Netherlands",
"speed": 4000
},
{
"country": "Belgium",
"speed": 2000
},
{
"country": "Austria",
"speed": 1000
},
{
"country": "Germany",
"speed": 5000
},
{
"country": "USA",
"speed": 4000
}
];
const dots = svg.selectAll('circle')
.data(testData)
.enter()
.append('circle')
.attr('transform', `translate(${graphWidth / 2}, ${graphHeight / 2})`)
.attr('cx', 223)
.attr('r', 5)
.attr('fill', 'yellow')
.attr('stroke', 'black');
function transition(circle, d) {
d3.select(circle)
.transition()
.ease(d3.easeLinear)
.duration(d.speed)
.attrTween('transform', rotTween)
.on('end', function() {
transition(circle, d);
});
}
svg.selectAll('circle').each(function(d) {
transition(this, d);
});
function rotTween() {
var i = d3.interpolate(0, 360);
return function(t) {
return `translate(${graphWidth / 2}, ${graphHeight / 2}) rotate(${i(t)}, 0, 0)`;
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
Alternatively, you can name the otherwise anonymous function you pass to .each() (in this case I named it repeat), and then call that function again in the .on("end" clause
const graphWidth = 500;
const graphHeight = 500;
const svg = d3.select("svg")
.attr("width", graphWidth)
.attr("height", graphHeight);
const testData = [{
"country": "Netherlands",
"speed": 4000
},
{
"country": "Belgium",
"speed": 2000
},
{
"country": "Austria",
"speed": 1000
},
{
"country": "Germany",
"speed": 5000
},
{
"country": "USA",
"speed": 4000
}
];
const dots = svg.selectAll('circle')
.data(testData)
.enter()
.append('circle')
.attr('transform', `translate(${graphWidth / 2}, ${graphHeight / 2})`)
.attr('cx', 223)
.attr('r', 5)
.attr('fill', 'yellow')
.attr('stroke', 'black');
svg.selectAll('circle')
.each(function repeat(d) {
d3.select(this)
.transition()
.ease(d3.easeLinear)
.duration(function(d) { return d.speed; })
.attrTween('transform', rotTween)
.on("end", repeat);
});
function rotTween() {
var i = d3.interpolate(0, 360);
return function(t) {
return `translate(${graphWidth / 2}, ${graphHeight / 2}) rotate(${i(t)}, 0, 0)`;
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
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 am trying to create multi line text as nodes of the directed graph as :
<rect height="27" width="56" rx="100" ry="100" style="fill: #ffffff;"></rect>
<text dx="6" dy="6">
<tspan x="0" dy="15">Donovan</tspan>
<tspan x="0" dy="15">3</tspan>
<tspan x="0" dy="15">what</tspan>
</text>
as seen in: http://jsfiddle.net/nikosdim/S4eaL/1/
I currently have this:
// setting up parameters to be use through rest of the code
var w = 2000;
var h = 800;
var r = 30;
var linkDistance = 100;
var boxHeight = 50;
var boxWidth = 50;
var colors = d3.scale.category10();
// This is what how we should be setting gravity, theta and charge ideally: http://stackoverflow.com/questions/9901565/charge-based-on-size-d3-force-layout
var charge = -5000;
var gravity = 0.3;
var theta = 0.01;
var dataset = {
nodes: [
{ "name": "Adam", "id": "0" },
{ "name": "Bob", "id": "1" },
{ "name": "Carrie", "id": "2" },
{ "name": "Donovan", "id": "3" },
{ "name": "Edward", "id": "4" },
{ "name": "Felicity", "id": "5" },
{ "name": "George", "id": "6" },
{ "name": "Hannah", "id": "7" },
{ "name": "Iris", "id": "8" },
{ "name": "Jerry", "id": "9" }
],
edges: [
{ "source": 0, "target": 4 },
{ "source": 1, "target": 5 },
{ "source": 2, "target": 5 },
{ "source": 2, "target": 5 },
{ "source": 5, "target": 8 },
{ "source": 5, "target": 9 },
{ "source": 6, "target": 7 },
{ "source": 7, "target": 8 },
{ "source": 8, "target": 9 }
]
};
var svg = d3.select("body").append("svg").attr({ "width": w, "height": h });
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w, h])
.linkDistance([linkDistance])
.charge(charge)
.theta(theta)
.gravity(gravity);
var edges = svg.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.attr("id", function (d, i) { return 'edge' + i; })
.attr('marker-end', 'url(#arrowhead)')
.style("stroke", "#ccc")
.style("pointer-events", "none");
var nodes = svg.selectAll("rect")
.data(dataset.nodes)
.enter()
.append("rect")
.attr({ "width": boxWidth })
.attr({ "height": boxHeight })
//.style("fill", function (d, i) { return colors(i); })
.style("fill", 'white')
.attr('stroke', 'black')
.call(force.drag);
var nodelabels = svg.selectAll(".nodelabel")
.data(dataset.nodes)
.enter()
.append("text")
.attr({
//"x": function (d) { return d.x; },
//"y": function (d) { return d.y; },
"class": "nodelabel",
"stroke": "black"
});
var edgepaths = svg.selectAll(".edgepath")
.data(dataset.edges)
.enter()
.append('path')
.attr({
//'d': function (d) { return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; },
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'fill': 'blue',
'stroke': 'red',
'id': function (d, i) { return 'edgepath' + i; }
})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(dataset.edges)
.enter()
.append('text')
.style("pointer-events", "none")
.attr({
'class': 'edgelabel',
'id': function (d, i) { return 'edgelabel' + i; },
'dx': 80,
'dy': 0,
'font-size': 10,
'fill': '#aaa'
});
edgelabels.append('textPath')
.attr('xlink:href', function (d, i) { return '#edgepath' + i; })
.style("pointer-events", "none")
.text(function (d, i) { return 'label ' + i; });
svg.append('defs').append('marker')
.attr({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 25,
'refY': 0,
//'markerUnits':'strokeWidth',
'orient': 'auto',
'markerWidth': 10,
'markerHeight': 10,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#ccc')
.attr('stroke', '#ccc');
force.on("tick", tick).start();
function ConstrainX(point) {
return Math.max(r, Math.min(w - r, point));
}
function ConstrainY(point) {
return Math.max(r, Math.min(h - r, point));
}
function tick(e) {
// Push sources up and targets down to form a weak tree.
var k = 60 * e.alpha;
dataset.edges.forEach(function (d, i) {
d.source.y -= k;
d.target.y += k;
});
edges.attr({
"x1": function (d) { return ConstrainX(d.source.x); },
"y1": function (d) { return ConstrainY(d.source.y); },
"x2": function (d) { return ConstrainX(d.target.x); },
"y2": function (d) { return ConstrainY(d.target.y); }
});
nodes.attr({
"x": function (d) { return ConstrainX(d.x) - boxWidth / 2; },
"y": function (d) { return ConstrainY(d.y) - boxHeight / 2; }
});
// appending boxWidth/2 to make sure the labels are within the box
nodelabels.attr("x", function (d) { return ConstrainX(d.x) - boxWidth / 2; })
.attr("y", function (d) { return ConstrainY(d.y); });
edgepaths.attr('d', function (d) {
var path = 'M ' + ConstrainX(d.source.x) + ' ' + ConstrainY(d.source.y) + ' L ' + ConstrainX(d.target.x) + ' ' + ConstrainY(d.target.y);
//console.log(d)
return path;
});
edgelabels.attr('transform', function (d, i) {
if (d.target.x < d.source.x) {
bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
var insertLinebreaks = function (d) {
var el = d3.select(this);
var name = d.name;
var id = d.id;
el.text('');
//for (var i = 0; i < words.length; i++) {
var tspan = el.append('tspan').text(name);
tspan = el.append('tspan').text(id);
//if (i > 0)
tspan.attr('x', 0);
tspan.attr('dy', '15');
tspan = el.append('tspan').text('what');
tspan.attr('x', '0');
tspan.attr('dy', '15');
//}
};
nodelabels.each(insertLinebreaks); <== Insert new lines
}
But this is messing new lines in the nodes. Once I insert the new lines the text shows up left aligned at the start of the screen. This is not what I want. I wanted the text to be aligned in the node as shown in the first image.
This is the output using the above code:
http://jsfiddle.net/6afc2vp8/
// **********start configuration***************
var dataset = {
"Nodes": [
{
"ID": 0,
"Identity": "af12689c-de83-4a0d-a63d-1f548fd02e26",
"RoleInstance": "RoleInstance",
"LogonID": "LogonID1",
"WeightedScore": 120,
"AccountInstance": "AccountInstance1",
"MinTimestampPst": "2014-11-19T17:08:46.6797242-05:00"
},
{
"ID": 1,
"Identity": "a5bd36db-00e6-4492-92d7-49278f0046a7",
"RoleInstance": "RoleInstance2",
"LogonID": "LogonID2",
"WeightedScore": 100,
"AccountInstance": "AccountInstance2",
"MinTimestampPst": "2014-11-19T17:08:46.6797242-05:00"
},
{
"ID": 2,
"Identity": "a5bd36db-00e6-4492-92d7-49278f0046a7",
"RoleInstance": "RoleInstance2",
"LogonID": "LogonID2",
"WeightedScore": 100,
"AccountInstance": "AccountInstance2",
"MinTimestampPst": "2014-11-19T17:08:46.6797242-05:00"
},
{
"ID": 3,
"Identity": "a5bd36db-00e6-4492-92d7-49278f0046a7",
"RoleInstance": "RoleInstance2",
"LogonID": "LogonID2",
"WeightedScore": 100,
"AccountInstance": "AccountInstance2",
"MinTimestampPst": "2014-11-19T17:08:46.6797242-05:00"
}
],
"Edges": [
{
"source": 0,
"target": 1
}, {
"source": 3,
"target": 2
}
]
};
// select all the features from the node that you want in the output
var nodelabelslist = ["ID", "Identity", "RoleInstance", "LogonID", "WeightedScore", "AccountInstance", "MinTimestampPst"];
var nodelabelslistlength = nodelabelslist.length;
// this is the rectangle height and width
var boxHeight = nodelabelslistlength * 20;
var boxWidth = 400;
var nodelabelverticaloffset = 15;
var nodelabelhorizontaloffset = 5;
var labelStartPos = -(nodelabelslistlength/2 - 1);
// setting up d3js parameters to be use through rest of the code
var w = 2000;
var h = 800;
var r = 30;
var linkDistance = 200;
var colors = d3.scale.category10();
// This is what how we should be setting gravity, theta and charge ideally: http://stackoverflow.com/questions/9901565/charge-based-on-size-d3-force-layout
var charge = -6000;
var gravity = 0.10;
var theta = 0.01;
// **********end configuration***************
var jsonNodes = dataset.Nodes;
var jsonEdges = dataset.Edges;
var svg = d3.select("body").append("svg").attr({ "width": w, "height": h });
var force = d3.layout.force()
.nodes(jsonNodes)
.links(jsonEdges)
.size([w, h])
.linkDistance([linkDistance])
.charge(charge)
.theta(theta)
.gravity(gravity);
var edges = svg.selectAll("line")
.data(jsonEdges)
.enter()
.append("line")
.attr("id", function (d, i) { return 'edge' + i; })
.attr('marker-end', 'url(#arrowhead)')
.style("stroke", "#000")
.style("pointer-events", "none");
var nodes = svg.selectAll("rect")
.data(jsonNodes)
.enter()
.append("rect")
//.style("fill", function (d, i) { return colors(i); })
.style("fill", 'none')
.attr('stroke', 'black')
.call(force.drag);
var nodelabelsarr = [];
for (var index = 0; index < nodelabelslist.length; ++index) {
var nodelabels = svg.selectAll(".nodelabel" + index)
.data(jsonNodes)
.enter()
.append("text")
.attr({
"class": "nodelabel",
"stroke": "black"
})
.text(function (d) { return nodelabelslist[index] + ": " + d[nodelabelslist[index]]; });
nodelabelsarr[index] = nodelabels;
}
var edgepaths = svg.selectAll(".edgepath")
.data(jsonEdges)
.enter()
.append('path')
.attr({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'fill': 'blue',
'stroke': 'red',
'id': function (d, i) { return 'edgepath' + i; }
})
.style("pointer-events", "none");
var edgelabels = svg.selectAll(".edgelabel")
.data(jsonEdges)
.enter()
.append('text')
.style("pointer-events", "none")
.attr({
'class': 'edgelabel',
'id': function (d, i) { return 'edgelabel' + i; },
'dx': 80,
'dy': 0,
'font-size': 10,
'fill': '#000'
});
edgelabels.append('textPath')
.attr('xlink:href', function (d, i) { return '#edgepath' + i; })
.style("pointer-events", "none")
.text(function (d, i) { return 'label ' + i; });
svg.append('defs').append('marker')
.attr({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 25,
'refY': 0,
//'markerUnits':'strokeWidth',
'orient': 'auto',
'markerWidth': 10,
'markerHeight': 10,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#000')
.attr('stroke', '#000');
force.on("tick", tick).start();
function ConstrainX(point) {
return Math.max(r, Math.min(w - r, point));
}
function ConstrainY(point) {
return Math.max(r, Math.min(h - r, point));
}
function edgeArc(d) {
var path = 'M ' + ConstrainX(d.source.x) + ' ' + ConstrainY(d.source.y) + ' L ' + ConstrainX(d.target.x) + ' ' + ConstrainY(d.target.y);
//console.log(d)
return path;
}
function edgelabeltransform(d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
var rx = bbox.x + bbox.width / 2;
var ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
}
function tick(e) {
// Push sources up and targets down to form a weak tree.
var k = 60 * e.alpha;
jsonEdges.forEach(function (d) {
d.source.y -= k;
d.target.y += k;
});
edges.attr({
"x1": function (d) { return ConstrainX(d.source.x); },
"y1": function (d) { return ConstrainY(d.source.y); },
"x2": function (d) { return ConstrainX(d.target.x); },
"y2": function (d) { return ConstrainY(d.target.y); }
});
nodes.attr({
"x": function (d) { return ConstrainX(d.x) - boxWidth / 2; },
"y": function (d) { return ConstrainY(d.y) - boxHeight / 2; }
})
.attr({ "width": boxWidth })
.attr({ "height": boxHeight });
// appending boxWidth/2 to make sure the labels are within the box
var startOffset = labelStartPos;
for (var index = 0; index < nodelabelsarr.length; index++) {
nodelabelsarr[index].attr("x", function (d) { return ConstrainX(d.x) - boxWidth / 2 + nodelabelhorizontaloffset; })
.attr("y", function (d) { return ConstrainY(d.y) + (nodelabelverticaloffset * startOffset); });
startOffset++;
}
edgepaths.attr('d', edgeArc);
edgelabels.attr('transform', edgelabeltransform);
}