Drawing a radial tree from node and edges rather than children - d3.js

I was able to draw a graph with d3 from neo4j data, but I'm struggling in drawing a tree. Examples I've seen use a json file in which each node contains an array of its own children. Although this looks pretty convenient, it's not easy - at least to me - to interface with neo4j. Indeed, neo4j wouldn't let one return {nodes: collect(distinct {name: n.name, children: collect(distinct {name: m.name})})} from a (n)--(m) graph. It would however let one return collections of nodes and edges from a path.
My question, would it be possible to draw a tree in d3 from nodes and edges without having children for each node in the data structure?
Many thanks in advance
Pierre

Cypher query to retrieve nodes and relationships from neo4j would be:
MATCH p=() <-[:REPORTS8TO]- ()
UNWIND nodes(p) AS n UNWIND rels(p) AS r
WITH n, r ORDER BY n.Name
RETURN {nodes: COLLECT(DISTINCT n), links: COLLECT(DISTINCT {source: id(endNode(r)), target: id(startNode(r))})}
Hence, javascript and d3 code:
- extract nodes and relationships from json
var obj = JSON.parse(xmlhttp.responseText);
var json = obj.data[0][0];
var nodeMap = {};
json.nodes.forEach(function(x) {
nodeMap[x.id] = x;
nodeMap[x.id].children = [];
});
var links = json.links.map(function(x) {
nodeMap[x.source].children.push(nodeMap[x.target]);
return { source: nodeMap[x.source], target: nodeMap[x.target] };
});
draw the tree
d3.select("#tree").select("svg").remove();
var width = 280, height = 870;
var margin = {
top: 0,
bottom: 0,
left: 10,
right: 120
}
var tree = d3.layout.tree()
.size([height - margin.top - margin.bottom, width - margin.left - margin.right]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#tree").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom )
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Compute the new tree layout from root
var nodes = tree.nodes(root).reverse(), links = tree.links(nodes);
var link = vis.selectAll("path.link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
var node = vis.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.y + ", " + d.x + ")"; })
node.append("rect")
.attr("width", 9)
.attr("height", 9)
.attr("y", -4)
.attr("class", function(d) { return "node "+d.type; } );
node.append("text")
.attr("dx", function(d) { return d.children ? -14 : 14; })
.attr("dy", 4)
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.attr("class", function(d) {return "node "+d.type+" text"; })
.attr("style", "stroke-wdth: 0.5px; font: 10px sans-serif;")
.text(function(d) { if (d.Name != "Panel") { return d.Name; }});

How about this for a cypher query that returns the manager(s) and their respective children.
match (manager)<-[:REPORTS_TO]-(direct_reports)
return id(manager), collect(id(direct_reports))

Related

Formatting flat JSON for tree graph in D3 v7

I have a flat JSON array and try to format it for a tree representation in D3.js v7 using the below code. I grouped the data and then used hierarchy to make the link and nodes as is described in the documentation but when I make the graph it produces an empty root and last children.
see: https://codepen.io/nvelden/pen/LYmveWz?editors=1111
//Load data
const data = [
{"root":"project","project_nr":"project 1","department":"1","devision":"A"},
{"root":"project","project_nr":"project 1","department":"1","devision":"B"},
{"root":"project","project_nr":"project 1","department":"2","devision":"A"},
{"root":"project","project_nr":"project 2","department":"3","devision":"A"}
]
var margin = {top: 10, right: 10, bottom: 10, left: 50},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var tree = d3.cluster()
.size([height, width])
.size([height-margin.top-margin.bottom,width-margin.left-margin.right]);
var groupedData = d3.group(data,
d => d.root,
d => d.project_nr,
d => d.department,
d => d.devision)
//Create root
var root = d3.hierarchy(groupedData)
//Attach canvas element
var svg = d3.select("body")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var g = svg
.append("g")
.attr('transform','translate('+ margin.left +','+ margin.right +')');
var link = g.selectAll(".link")
.data(tree(root).links())
.enter()
.append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.x(function(d) {return d.y;})
.y(function(d) {return d.x;}));
var node = g.selectAll(".node")
.data(root.descendants())
.enter()
.append("g")
.attr("class", "link")
.attr("class", d =>
{ return "node" + (d.children ? " node--internal" : " node--leaf")})
.attr("transform", d =>
{ return "translate(" + d.y + ","+ d.x + ")" ; })
var text = g.selectAll("text")
.data(root.descendants())
.enter().append("text")
.text(d => d.data[0])
.attr('dy', "0.32em")
.attr("class", "label glow")
.attr('text-anchor', "center")
.attr("x", d => d.y)
.attr("y", d => d.x);
node.append("circle")
.attr("r", 2.5)
How should I format the data to get the tree in the below graph?
If we assume that there is only one project at the top, then it would be better not to group by project. Similarly, since the divisions are leaf-nodes, it would be better not to group by division.
groupedData = d3.group(
data,
(d) => d.project_nr,
(d) => d.department
);
However now we're missing the "project" name in the root node, and the division name in the leaf nodes. These can be retrieved by using:
({ data }) => Array.isArray(data) ? data[0] || "project" : data.devision
Here's a notebook with the complete code:
https://observablehq.com/#recifs/data-to-tree--support

