How to render a general lattice with D3JS? - d3.js

I am looking to an algorithm or layout to be implemented in D3JS to render a graph of a "general" lattice, a lattice that is not (necessarily) binomial.
An example of the kind of result I would like to obtain is:
I want to visualize the lattice as a hierarchy from the bottom to the top, similar to a tree-styled structure.
I tried to adapt a force layout, but this worked badly, especially because it was hard to locate the levels of the hierarchy properly.
I don't have any constraint about the form of the input, that it can be the more appropriate for the procedure.

You could do something like this, but I'm not sure how useful it would be. It makes use of the cola.js library.
// Sample data set
var json = {
nodes: [
{ "name": 'Beverage' },
{ "name": 'nonAlcoholic' },
{ "name": 'sparkling' },
{ "name": 'alcoholic' },
{ "name": 'hot' },
{ "name": 'caffeinic' },
{ "name": 'MineralWater' },
{ "name": 'madeFromGrain' },
{ "name": 'madeFromGrapes' },
{ "name": 'HerbTea' },
{ "name": 'Coffee' },
{ "name": 'Cola' },
{ "name": 'Beer' },
{ "name": 'Wine' },
{ "name": 'Champagne' },
{ "name": '' }
],
links: [
{ "source": 0, "target": 1},
{ "source": 0, "target": 2},
{ "source": 0, "target": 3},
{ "source": 1, "target": 4},
{ "source": 1, "target": 5},
{ "source": 1, "target": 6},
{ "source": 2, "target": 6},
{ "source": 2, "target": 7},
{ "source": 2, "target": 14},
{ "source": 3, "target": 7},
{ "source": 3, "target": 8},
{ "source": 4, "target": 9},
{ "source": 4, "target": 10},
{ "source": 5, "target": 10},
{ "source": 5, "target": 11},
{ "source": 6, "target": 11},
{ "source": 7, "target": 12},
{ "source": 8, "target": 13},
{ "source": 8, "target": 13},
{ "source": 13, "target": 14},
{ "source": 10, "target": 15},
{ "source": 11, "target": 15},
{ "source": 12, "target": 15},
{ "source": 14, "target": 15},
]
};
var width = 800, height = 600, n = 10000;
var svg = d3.select('#vis').append('svg').attr('width', width).attr('height', height);
var force = cola.d3adaptor()
.linkDistance(80)
.size([width, height])
.nodes(json.nodes)
.links(json.links)
.flowLayout("y", 25)
.on("tick", tick)
.start();
var node = svg.selectAll("circle.node")
.data(json.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5);
var text = svg.selectAll("text.label")
.data(json.nodes)
.enter().append("text")
.attr("class", "label")
.text(function(d) { return d.name; });
var link = svg.selectAll("line.link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
function tick() {
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; });
text.attr("x", function(d) { return d.x + 8; })
.attr("y", function(d) { return d.y; });
}
function hover(d) {
switch(d3.event.type) {
case "mouseover": var tip = svg.append("g")
.attr("class", "tip")
.attr("transform", "translate(" + d3.event.x + "," + d3.event.y + ")");
var rect = tip.append("rect")
.attr("fill", "white");
var text = tip.append("text")
.text(d.name);
var bounds = text.node().getBBox();
rect.attr("x", bounds.x)
.attr("y", bounds.y)
.attr("width", bounds.width)
.attr("height", bounds.height);
break;
case "mouseout": svg.selectAll("g.tip")
.remove();
break;
default: break;
}
}
text {
font: 10px sans-serif;
}
line {
stroke: #000;
stroke-width: 1.5px;
}
circle {
stroke: #fff;
stroke-width: 1.5px;
}
<div id="vis"></div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://marvl.infotech.monash.edu/webcola/cola.v3.min.js"></script>
Using cola.js you can specify additional constraints to assist with laying things out correctly. As it stands, the only constraint that I've placed on it is a flowLayout parameter. The good thing about this is the layout is repeatable and achieving a similar result with a d3 force layout may be quite difficult (although, in fairness, it's not really intended to be used that way).
I wouldn't claim that this is a great result, but short of manually determining node positions, or accepting a standard d3 force layout, this might be among the easiest and quickest methods to get something approaching what you're after.
Edit: showing how the hover was used.
// Sample data set
var json = {
nodes: [
{ "name": 'Beverage' },
{ "name": 'nonAlcoholic' },
{ "name": 'sparkling' },
{ "name": 'alcoholic' },
{ "name": 'hot' },
{ "name": 'caffeinic' },
{ "name": 'MineralWater' },
{ "name": 'madeFromGrain' },
{ "name": 'madeFromGrapes' },
{ "name": 'HerbTea' },
{ "name": 'Coffee' },
{ "name": 'Cola' },
{ "name": 'Beer' },
{ "name": 'Wine' },
{ "name": 'Champagne' },
{ "name": '' }
],
links: [
{ "source": 0, "target": 1},
{ "source": 0, "target": 2},
{ "source": 0, "target": 3},
{ "source": 1, "target": 4},
{ "source": 1, "target": 5},
{ "source": 1, "target": 6},
{ "source": 2, "target": 6},
{ "source": 2, "target": 7},
{ "source": 2, "target": 14},
{ "source": 3, "target": 7},
{ "source": 3, "target": 8},
{ "source": 4, "target": 9},
{ "source": 4, "target": 10},
{ "source": 5, "target": 10},
{ "source": 5, "target": 11},
{ "source": 6, "target": 11},
{ "source": 7, "target": 12},
{ "source": 8, "target": 13},
{ "source": 8, "target": 13},
{ "source": 13, "target": 14},
{ "source": 10, "target": 15},
{ "source": 11, "target": 15},
{ "source": 12, "target": 15},
{ "source": 14, "target": 15},
]
};
var width = 800, height = 600, n = 10000;
var svg = d3.select('#vis').append('svg').attr('width', width).attr('height', height);
var force = cola.d3adaptor()
.linkDistance(80)
.size([width, height])
.nodes(json.nodes)
.links(json.links)
.flowLayout("y", 25)
.on("tick", tick)
.start();
var link = svg.selectAll("line.link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll("circle.node")
.data(json.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.on("mouseover", hover)
.on("mouseout", hover);
function tick() {
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 hover(d) {
switch(d3.event.type) {
case "mouseover":
console.log(d3.event);
var tip = svg.append("g")
.attr("class", "tip")
.attr("transform", "translate(" + d3.event.clientX + "," + d3.event.clientY + ")");
var rect = tip.append("rect")
.attr("fill", "white");
var text = tip.append("text")
.text(d.name);
var bounds = text.node().getBBox();
rect.attr("x", bounds.x)
.attr("y", bounds.y)
.attr("width", bounds.width)
.attr("height", bounds.height);
break;
case "mouseout": svg.selectAll("g.tip")
.remove();
break;
default: break;
}
}
text {
font: 10px sans-serif;
}
line {
stroke: #000;
stroke-width: 1.5px;
}
circle {
stroke: #fff;
stroke-width: 1.5px;
}
<div id="vis"></div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="http://marvl.infotech.monash.edu/webcola/cola.v3.min.js"></script>

Related

Updating nodes and links with labels in d3 force directed network graph is not removing the nodes properly

I am trying to create a d3 force directed network graph where based on a given network I can update the network by modifying the links and nodes and re-update them in the svg.
I tweaked the code so that a g element can be created which enclose each node circle so that I can add a text inside that same g element.
But now the labels are working perfectly but when I transition from graph 3 to graph 1 by clicking button 3 first then clicking button 1, the links that are redundant (a->d, a->f) are removed perfectly but the nodes that are redundant (e and f) stays in the svg.
I could not figure out whether it was a wrong selection or do I need some tweaking in the tick() function?
Here is the code:
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
var circles = nodeElements.append("circle")
.attr("r", 8);
var labels = nodeElements.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.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; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).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) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>
Here is the jsfiddle version of the code: https://jsfiddle.net/syedarehaq/myd0h5w1/
In the provided code, variable nodeElements contains the enter selection, rather than the whole data binding selection.
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
.enter().append("g")
.attr("class", "node");
nodeElements declaration should not contain the .enter bit - it should be just like linkSelection variable declaration:
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
Then, in order to append the new circles and texts only to the entering g elements, adapt as follows:
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
The exit function call is now working as expected.
Demo in the snippet below.
var height = 200;
var width = 200;
const graph = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 }
]
}
const graph2 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 }
]
}
const graph3 = {
"nodes": [
{ "name": "a", "group": 1 },
{ "name": "b", "group": 2 },
{ "name": "c", "group": 3 },
{ "name": "d", "group": 4 },
{ "name": "e", "group": 4 },
{ "name": "f", "group": 4 }
],
"links": [
{ "source": "a", "target": "b", "value": 1 },
{ "source": "b", "target": "c", "value": 1 },
{ "source": "c", "target": "d", "value": 1 },
{ "source": "a", "target": "d", "value": 1 },
{ "source": "f", "target": "a", "value": 1 }
]
}
var simulation = d3.forceSimulation()
.force("ct", d3.forceCenter(height / 2, width / 2))
.force("link", d3.forceLink().id(function(d) { return d.name; })
.distance(50).strength(2))
.force("charge", d3.forceManyBody().strength(-240))
// use forceX and forceY instead to change the relative positioning
// .force("centering", d3.forceCenter(width/2, height/2))
.force("x", d3.forceX(width / 2))
.force("y", d3.forceY(height / 2))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g").attr("class", "links");
svg.append("g").attr("class", "nodes");
function start(graph) {
var linkElements = svg.select(".links").selectAll(".link").data(graph.links);
linkElements.enter().append("line").attr("class", "link");
linkElements.exit().remove();
var nodeElements = svg.select(".nodes").selectAll(".node")
.data(graph.nodes, function(d) { return d.name })
var enterSelection = nodeElements.enter().append("g")
.attr("class", "node");
var circles = enterSelection.append("circle")
.attr("r", 8);
var labels = enterSelection.append("text")
.text(function(d) { return d.name; })
.attr("x", 10)
.attr("y", 10);
nodeElements.exit().remove();
simulation.nodes(graph.nodes);
simulation.force("link").links(graph.links);
simulation.alphaTarget(0.1).restart();
}
function tick() {
var nodeElements = svg.select(".nodes").selectAll(".node");
var linkElements = svg.select(".links").selectAll(".link");
nodeElements.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
linkElements.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; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).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) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
start(graph);
document.getElementById('btn1').addEventListener('click', function() {
start(graph);
});
document.getElementById('btn2').addEventListener('click', function() {
start(graph2);
});
document.getElementById('btn3').addEventListener('click', function() {
start(graph3);
});
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
stroke-width: 1.5px;
}
text {
font-family: sans-serif;
font-size: 10px;
fill: #000000;
}
<body>
<div>
<button id='btn1'>1</button>
<button id='btn2'>2</button>
<button id='btn3'>3</button>
</div>
</body>
<script src="https://d3js.org/d3.v5.min.js"></script>