D3 How to update the chart after selection from drop down menu with new data

I'm building a waterfall chart in D3. When the page will load, it will render the default page but user will have choice to select different
'Company' and 'Year' from the drop down menu. I have been able to create the chart what I want. But when I select any different Company or Year, D3 adds another chart on top of the existing instead of replacing it and thats because I'm targeting a particular div / svg from the HTML. How can I use D3 to update the chart with new data instead add another one of top? And if I can have that movement of chart bars with transition, that will be awesome.
HTML is a simple svg:
<svg class="chart"></svg>
Here is the function to create the chart which I call when Ajax call is successful:
function waterfallChart (dataset) {
var data = [];
for (var key in dataset[0]) {
data.push({
name: key,
value: dataset[0][key]
})
}
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
padding = 0.3;
var x = d3.scaleBand()
.domain(data.map(function(d) {
return d.name
}))
.range([0, width])
.padding(padding);
var y = d3.scaleLinear()
.range([height, 0]);
var xAxis = d3.axisBottom(x)
var yAxis = d3.axisLeft(y)
.tickFormat(function(d) {
return dollarFormatter(d);
});
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var cumulative = 0;
for (var i = 0; i < data.length; i++) {
data[i].start = cumulative;
cumulative += data[i].value;
data[i].end = cumulative;
data[i].class = (data[i].value >= 0) ? 'positive' : 'negative'
}
data.push({
name: 'Total',
end: cumulative,
start: 0,
class: 'total'
});
x.domain(data.map(function(d) {
return d.name;
}));
y.domain([0, d3.max(data, function(d) {
return d.end;
})]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", function(d) {
return "bar " + d.class
})
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)";
});
bar.append("rect")
.attr("y", function(d) {
return y(Math.max(d.start, d.end));
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end));
})
.attr("width", x.bandwidth());
bar.append("text")
.attr("x", x.bandwidth() / 2)
.attr("y", function(d) {
return y(d.end) + 5;
})
.attr("dy", function(d) {
return ((d.class == 'negative') ? '-' : '') + ".75em"
})
.text(function(d) {
return dollarFormatter(d.end - d.start);
});
bar.filter(function(d) {
return d.class != "total"
}).append("line")
.attr("class", "connector")
.attr("x1", x.bandwidth() + 5)
.attr("y1", function(d) {
return y(d.end)
})
.attr("x2", x.bandwidth() / (1 - padding) - 5)
.attr("y2", function(d) {
return y(d.end)
})
function dollarFormatter(n) {
n = Math.round(n);
var result = n;
if (Math.abs(n) > 1000) {
result = Math.round(n/1000) + 'B';
}
return '$ ' + result;
}
}
Here is code where I have event listener and on selection it will run the above function:
$("#airline-selected, #year-selected").change(function chartsData(event) {
event.preventDefault();
var airlineSelected = $('#airline-selected').find(":selected").val();
var yearSelected = $('#year-selected').find(":selected").val();
$.ajax({
url: "{% url 'airline_specific_filtered' %}",
method: 'GET',
data : {
airline_category: airlineSelected,
year_category: yearSelected
},
success: function(dataset){
waterfallChart(dataset)
},
error: function(error_data){
console.log("error")
console.log(error_data)
}
})
});
You are missing some pretty important things here. If you are going to do updates on your data you need to do a couple things.
Give a key to the data() function. You need to give D3 a way to identify data when you update it so it knows if it should add, remove, or leave existing data. The key does this. For instance you might do something like this:
.data(data, function(d) { return d.name })
Now d3 will be able to tell you data items apart assuming d.name is a unique identifier.
You need an exit() for data that is removed during update. You need to save the data joined selection so you can call enter and exit on it:
var bar = chart.selectAll(".bar")
.data(data, function(d) { return d.name})
now you can call: bar.exit().remove() to get rid of deleted items and bar.enter() to add items.
You need to make a selection that hasn't had enter() called on it to update attributes.
Probably more a matter of style, but you should set up the SVG and margins outside the update function since they state the same. You can still update the axis and scales by calling the appropriate functions in the update.
The code you posted is a little hard for other people to run — you'll always get better faster answers if you post code that has been reduced to the main problem and that others can run without needing access to offsite data or apis.
Here's an example that updates on a setInterval between two data sets based on your code. But you should also look at the General Update Patterns - they are very simple but have almost everything you need to know. (https://bl.ocks.org/mbostock/3808234)
dataset = [
{name: "Albert", start: 0, end:220},
{name: "Mark", start: 0, end:200},
{name: "Søren", start: 0, end:100},
{name: "Immanuel", start: 0, end:60},
{name: "Michel", start: 0, end:90},
{name: "Jean Paul", start: 0, end: 80}
]
dataset2 = [
{name: "Albert", start: 0, end:20},
{name: "Immanuel", start:0, end:220},
{name: "Jaques", start: 0, end:100},
{name: "Gerhard", start:0 , end:50},
{name: "Søren", start: 0, end:150},
{name: "William", start: 0, end: 180}
]
var margin = {
top: 10,
right: 30,
bottom: 30,
left: 40
},
width = 400 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom,
padding = 0.3;
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand()
.range([0, width])
.padding(padding);
var y = d3.scaleLinear()
.range([height, 0])
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
chart.append("g")
.attr("class", "y axis")
var currentData = dataset
waterfallChart(currentData)
setInterval(function() {
currentData = currentData === dataset ? dataset2 : dataset
waterfallChart(currentData)
}, 3000)
function waterfallChart(data) {
var t = d3.transition()
.duration(750)
x.domain(data.map(function(d) {
return d.name
}))
y.domain([0, d3.max(data, function(d) {
return d.end
})])
var xAxis = d3.axisBottom(x)
var yAxis = d3.axisLeft(y)
d3.select('g.x').transition(t).call(xAxis)
d3.select('g.y').call(yAxis)
var bar = chart.selectAll(".bar")
.data(data, function(d) {
return d.name
})
// ENTER -- ADD ITEMS THAT ARE NEW IN DATA
bar.enter().append("g")
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)"
})
.attr("class", 'bar')
.append("rect")
.attr("y", function(d) {
return y(Math.max(d.start, d.end));
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end));
})
.attr("width", x.bandwidth())
// UPDATE EXISTING ITEMS
chart.selectAll(".bar")
.transition(t)
.attr("transform", function(d) {
return "translate(" + x(d.name) + ",0)"
})
.select('rect')
.attr("y", function(d) {
return y(Math.max(d.start, d.end))
})
.attr("height", function(d) {
return Math.abs(y(d.start) - y(d.end))
})
.attr("width", x.bandwidth())
// REMOVE ITEMS DELETED FROM DATA
bar.exit().remove()
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg class="chart"></svg>

D3 tree square node not in desired position

I want to create a tree view using d3 like this one http://bl.ocks.org/mbostock/4339083,
but instead of circles in node, I would like to have squares. I found this post that gave me a clue d3.js: modifyng links in a tree layout but not solved my issue. This is my fiddle http://jsfiddle.net/yp7o8wbm/ .
As you can see, all node are not in the correct position.
This is the js code:
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = 960 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var rectSize = 40;
var i = 0;
var tree = d3.layout.tree().size([height, width]);
var diagonal = d3.svg.diagonal()
.source(function(d) { return {"x":d.source.x, "y":(d.source.y+rectSize)}; })
.target(function(d) { return {"x":(d.target.x), "y":d.target.y}; })
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
update(root);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Declare the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter the nodes.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; });
nodeEnter.append("rect")
.attr("x", function(d) { return d.x ; })
.attr("y", function(d) { return d.y ; })
.attr("width", rectSize)
.attr("height", rectSize);
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -13 : 13; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1);
// Declare the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter the links.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", diagonal);
}
I can not realize where is the error in my code?
You are setting the position twice in different ways:
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; //<-- setting it on the parent using a "translate"
});
nodeEnter.append("rect")
.attr("x", function(d) { return d.x ; }) //<-- setting it on the rect using a "x" attribute
Do this instead:
nodeEnter.append("rect")
.attr("x", 0) //<-- x is taken care of by translate
.attr("y", -rectSize/2) //<-- just use y to center the rect
.attr("width", rectSize)
.attr("height", rectSize);
Updated fiddle.

Problems parsing data with the D3.js Sankey layout