Which concepts and techniques do I need to achieve this kind of graph

I want to draw a dynamic visual of how certain functional blocks interact with each other. I'm having difficulty defining what kind of graph I need, and what (if any) d3 layout function is best suited to start with. I'm not even sure my example falls within the definition of a graph.
The general idea is to visualize a group of functions with their inputs and outputs. It starts with a set of inputs, and ends with a set of outputs. In between there are several functions who each take inputs and generate 1 or more outputs. Each output can serve as input for one or more functions. So each edge/line represents an output being transferred to a function to serve as input (and is uni-directional)
I'm not looking for answer in code, but rather an insight on which concepts I need to start with. The image probably has issues and can't be achieved literally, but I can't point out what exactly.
const nodes = [
{
id: 1,
title: 'Function A',
x: 100,
y: 25,
points: [
{
id: 11,
dx: 50,
dy: 0
},
{
id: 12,
dx: 0,
dy: 20
}
]
},
{
id: 2,
title: 'Function B',
x: 300,
y: 100,
points: [
{
id: 21,
dx: -50,
dy: 0
},
{
id: 22,
dx: 0,
dy: 20
}
]
},
{
id: 3,
title: 'Function C',
x: 170,
y: 160,
points: [
{
id: 31,
dx: 0,
dy: -20
},
{
id: 32,
dx: 50,
dy: 0
}
]
}
];
const links = [
{source: 11, target:21},
{source: 12, target:31},
{source: 22, target:32}
];
var selectedNode = null;
const renderNodes = svg => {
const allNodes = svg.selectAll('.node').data(nodes, node => node.id);
const addedNodes = allNodes.enter()
.append('g')
.style('cursor', 'pointer')
.attr('transform', d => `translate(${d.x},${d.y})`)
.on('click', d => {
selectedNode = d.id;
renderAll();
});
addedNodes.append('rect')
.attr('width', 100)
.attr('height', 40)
.attr('x', -50)
.attr('y', -20)
.attr('rx', 5);
addedNodes.append('text')
.text(d => d.title)
.style('fill', 'white')
.attr('y', 6)
.attr('text-anchor', 'middle');
addedNodes.merge(allNodes)
.select('rect')
.style('fill', d => (d.id === selectedNode) ? 'blue' : 'black');
allNodes.exit().remove();
};
const renderConnectionPoints = (svg, points) => {
const allPoints = svg.selectAll('.point').data(points, point => point.id);
const addedPoints = allPoints.enter()
.append('g')
.style('cursor', 'pointer')
.attr('transform', d => `translate(${d.x},${d.y})`);
addedPoints.append('circle')
.attr('r', 4)
.style('fill', 'white');
addedPoints.merge(allPoints)
.select('circle')
.style('stroke', d => (d.parentId === selectedNode) ? 'blue' : 'black');
allPoints.exit().remove();
}
const renderLinks = (svg, _links) => {
const linkPath = d => `M ${d.source.x}, ${d.source.y}
C ${(d.source.x + d.target.x) / 2}, ${d.source.y}
${(d.source.x + d.target.x) / 2}, ${d.target.y}
${d.target.x}, ${d.target.y}`;
const allLinks = svg.selectAll('.link').data(_links, link => link.id);
const addedLinks = allLinks.enter()
.append('path')
.style('fill', 'none')
.attr('d', linkPath);
addedLinks.merge(allLinks)
.style('stroke', d => (d.source.parentId === selectedNode ||
d.target.parentId === selectedNode) ? 'blue' : 'lightgray');
allLinks.exit().remove();
}
const getPoints = () => nodes.reduce((all, node) => {
node.points.forEach(point => all.push({
id: point.id, x: node.x + point.dx, y: node.y + point.dy, parentId: node.id}));
return all;
}, []);
const getLinks = points => links.map(link => {
const source = points.find(point => point.id === link.source);
const target = points.find(point => point.id === link.target);
return {source, target};
});
const renderAll = () => {
const svg = d3.select('svg');
const points = getPoints();
const _links = getLinks(points);
renderNodes(svg);
renderLinks(svg, _links);
renderConnectionPoints(svg, points);
}
renderAll();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width='400' height='180'>
</svg>
Hi,
Take a look at the snippet.
The data model is very simple: nodes, their connection points and links
Just provide your data and use the snippet's code to render it.
Good luck and feel free to ask any question :)
sorry by my bad english, I speak spanish...
I also look for something similar, researching I have achieved this, and I am missing:
Transform nodes circles into rectangles
Add text labels to each node and each link
Once everything is organized initially, they remain in a fixed location
Capture the click on each node and / or link to trigger another application
I leave the code below, observe especially the definition of nodes_data and links_data, and functions to define size and color:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: black ;
stroke-width: 0px;
}
</style>
<svg width="1000" height="600"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
//create somewhere to put the force directed graph
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
//var radius = 15;
var nodes_data = [
{"name": "PO1", "entity": "PO"},
{"name": "PO2", "entity": "PO"},
{"name": "PO3", "entity": "PO"},
{"name": "PO4", "entity": "PO"},
{"name": "PO5", "entity": "PO"},
{"name": "PO6", "entity": "PO"},
{"name": "PO7", "entity": "PO"},
{"name": "PY1", "entity": "PY"},
{"name": "PY2", "entity": "PY"},
{"name": "L1", "entity": "X"},
{"name": "L2", "entity": "X"},
{"name": "L3", "entity": "X"},
{"name": "L4", "entity": "X"},
{"name": "TK1", "entity": "TK"},
{"name": "TK2", "entity": "TK"},
{"name": "PIL1", "entity": "TK"},
{"name": "BBA1", "entity": "BA"},
{"name": "BBA2", "entity": "BA"},
{"name": "ULAC1", "entity": "UL"},
{"name": "VtaYPF", "entity": "VTA"}
]
//Sample links data
//type: A for Ally, E for Enemy
var links_data = [
{"source": "PO1", "target": "L1", "type":"A" },
{"source": "PO2", "target": "L1", "type":"A" },
{"source": "PO3", "target": "L1", "type":"A"},
{"source": "PO4", "target": "L2", "type":"A"},
{"source": "PO5", "target": "L2", "type":"A"},
{"source": "PO6", "target": "L3", "type":"A"},
{"source": "PO7", "target": "L3", "type":"A"},
{"source": "L1", "target": "L3", "type":"A"},
{"source": "L2", "target": "L3", "type":"A"},
{"source": "L3", "target": "TK1", "type":"A"},
{"source": "L3", "target": "TK2", "type":"A"},
{"source": "TK1", "target": "L4", "type":"A"},
{"source": "TK2", "target": "L4", "type":"A"},
{"source": "L4", "target": "PIL1", "type":"A"},
{"source": "PIL1", "target": "ULAC1", "type":"A"},
{"source": "PIL1", "target": "BBA1", "type":"A"},
{"source": "PIL1", "target": "BBA2", "type":"A"},
{"source": "ULAC1", "target": "VtaYPF", "type":"A"},
{"source": "BBA1", "target": "PY1", "type":"A"},
{"source": "BBA2", "target": "PY2", "type":"A"}
]
//set up the simulation and add forces
var simulation = d3.forceSimulation()
.nodes(nodes_data);
var link_force = d3.forceLink(links_data)
.id(function(d) { return d.name; });
var charge_force = d3.forceManyBody()
.strength(-100);
var center_force = d3.forceCenter(width / 2, height / 2);
simulation
.force("charge_force", charge_force)
.force("center_force", center_force)
.force("links",link_force)
;
//add tick instructions:
simulation.on("tick", tickActions );
//add encompassing group for the zoom
var g = svg.append("g")
.attr("class", "everything");
//draw lines for the links
var link = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(links_data)
.enter().append("line")
.attr("stroke-width", 2)
.style("stroke", linkColour);
//draw circles for the nodes
var node = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes_data)
.enter()
.append("circle")
.attr("r", radius)
.attr("fill", circleColour);
//add drag capabilities
var drag_handler = d3.drag()
.on("start", drag_start)
.on("drag", drag_drag)
.on("end", drag_end);
drag_handler(node);
//add zoom capabilities
var zoom_handler = d3.zoom()
.on("zoom", zoom_actions);
zoom_handler(svg);
/** Functions **/
//Function to choose what color circle we have
//Let's return blue for males and red for females
function circleColour(d){
var my_color
switch (d.entity) {
case "PO": my_color = "black"; break;
case "PY": my_color = "cyan"; break;
case "TK": my_color = "blue"; break;
case "UL": my_color = "green"; break;
case "VTA": my_color = "green"; break;
case "BA": my_color = "cyan"; break;
case "X": my_color = "grey"; break;
}
return my_color
}
//Function to choose the line colour and thickness
//If the link type is "A" return green
//If the link type is "E" return red
function linkColour(d){
if(d.type == "A"){
return "green";
} else {
return "red";
}
}
//Drag functions
//d is the node
function drag_start(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
//make sure you can't drag the circle outside the box
function drag_drag(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function drag_end(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
//Zoom functions
function zoom_actions(){
g.attr("transform", d3.event.transform)
}
function tickActions() {
//update circle positions each tick of the simulation
node
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
//update link positions
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; });
}
function radius(d){
var my_rad
switch (d.entity) {
case "PO": my_rad = 10; break;
case "PY": my_rad = 10; break;
case "TK": my_rad = 20; break;
case "UL": my_rad = 20; break;
case "VTA": my_rad = 15; break;
case "BA": my_rad = 10; break;
case "X": my_rad = 3; break;
}
return my_rad
}
</script>
The above code generate this live image

D3 Tree layout visualization - Inherit child with multiple parents

I'm fresh bee for D3-visulization. Currently working with creating D3 tree layout visualization for data lineage. In a data lineage flow, a child node can be derived from more than one parent. Here is the example. In below example, a 'DevLead' may work with 2 managers.
var data = [
{ "name": "Director", "parent": "null", "depth": 0 },
{ "name": "Manager1", "parent": "Director", "depth": 1 },
{ "name": "Manager2", "parent": "Director", "depth": 1 },
{ "name": "DevLead", "parent": "Manager1", "depth": 2 },
{ "name": "DevLead", "parent": "Manager2", "depth": 2 }
];
Getting output refer below image attached.
I'd like to see 'DevLead' children should show only one, and there should be a derivation from 'Manager1' and 'Manager2'. Could any one help with this.
D3 Tree Layout does not exactly supports multiple parents
What Can you do?
Use network graph instead - downside is that node positioning is
hard
I had similar requirements and tried building network graph similar
with tree layout, but when there are many nodes, it gets messy ...
you can check it on
codepen
use hack on tree layout - draw additional link from other node
check this example
another hack using hidden nodes - jsfiddle
Also, I think, these links will help you further :
Family Tree in d3.js
d-tree library - data with multiple parents
If you go with first option, here, you can play with this snippet by removing and adding nodes in data
<!DOCTYPE html>
<html >
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/x-icon" href="https://production-assets.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" />
<link rel="mask-icon" type="" href="https://production-assets.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" />
<title>CodePen - A Pen by dato</title>
</head>
<body translate="no" >
<script src='https://d3js.org/d3.v3.min.js'></script>
<script>
var width = window.innerWidth - 20,
height = window.innerHeight - 20,
radius = 30;
var min_zoom = 0.1;
var max_zoom = 7;
var zoom = d3.behavior.zoom().scaleExtent([min_zoom, max_zoom])
var fill = d3.scale.category20();
var force = d3.layout.force()
.charge(-8000)
.linkDistance(200)
.size([width, height]);
force.drag().on("dragstart", dragstarted)
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var chart = svg.append('g');
var json = {
"nodes": [{
"name": "node0"
}, {
"name": "node1"
}, {
"name": "node2"
}, {
"name": "node3"
}, {
"name": "node4"
}, {
"name": "node5"
}, {
"name": "node6"
}, {
"name": "node7"
}, {
"name": "node8"
}, {
"name": "node9"
}, {
"name": "node10"
}, {
"name": "node11"
}, {
"name": "node12"
}, {
"name": "node13"
}, {
"name": "node14"
}, {
"name": "node15"
}, {
"name": "node16"
}, {
"name": "node17"
}, {
"name": "node18"
}, {
"name": "node19"
}, {
"name": "node20"
}, {
"name": "node21"
}, {
"name": "node22"
}, {
"name": "node23"
}, {
"name": "node24"
}, {
"name": "node25"
}, {
"name": "node26"
}, {
"name": "node27"
}, {
"name": "node28"
}, {
"name": "node29"
}, {
"name": "node30"
}, {
"name": "node31"
}, {
"name": "node32"
}, {
"name": "node33"
}, {
"name": "node34"
}, {
"name": "node35"
}, {
"name": "node36"
}, {
"name": "node37"
}, {
"name": "node38"
}, {
"name": "node39"
}, {
"name": "node40"
}, {
"name": "node41"
}, {
"name": "node42"
}, {
"name": "node43"
}, {
"name": "node44"
}, {
"name": "node45"
}, {
"name": "node46"
}, {
"name": "node47"
}, {
"name": "node48"
}, {
"name": "node49"
}, {
"name": "node50"
}, {
"name": "node51"
}, {
"name": "node52"
}, {
"name": "node53"
}, {
"name": "node54"
}, {
"name": "node55"
}, {
"name": "node56"
}, {
"name": "node57"
}, {
"name": "node58"
}, {
"name": "node59"
}, {
"name": "node60"
}, {
"name": "node61"
}, {
"name": "node62"
}, {
"name": "node63"
}, {
"name": "node64"
}, {
"name": "node65"
}, {
"name": "node66"
}, {
"name": "node67"
}, {
"name": "node68"
}, {
"name": "node69"
}, {
"name": "node70"
}, {
"name": "node71"
}, {
"name": "node72"
}, {
"name": "node73"
}, {
"name": "node74"
}, {
"name": "node75"
}, {
"name": "node76"
}, {
"name": "node77"
}, {
"name": "node78"
}, {
"name": "node79"
}, {
"name": "node80"
}, {
"name": "node81"
}, {
"name": "node82"
}, {
"name": "node83"
}, {
"name": "node84"
}, {
"name": "node85"
}, {
"name": "node86"
}, {
"name": "node87"
}, {
"name": "node88"
}, {
"name": "node89"
}, {
"name": "node90"
}, {
"name": "node91"
}, {
"name": "node92"
}, {
"name": "node93"
}, {
"name": "node94"
}, {
"name": "node95"
}, {
"name": "node96"
}, {
"name": "node97"
}, {
"name": "node98"
}, {
"name": "node99"
}],
"links": [ {
"source": 0,
"target": 1
}, {
"source": 0,
"target": 2
}, {
"source": 1,
"target": 3
}, {
"source": 1,
"target": 4
}, {
"source": 2,
"target": 5
}, {
"source": 2,
"target": 6
}, {
"source": 3,
"target": 7
}, {
"source": 3,
"target": 8
}, {
"source": 4,
"target": 9
}, {
"source": 4,
"target": 10
}, {
"source": 5,
"target": 11
}, {
"source": 5,
"target": 12
}, {
"source": 6,
"target": 13
}, {
"source": 6,
"target": 14
}, {
"source": 7,
"target": 15
}, {
"source": 7,
"target": 16
}, {
"source": 8,
"target": 17
}, {
"source": 8,
"target": 18
}, {
"source": 9,
"target": 19
}, {
"source": 9,
"target": 20
}, {
"source": 10,
"target": 21
}, {
"source": 10,
"target": 22
}, {
"source": 11,
"target": 23
}, {
"source": 11,
"target": 24
}, {
"source": 12,
"target": 25
}, {
"source": 12,
"target": 26
}, {
"source": 13,
"target": 27
}, {
"source": 13,
"target": 28
}, {
"source": 14,
"target": 29
}, {
"source": 14,
"target": 30
}, {
"source": 15,
"target": 31
}, {
"source": 15,
"target": 32
}, {
"source": 16,
"target": 33
}, {
"source": 16,
"target": 34
}, {
"source": 17,
"target": 35
}, {
"source": 17,
"target": 36
}, {
"source": 18,
"target": 37
}, {
"source": 18,
"target": 38
}, {
"source": 19,
"target": 39
}, {
"source": 19,
"target": 40
}, {
"source": 20,
"target": 41
}, {
"source": 20,
"target": 42
}, {
"source": 21,
"target": 43
}, {
"source": 21,
"target": 44
}, {
"source": 22,
"target": 45
}, {
"source": 22,
"target": 46
}, {
"source": 23,
"target": 47
}, {
"source": 23,
"target": 48
}, {
"source": 24,
"target": 49
}, {
"source": 24,
"target": 50
}, {
"source": 25,
"target": 51
}, {
"source": 25,
"target": 52
}, {
"source": 26,
"target": 53
}, {
"source": 26,
"target": 54
}, {
"source": 27,
"target": 55
}, {
"source": 27,
"target": 56
}, {
"source": 28,
"target": 57
}, {
"source": 28,
"target": 58
}, {
"source": 29,
"target": 59
}, {
"source": 29,
"target": 60
}, {
"source": 30,
"target": 61
}, {
"source": 30,
"target": 62
}, {
"source": 31,
"target": 63
}, {
"source": 31,
"target": 64
}, {
"source": 32,
"target": 65
}, {
"source": 32,
"target": 66
}, {
"source": 33,
"target": 67
}, {
"source": 33,
"target": 68
}, {
"source": 34,
"target": 69
}, {
"source": 34,
"target": 70
}, {
"source": 35,
"target": 71
}, {
"source": 35,
"target": 72
}, {
"source": 36,
"target": 73
}, {
"source": 36,
"target": 74
}, {
"source": 37,
"target": 75
}, {
"source": 37,
"target": 76
}, {
"source": 38,
"target": 77
}, {
"source": 38,
"target": 78
}, {
"source": 39,
"target": 79
}, {
"source": 39,
"target": 80
}, {
"source": 40,
"target": 81
}, {
"source": 40,
"target": 82
}, {
"source": 41,
"target": 83
}, {
"source": 41,
"target": 84
}, {
"source": 42,
"target": 85
}, {
"source": 42,
"target": 86
}, {
"source": 43,
"target": 87
}, {
"source": 43,
"target": 88
}, {
"source": 44,
"target": 89
}, {
"source": 44,
"target": 90
}, {
"source": 45,
"target": 91
}, {
"source": 45,
"target": 92
}, {
"source": 46,
"target": 93
}, {
"source": 46,
"target": 94
}, {
"source": 47,
"target": 95
}, {
"source": 47,
"target": 96
}, {
"source": 48,
"target": 97
}, {
"source": 48,
"target": 98
}, {
"source": 49,
"target": 99
},{
"source": 0,
"target": 99
}]
}
var link = chart.selectAll("line")
.data(json.links)
.enter()
.append("line")
.attr("stroke", function(d) {
return 'blue'
})
var node = chart.selectAll("circle")
.data(json.nodes)
.enter().append("circle")
.attr("r", radius - .75)
.style("fill", function(d) {
return fill(d.group);
})
.style("stroke", function(d) {
return d3.rgb(fill(d.group)).darker();
})
.on('mouseover', d => console.log(d))
.call(force.drag);
function dragstarted() {
d3.event.sourceEvent.stopPropagation();
}
zoom.on("zoom", function(d) {
var evt = d3.event;
debugger;
/*
var dcx = (window.innerWidth/2-d.x*zoom.scale());
var dcy = (window.innerHeight/2-d.y*zoom.scale());
*/
var dcx = evt.translate[0]
var dcy = evt.translate[1]
zoom.translate([dcx, dcy]);
chart.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");;
});
force
.nodes(json.nodes)
.links(json.links)
.on("tick", tick)
.start();
svg.call(zoom)
function tick(e) {
console.log(e)
var k = 6 * e.alpha;
// Push sources up and targets down to form a weak tree.
link
.each(function(d,i) {
d.source.y -= k * 60, d.target.y += k * 100;
/*
if(i%2==1){
d.source.x -= 0.4/k
}else{
d.source.x += 0.4/k
}
*/
})
.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;
});
}
</script>
</body>
</html>
use hack on tree layout - draw additional link from other node
check this example
If a hack is the solution you're looking for the implementation below might be helpful. It's a d3 collapsable tree based on Rob Schmueckers Blog Multiple Parent Nodes D3.js example which can handle multiple parent links and the basic tree events. It's typically split into index.html building the html structure, a stylesheet style.css and the actual script tree.js
index.html :
Define a div with id tree_view later holding the tree. Call tree.js to create the tree.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 collapsable multiple parents tree</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
</head>
<body>
<div id="tree_view"></div> <!-- div holding tree -->
<script src="tree.js"></script> <!-- script to create tree -->
</body>
</html>
style.css :
#tree_view {
width: 100%;
height: 100%;
margin-top: 30px;
}
/* basic node */
.node {
cursor: pointer;
text-anchor: start;
}
/* rectangle node */
.node rect {
stroke: gray;
stroke-width: 1.5px;
}
/* node text */
.node text {
font: 12px sans-serif;
}
/* standard links (.link) and multiparent links (.mpLink) */
.link, .mpLink {
fill: none;
stroke: #ccc;
}
tree.js : (full code below)
Basically you need to define a new link object for each additional link you want to add. Each link needs a source and target node. The backup nodes are necessary for event handling:
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
Now you can handle all additional links in the update process (updateTree(source)):
// ======== add additional links (mpLinks) ========
let mpLink = svg.selectAll("path.mpLink")
.data(additionalLinks);
mpLink.enter().insert("path", "g")
.attr("class", "mpLink")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
mpLink.transition()
.duration(duration)
.attr("d", diagonal)
.attr("stroke-width", 1.5)
mpLink.exit().transition()
.duration(duration)
.attr("d", function (d) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
and define the on event behavior in method click(d).
The full tree.js code:
// plot properties
let root;
let tree;
let diagonal;
let svg;
let duration = 750;
let treeMargin = { top: 0, right: 20, bottom: 20, left: 20 };
let treeWidth = window.innerWidth - treeMargin.right - treeMargin.left;
let treeHeight = window.innerHeight - treeMargin.top - treeMargin.bottom;
let treeDepth = 5;
let maxTextLength = 90;
let nodeWidth = maxTextLength + 20;
let nodeHeight = 36;
let scale = 1;
// tree data
let data = [
{
"name": "Root",
"parent": "null",
"children": [
{
"name": "Level 2: A",
"parent": "Top Level",
"children": [
{
"name": "A1",
"parent": "Level 2: A"
},
{
"name": "A2",
"parent": "Level 2: A"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level"
}
]
}
];
// additional (multiparent) links data array
let additionalLinks = []
/**
* Initialize tree properties
* #param {Object} treeData
*/
function initTree(treeData) {
// init
tree = d3.layout.tree()
.size([treeWidth, treeHeight]);
diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.x + nodeWidth / 2, d.y + nodeHeight / 2]; });
svg = d3.select("div#tree_view")
.append("svg")
.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
root = treeData[0];
root.x0 = treeHeight / 2;
root.y0 = 0;
// fill additionalLinks array
let pairNode1 = tree.nodes(root).filter(function(d) {
return d['name'] === 'Level 2: B';
})[0];
let pairNode2 = tree.nodes(root).filter(function(d) {
return d['name'] === 'A2';
})[0];
let link = new Object();
link.source = pairNode1;
link.target = pairNode2;
link._source = pairNode1; // backup source
link._target = pairNode2; // backup target
additionalLinks.push(link)
// update
updateTree(root);
d3.select(self.frameElement).style("height", "500px");
// add resize listener
window.addEventListener("resize", function (event) {
resizeTreePlot();
});
}
/**
* Perform tree update. Update nodes and links
* #param {Object} source
*/
function updateTree(source) {
let i = 0;
let nodes = tree.nodes(root).reverse();
let links = tree.links(nodes);
nodes.forEach(function (d) { d.y = d.depth * 80; });
// ======== add nodes and text elements ========
let node = svg.selectAll("g.node")
.data(nodes, function (d) { return d.id || (d.id = ++i); });
let nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) { return `translate(${source.x0},${source.y0})`; })
.on("click", click);
nodeEnter.append("rect")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("rx", 2)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeEnter.append("text")
.attr("y", nodeHeight / 2)
.attr("x", 13)
.attr("dy", ".35em")
.text(function (d) { return d.name; })
.style("fill-opacity", 1e-6);
let nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${d.x},${d.y})`; });
nodeUpdate.select("rect")
.attr("width", nodeWidth)
.style("fill", function(d) { return d._children ? "#ace3b5": "#f4f4f9"; });
nodeUpdate.select("text").style("fill-opacity", 1);
let nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function (d) { return `translate(${source.x},${source.y})`; })
.remove();
nodeExit.select("rect")
.attr("width", nodeWidth)
.attr("rx", 2)
.attr("height", nodeHeight);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// ======== add links ========
let link = svg.selectAll("path.link")
.data(links, function (d) { return d.target.id; });
link.enter().insert("path", "g")
.attr("class", "link")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.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) {
let o = { x: source.x, y: source.y };
return diagonal({ source: o, target: o });
})
.remove();
// ======== add additional links (mpLinks) ========
let mpLink = svg.selectAll("path.mpLink")
.data(additionalLinks);
mpLink.enter().insert("path", "g")
.attr("class", "mpLink")
.attr("x", nodeWidth / 2)
.attr("y", nodeHeight / 2)
.attr("d", function (d) {
var o = { x: source.x0, y: source.y0 };
return diagonal({ source: o, target: o });
});
mpLink.transition()
.duration(duration)
.attr("d", diagonal)
.attr("stroke-width", 1.5)
mpLink.exit().transition()
.duration(duration)
.attr("d", function (d) {
let 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;
});
}
/**
* Handle on tree node clicked actions
* #param {Object} d node
*/
function click(d) {
// update regular links
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
// update additional links
additionalLinks.forEach(function(link){
let sourceVisible = false;
let targetVisible = false;
tree.nodes(root).filter(function(n) {
if(n["name"] == link._source.name){
sourceVisible = true;
}
if(n["name"] == link._target.name){
targetVisible = true;
}
});
if(sourceVisible && targetVisible){
link.source = link._source;
link.target = link._target;
}
else if(!sourceVisible && targetVisible
|| !sourceVisible && !targetVisible){
link.source = d;
link.target = link.source;
}
else if(sourceVisible && !targetVisible){
link.source = link._source;
link.target = link.source;
}
});
// define more links behavior here...
updateTree(d);
}
/**
* Update tree dimension
*/
function updateTreeDimension() {
tree.size([treeWidth, treeHeight]);
svg.attr("width", treeWidth + treeMargin.right + treeMargin.left)
.attr("height", treeHeight + treeMargin.top + treeMargin.bottom)
.attr("transform", `translate(${treeMargin.left},${treeMargin.top})scale(${scale},${scale})`);
}
/**
* Resize the tree using current window dimension
*/
function resizeTreePlot() {
treeWidth = 0.9 * window.innerWidth - treeMargin.right - treeMargin.left;
treeHeight = (treeDepth + 2) * nodeHeight * 2;
updateTreeDimension();
updateTree(root);
}
// plot tree
initTree(data);
updateTree(root);
Here you can find the full demo. You can also fork the project.

How to give color to edges of d3js graph?

I am trying to give color to the edges but the desire outcome is not according to my desire.The color scheme changes every time I change the JSON file.
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="D3js_edges_connected_by_nodes_id.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<title>Weighted Citation Graph</title>
<style>
path.link {
fill: none;
stroke: #666;
stroke-width: 1.5px;
}
circle {
fill: #ccc;
stroke: #333;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
body {
background-color: white;
margin: 0px;
}
.graphContainer {
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
}
</style>
<script>
function load_graph(text) {
var color = d3.scale.category20();
try{
//var data = JSON.parse(text);
} catch (e) {
window.alert("sometext: "+e);
}
var data = { "nodes": [{ "id": 127230, "name": "Optimization of query evaluation algorithms", "citation": 26, "group": 7 }, { "id": 127254, "name": "Flow algorithms for parallel query optimization", "citation": 22, "group": 7 }, { "id": 127380, "name": "Randomized approximation algorithms for query optimization problems on two processors", "citation": 14, "group": 7 }, { "id": 127438, "name": "Optimization algorithms for simultaneous multidimensional queries in OLAP environments", "citation": 12, "group": 7 }, { "id": 127063, "name": "Query optimization in database systems", "citation": 230, "group": 7 }, { "id": 127158, "name": "Query optimization in a memory-resident domain relational calculus database system", "citation": 41, "group": 7 }, { "id": 129760, "name": "An Overview of TQuel", "citation": 22, "group": 7 }, { "id": 129867, "name": "ADVISORS", "citation": 10, "group": 7 }, { "id": 129872, "name": "Tellabs and THRIP through the Telkom Centre of Excellence at Rhodes University.", "citation": 10, "group": 7 }, { "id": 127412, "name": "Optimal service ordering in decentralized queries over web services", "citation": 13, "group": 7 }, { "id": 130856, "name": "Queries over Web Services", "citation": 10, "group": 7 }, { "id": 130959, "name": "Exploiting Parallelism to Accelerate Keyword Search On Deep-web Sources", "citation": 10, "group": 7 }, { "id": 131199, "name": "Contents lists available at ScienceDirect Future Generation Computer Systems", "citation": 10, "group": 7 }, { "id": 131211, "name": "Flow Algorithms for Parallel Query Optimization", "citation": 10, "group": 7 }, { "id": 127373, "name": "Multi-query Optimization for On-Line Analytical Processing", "citation": 14, "group": 7 }, { "id": 133379, "name": "Concise descriptions of subsets of structured sets", "citation": 21, "group": 7 }], "links": [{ "source": 127230, "target": 127063, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127230, "target": 127158, "name": "71 %", "value": 71, "grouo": "#1F75FE" }, { "source": 127230, "target": 129760, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129867, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129872, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 127063, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127230, "target": 127158, "name": "71 %", "value": 71, "grouo": "#1F75FE" }, { "source": 127230, "target": 129760, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129867, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127230, "target": 129872, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 127412, "name": "5 %", "value": 5, "grouo": "#1F75FE" }, { "source": 127254, "target": 130856, "name": "2 %", "value": 2, "grouo": "#1F75FE" }, { "source": 127254, "target": 130959, "name": "Least Similar", "value": 10, "grouo": "#ACE5EE" }, { "source": 127254, "target": 131199, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 131211, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127254, "target": 127412, "name": "5 %", "value": 5, "grouo": "#1F75FE" }, { "source": 127254, "target": 130856, "name": "2 %", "value": 2, "grouo": "#1F75FE" }, { "source": 127254, "target": 130959, "name": "Least Similar", "value": 10, "grouo": "#ACE5EE" }, { "source": 127254, "target": 131199, "name": "Nothing Matched", "value": 10, "grouo": "#EE204D" }, { "source": 127254, "target": 131211, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 127373, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 133379, "name": "3 %", "value": 3, "grouo": "#1F75FE" }, { "source": 127438, "target": 127373, "name": "Most Similar", "value": 100, "grouo": "#1A4876" }, { "source": 127438, "target": 133379, "name": "3 %", "value": 3, "grouo": "#1F75FE" }] };
// used to store the number of links between two nodes.
// mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
var mLinkNum = {};
// sort links first
// sortLinks();
data.links.sort(function (a, b) {
if (a.source > b.source) { return 1; }
else if (a.source < b.source) { return -1; }
else {
if (a.target > b.target) { return 1; }
if (a.target < b.target) { return -1; }
else { return 0; }
}
})
// set up linkIndex and linkNumer, because it may possible multiple links share the same source and target node
setLinkIndexAndNum();
var w = 1345,
h = 1000;
//var w = 3000,
// h = 3000;
var force = d3.layout.force()
.size([w, h])
.linkDistance(200)
.charge(-800)
.on("tick", tick);
var svg = d3.select(".graphContainer").append("svg:svg")
.attr("width", w)
.attr("height", h);
var color = d3.scale.category10()
var edges = [];
data.links.forEach(function (e) {
var sourceNode = data.nodes.filter(function (n) {
return n.id === e.source;
})[0],
targetNode = data.nodes.filter(function (n) {
return n.id === e.target;
})[0];
edges.push({
source: sourceNode,
target: targetNode,
name: e.name,
value: e.value,
linkindex: e.linkindex,
grouo: e.grouo
});
});
console.log(edges)
force
.nodes(data.nodes)
.links(edges)
.start();
var path = svg.append("svg:g")
.selectAll("line")
.data(edges)
.enter().append("svg:path")
.attr("class", "link")
.style("stroke-width", function (d, i) {
console.log(d.value)
return Math.sqrt(d.value);
}).style('stroke', function (d) {
return color(d.grouo);
});
//path.append("title").text(function (d) { return d.name });
var circle = svg.append("svg:g")
.selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", function (d) {
return (Math.sqrt(d.citation));
})
.style("fill", function (d) {
return color(d.group);
})
.call(force.drag);
circle.append("title").text(function (d) { return d.name });
//circle.on("click", function () {
// d3.select(this)
// .attr("r", function (d) {
// return (Math.sqrt(d.citation) * 2);
// })
// .style("fill", "lightsteelblue");
//});
circle.on("click", function (d) {
var thisNode = d.id
var connected = data.links.filter(function (e) {
return e.source === thisNode || e.target === thisNode
});
circle.attr("opacity", function (d) {
return (connected.map(d => d.source).indexOf(d.id) > -1 || connected.map(d => d.target).indexOf(d.id) > -1) ? 1 : 0.1
});
path.attr("opacity", function (d) {
return (d.source.id == thisNode || d.target.id == thisNode) ? 1 : 0.1
});
})
circle.on("dblclick", function (d) {
var thisNode = d.id
var connected = data.links.filter(function (e) {
return e.source === thisNode || e.target === thisNode
});
circle.attr("opacity", function (d) {
return (connected.map(d => d.source).indexOf(d.id) > -1 || connected.map(d => d.target).indexOf(d.id) > -1) ? 1 : 1
});
path.attr("opacity", function (d) {
return (d.source.id == thisNode || d.target.id == thisNode) ? 1 : 1
});
})
var text = svg.append("svg:g")
.selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
console.log('test');
//A copy of the text with a thick white stroke for legibility.
//text.append("svg:text")
//.attr("x", 8)
//.attr("y", ".31em")
//.attr("class", "shadow")
//.text(function (d) {
//return d.name;
//});
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function (d) {
// return d.name;
});
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", function (d, i) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = 75 * d.linkindex; //linknum is defined above
var output = "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
//console.log(d)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
// Add tooltip to the connection path
path.append("svg:title")
.text(function (d, i) {
return d.name;
});
circle.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// sort the links by source, then target
function sortLinks1() {
data.links.sort(function (a, b) {
if (a.source > b.source) {
return 1;
} else if (a.source < b.source) {
return -1;
} else {
if (a.target > b.target) {
return 1;
}
if (a.target < b.target) {
return -1;
} else {
return 0;
}
}
});
}
//any links with duplicate source and target get an incremented 'linknum'
function setLinkIndexAndNum1() {
for (var i = 0; i < data.links.length; i++) {
if (i != 0 &&
data.links[i].source == data.links[i - 1].source &&
data.links[i].target == data.links[i - 1].target) {
data.links[i].linkindex = data.links[i - 1].linkindex + 1;
console.log(data.links[i].linkindex)
} else {
data.links[i].linkindex = 1;
console.log(data.links[i].linkindex)
}
// save the total number of links between two nodes
if (mLinkNum[data.links[i].target + "," + data.links[i].source] !== undefined) {
mLinkNum[data.links[i].target + "," + data.links[i].source] = data.links[i].linkindex;
} else {
mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
}
}
}
function setLinkIndexAndNum() {
for (var i = 0; i < data.links.length; i++) {
if (i != 0 &&
data.links[i].source == data.links[i - 1].source &&
data.links[i].target == data.links[i - 1].target) {
data.links[i].linkindex = data.links[i - 1].linkindex + 1;
}
else {
data.links[i].linkindex = 1;
};
};
}
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<script src="//d3js.org/d3.v3.min.js"></script>
<%--<textarea runat="server" id="textarea" cols="80" rows="20"></textarea>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>--%>
</div>
<div id="graphContainer" class="graphContainer"></div>
</form>
</body>
</html>
Can anybody help me out. thanks!
If you look at the data variable I have given different colors for each edge according to description of "name" attribute.
The reason you see different colours is because the colour scale d3.scale.category10() - like all d3 ordinal scales (at least in v3) - assigns its colours on a first come first served basis.
https://github.com/d3/d3-3.x-api-reference/blob/master/Ordinal-Scales.md#_ordinal
So if you changed your json so a node with group 'A' is encountered first, then it and subsequent group 'A' nodes will be given the first colour in the scale. If previously it was a node with group 'B' that was encountered first all the nodes with group 'B' would have got that colour instead.
One way to avoid this is to run through your data, collate the groups, order them, and pass them as a domain to your colour scale before setting the colours for individual nodes. That way A, B etc would be in the same order each time.
However, this would not help in situations where a change in your data meant particular groups were missing altogether, you'd still see a shift in the colour assignment. For that you'd need to supply a fixed list of all possible groups to assign to the colour scale, even if those groups weren't all in your current data.
PS. You assign color to the category20 colour scale, and then later on reassign it to category10. That doesn't have any effect on why you're seeing what you're seeing though as you don't use color till afterwards.
PPS. Another thing I've noticed is that your link colours, as defined by the .grouo data, are already hexadecimal colour strings. I suspect you don't mean to pass them to the color scale and mean to use them as they are i.e. style ("stroke", function(d) { return d.grouo; ]);

how to change the color of a link in d3js?

here is my code and i want to change the color of my link.
i have tried so many techniques but no effect.
please kindly help me out.
im using asp.net
<%# Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="D3js_demo.WebForm1" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script type="text/javascript" src="http://d3js.org/d3.v2.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
<title>Weighted Citation Graph</title>
<style>
path.link {
fill: #ccc;
stroke: #333;
stroke-width: 1.5px;
}
circle {
fill: #ccc;
stroke: #333;
stroke-width: 1.5px;
}
text {
font: 10px sans-serif;
pointer-events: none;
}
text.shadow {
stroke: #fff;
stroke-width: 3px;
stroke-opacity: .8;
}
body {
background-color: white;
margin: 0px;
}
.graphContainer {
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
$(function () {
var color = d3.scale.category20();
var data = {
"nodes": [
{ "id": 0, "name": "paper1", "citation": 5, "group": 1 },
{ "id": 1, "name": "paper2", "citation": 8, "group": 2 },
{ "id": 2, "name": "paper3", "citation": 12, "group": 3 },
{ "id": 3, "name": "paper4", "citation": 25, "group": 4 },
{ "id": 4, "name": "paper5", "citation": 15, "group": 5 },
{ "id": 5, "name": "paper6", "citation": 5, "group": 1 },
{ "id": 6, "name": "paper7", "citation": 8, "group": 2 },
{ "id": 7, "name": "paper8", "citation": 12, "group": 3 },
{ "id": 8, "name": "paper9", "citation": 25, "group": 4 },
{ "id": 9, "name": "paper10", "citation": 15, "group": 5 }
],
"links": [
{ "source": 0, "target": 1, "name": "A-B-1", "value": 8 , "grouo": 1},
{ "source": 0, "target": 1, "name": "A-B-2", "value": 24, "grouo": 2 },
{ "source": 0, "target": 2, "name": "A-C-1", "value": 100, "grouo": 1 },
{ "source": 0, "target": 2, "name": "A-C-3", "value": 44, "grouo": 2 },
{ "source": 2, "target": 3, "name": "A-D-1", "value": 169, "grouo": 1 },
{ "source": 2, "target": 3, "name": "A-D-2", "value": 80, "grouo": 2 },
{ "source": 2, "target": 4, "name": "A-E-1", "value": 16, "grouo": 1 },
{ "source": 2, "target": 4, "name": "A-E-5", "value": 200, "grouo": 2 },
{ "source": 4, "target": 5, "name": "A-B-1", "value": 8, "grouo": 1 },
{ "source": 4, "target": 5, "name": "A-B-2", "value": 24, "grouo": 2 },
{ "source": 5, "target": 6, "name": "A-C-1", "value": 12, "grouo": 1 },
{ "source": 5, "target": 6, "name": "A-C-3", "value": 44, "grouo": 2 },
{ "source": 5, "target": 7, "name": "A-D-1", "value": 125, "grouo": 1 },
{ "source": 5, "target": 7, "name": "A-D-2", "value": 225, "grouo": 2 },
{ "source": 7, "target": 8, "name": "A-E-1", "value": 36, "grouo": 1 },
{ "source": 7, "target": 8, "name": "A-E-5", "value": 81, "grouo": 2 },
{ "source": 8, "target": 3, "name": "A-C-1", "value": 9, "grouo": 1 },
{ "source": 8, "target": 3, "name": "A-C-3", "value": 16, "grouo": 2 },
{ "source": 8, "target": 9, "name": "A-D-1", "value": 50, "grouo": 1 },
{ "source": 8, "target": 9, "name": "A-D-2", "value": 100, "grouo": 2 }
]
};
// used to store the number of links between two nodes.
// mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
var mLinkNum = {};
// sort links first
sortLinks();
// set up linkIndex and linkNumer, because it may possible multiple links share the same source and target node
setLinkIndexAndNum();
var w = 960,
h = 500;
var force = d3.layout.force()
.nodes(d3.values(data.nodes))
.links(data.links)
.size([w, h])
.linkDistance(200)
.charge(-1000)
.on("tick", tick)
.start();
var svg = d3.select(".graphContainer").append("svg:svg")
.attr("width", w)
.attr("height", h);
var path = svg.append("svg:g")
.selectAll("line")
.data(force.links())
.enter().append("svg:path")
.attr("class", "link")
.style("stroke-width", function (d) { return Math.sqrt(d.value); });
var circle = svg.append("svg:g")
.selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", function (d) { return (d.citation); })
.style("fill", function (d) { return color(d.group); })
.call(force.drag);
var text = svg.append("svg:g")
.selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
// A copy of the text with a thick white stroke for legibility.
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function (d) { return d.name; });
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function (d) { return d.name; });
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
path.attr("d", function (d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
// get the total link numbers between source and target node
var lTotalLinkNum = mLinkNum[d.source.id + "," + d.target.id] || mLinkNum[d.target.id + "," + d.source.id];
if (lTotalLinkNum > 1) {
// if there are multiple links between these two nodes, we need generate different dr for each path
dr = dr / (1 + (1 / lTotalLinkNum) * (d.linkindex - 1));
}
// generate svg path
return "M" + d.source.x + "," + d.source.y +
"A" + dr + "," + dr + " 0 0 1," + d.target.x + "," + d.target.y +
"A" + dr + "," + dr + " 0 0 0," + d.source.x + "," + d.source.y;
});
// Add tooltip to the connection path
path.append("svg:title")
.text(function (d, i) { return d.name; });
circle.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
// sort the links by source, then target
function sortLinks() {
data.links.sort(function (a, b) {
if (a.source > b.source) {
return 1;
}
else if (a.source < b.source) {
return -1;
}
else {
if (a.target > b.target) {
return 1;
}
if (a.target < b.target) {
return -1;
}
else {
return 0;
}
}
});
}
//any links with duplicate source and target get an incremented 'linknum'
function setLinkIndexAndNum() {
for (var i = 0; i < data.links.length; i++) {
if (i != 0 &&
data.links[i].source == data.links[i - 1].source &&
data.links[i].target == data.links[i - 1].target) {
data.links[i].linkindex = data.links[i - 1].linkindex + 1;
}
else {
data.links[i].linkindex = 1;
}
// save the total number of links between two nodes
if (mLinkNum[data.links[i].target + "," + data.links[i].source] !== undefined) {
mLinkNum[data.links[i].target + "," + data.links[i].source] = data.links[i].linkindex;
}
else {
mLinkNum[data.links[i].source + "," + data.links[i].target] = data.links[i].linkindex;
}
}
}
});
</script>
</div>
<div id="graphContainer" class="graphContainer"></div>
</form>
</body>
</html>
If you want to color them according to the stroke-width and using the same color scale, just change your var path:
var path = svg.append("svg:g")
.selectAll("line")
.data(force.links())
.enter().append("svg:path")
.attr("class", "link")
.style("stroke-width", function (d) { return Math.sqrt(d.value); })
.style("stroke", function(d){
return color(d.value)
});
This is how it looks like: https://jsfiddle.net/4xt5v51m/

Resources