I'm looking for some hints as to what I am doing wrong with a Sankey diagram I'm creating. I am charting changes in food consumption over time, and using the Sankey layout to visualize how these values changed over a period of forty years.
The bl.ock and small dataset are here. The relevant code:
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 1260 - margin.left - margin.right,
height = 1000 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"),
format = function(d) { return formatNumber(d) + " TWh"; },
color = d3.scale.category20();
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
var path = sankey.link();
// ========================== Prepare data ==========================
queue()
.defer(d3.csv, "grains.csv")
.await(ready);
// ========================== Start viz ==========================
function ready(error, csv_data) {
nodes = [];
edges = [];
nodesArray = [];
// Scales
yearScale = d3.scale.linear().domain([1640,1688]).range([20,width -20]);
radiusScale = d3.scale.linear().domain([0,300]).range([2,12]).clamp(true);
chargeScale = d3.scale.linear().domain([0,100]).range([0,-100]).clamp(true);
uniqueValues = d3.set(nodesArray.map(function(d) {return d.name})).values();
colorScale = d3.scale.category20b(uniqueValues);
sortScale = d3.scale.ordinal().domain(uniqueValues).rangePoints([-0.001,.001]);
// Create a JSON link array
// This creates unique nodes for each item and its corresponding date.
// For example, nodes are rendered as "peas-1640," "peas-1641," etc.
csv_data.forEach(function(link) {
key = link.translation + '-' + link.date;
link.source = nodes[key] || (nodes[key] = {name: link.translation, date: link.date, origX: yearScale(parseInt(link.date)), value: link.value || 0});
});
// Build the edgesArray array
// This creates the edgesArray to correspond with unique nodes. We're telling
// items and dates to remain together. So, the code below tells the graph
// layout that `1641` is preceded by `1640` and followed by `1642`, etc.
var y = "→";
for (x in nodes) {
nodesArray.push(nodes[x])
if(nodes[y]) {
nodes[y].date = parseInt(nodes[y].date);
if (nodes[y].name == nodes[x].name) {
var newLink = {source:nodes[y], target:nodes[x]}
edges.push(newLink);
}
}
y = x;
}
sankey
.nodeWidth(10)
.nodePadding(10)
.size([1200, 1200])
.nodes(nodesArray.filter(function(d,i) {return d.date < 1650}))
.links(edges.filter(function(d,i) { return i < 50 && d.source.date < 1650 && d.target.date < 1650} )) // filtering to test a smaller data set
.layout(32);
var link = svg.append("g").selectAll(".link")
.data(edges.filter(function(d,i) { return i < 50 && d.source.date < 1650 && d.target.date < 1650} )) // filtering to test a smaller data set
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; });
link.append("title")
.text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
var node = svg.append("g").selectAll(".node")
.data(nodesArray.filter(function(d,i) {return d.date < 1650})) // filtering to test a smaller data set
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", function() { this.parentNode.appendChild(this); })
.on("drag", dragmove));
node.append("rect")
.attr("height", function(d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function(d) { return d.color = color(d.name.replace(/ .*/, "")); })
.style("stroke", function(d) { return d3.rgb(d.color).darker(2); })
.append("title")
.text(function(d) { return d.name + "\n" + format(d.value); });
node.append("text")
.attr("x", -6)
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function(d) { return d.name; })
.filter(function(d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
function dragmove(d) {
d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
sankey.relayout();
link.attr("d", path);
}
};
Unfortunately, I'm getting an error as you can see in the bl.ock. The Boss suggested it might be a circular link but I'm at a bit of a loss. Any hints or suggestions?
EDIT: For some clarity, I'm after something like this:
(Source)
From what I can tell, I think I'm building the nodes and edges correctly. If we look at the console for the nodes array and edges array:
It's not like a usual Sankey or alluvial diagram, which, as I've often seen them, shows collapses and expansions of items. In my case the date, food item, and value are all a single stream throughout the length of the visualization but are resized/repositioned based on the value for a given year (like the example image above).

D3 tree: lines instead of diagonal projection

I am using d3.js to create a tree using this example.
This handles the data I have perfectly and produces the desired outcome except for one detail: I don't want those wiggly connector lines between the nodes, I want a clean and simple line. Can anyone show me how to produce that?
I've been looking at the API documentation for d3.js, but with no success. From what I understood, the svg.line function should produce a straight line given a set of two pairs of coordinates (x,y). What I think I need to know is: given this data, how to create a line given the (cx,cy) of each pair of nodes in the links array:
var margin = {top: 40, right: 40, bottom: 40, left: 40};
var width = 960 - margin.left - margin.right;
var height = 500 - margin.top - margin.bottom;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("graph.csv", function(links) {
var nodesByName = {};
links.forEach(function(link) {
var parent = link.source = nodeByName(link.source),
child = link.target = nodeByName(link.target);
if (parent.children) parent.children.push(child);
else parent.children = [child];
});
var nodes = tree.nodes(links[0].source);
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", diagonal);
svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.attr("cx", function(d) { return d.y; })
.attr("cy", function(d) { return d.x; });
function nodeByName(name) {
return nodesByName[name] || (nodesByName[name] = {name: name});
}
});
Actually I figured out from other example:
svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.attr("x1", function(d) { return d.source.y; })
.attr("y1", function(d) { return d.source.x; })
.attr("x2", function(d) { return d.target.y; })
.attr("y2", function(d) { return d.target.x; });

Resources