Formatting flat JSON for tree graph in D3 v7 - d3.js

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

Related

How to generate multiple panels of multilines plots in D3?

I'm trying to generate multiple panels of multiple lines plots in D3 with a 2 levels nested data structure.
Can someone please point me on how to properly generate line plots. I've intuitively tried to use a 2 levels nested data structure, but I can`t find how to properly distribute the lines in their corresponding panels.
See here for the results I have so far:
http://jtremblay.github.io/viz/example.html
Here is my code.
var s = `condition,taxon,abundance,date
condition01,speciesA,0.31,2017-04-13
condition01,speciesA,0.54,2017-04-20
condition01,speciesB,0.21,2017-04-13
condition01,speciesB,0.60,2017-04-20
condition02,speciesA,0.31,2017-04-13
condition02,speciesA,0.48,2017-04-20
condition02,speciesB,0.19,2017-04-13
condition02,speciesB,0.61,2017-04-20
condition03,speciesA,0.13,2017-04-13
condition03,speciesA,0.11,2017-04-20
condition03,speciesB,0.04,2017-04-13
condition03,speciesB,0.11,2017-04-20
`;
var data = d3.csvParse(s);
data.forEach(function(d) { // Make every date in the csv data a javascript date object format
var aDate = new Date(d.date);
d.date = aDate;
});
var taxa = data.map(function (d){
return d.taxon
});
taxa = taxa.filter(onlyUniqueArray);
var dates = data.map(function (d){
return d.dates
});
var dataNested = d3.nest() // nest function allows to group the calculation per level of a factor
.key(function(d) { return d.condition;})
.key(function(d) { return d.taxon;})
.entries(data);
console.log(dataNested);
var fillColors = ["#0000CD", "#00FF00", "#FF0000", "#808080"]
// color palette
var color = d3.scaleOrdinal()
.domain(taxa)
.range(fillColors);
//Margins
var margin = { top: 20, right: 20, bottom: 60, left: 50},
width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// Define dom and svg
var dom = d3.select("#viz");
var svg = dom.selectAll("multipleLineCharts")
.data(dataNested)
.enter()
.append("div")
.attr("class", "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 + ")")
//.attr("fake", function(d) {console.log("d inside svg:"); console.log(d);})
// Add X axis --> it is a date format
var xScale = d3.scaleTime()
.rangeRound([0, width])
xScale.domain(d3.extent(data, function(d) {return d.date; }));
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(xScale))
.selectAll("text")
.style("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("dx", "-0.8em")
.attr("dy", "-0.45em")
//Add Y axis - Here because we want all panels to be on same scale, we cant use the dates from the global data structure.
var yScale = d3.scaleLinear()
.domain([
d3.min(data, function(d) { return d.abundance; } ),
d3.max(data, function(d) { return d.abundance; } )
])
.range([ height, 0 ]);
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale).ticks(5));
//Add Z scale (colors)
var zScale = d3.scaleOrdinal()
.range(fillColors);
zScale.domain(taxa);
// generate lines.
svg
.append("path")
.attr("class", "line")
.style("stroke", function(d) { return zScale(d.key); })
.attr("d", function(d, i){
return d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.abundance); })
(data); //I know something else should go in there, but can't figure out what/how exactly...
})
/* Util functions */
function onlyUniqueArray(value, index, self) {
return self.indexOf(value) === index;
}
I don't understand how to effectively handle my data structure for what I want to do...
Is my 2x nested data structure is adequate for what I'm trying to accomplish? I've tried with a one level nested data structure, but with no success.
Finally solved it. This example helped me to understand how to handle nested selections : http://bl.ocks.org/stepheneb/1183998
Essentially, the last block of code was replaced with this:
// generate lines.
var lines = svg.selectAll("lines")
.data(function(d) { return d.values;})
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d){
return d3.line()
.x(function(d) { return xScale(d.date); })
.y(function(d) { return yScale(d.abundance); })
(d.values);
})
.style("stroke", function(d) { return zScale(d.key); })
With a working example here: http://jtremblay.github.io/viz/example-fixed.html

d3js v4 can't remove old data chart after update

I've modified nice AlainRo’s Block for my needs (unfortunately can't link to it, because have not enough reputation), and I can't remove old data chart after entering new data. There is my codepen. In another example I've added merge(), and the chart is well aligned but the old one is still visible and text values are missed.
I spent a lot of time on it, and I run out of ideas.
There's code
barData = [
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) },
{ index: _.uniqueId(), value: _.random(1, 20) }
];
var margin = {top: 20, right: 20, bottom: 50, left: 70},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom,
delim = 4;
var scale = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
var x = d3.scaleLinear()
.domain([0, barData.length])
.rangeRound([0, width]);
var y = d3.scaleLinear()
.domain([0, 21])
.rangeRound([height, 0]);
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 + ")");
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
function draw() {
x.domain([0, barData.length]);
var brush = d3.brushY()
.extent(function (d, i) {
return [[x(i)+ delim/2, 0],
[x(i) + x(1) - delim/2, height]];})
.on("brush", brushmove);
var svgbrush = svg.selectAll('.brush')
.data(barData)
.enter()
.append('g')
.attr('class', 'brush')
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
svgbrush
.append('text')
.attr('y', function (d){return scale(d.value) + 25;})
.attr('x', function (d, i){return x(i) + x(0.5);})
.attr('dx', '-.60em')
.attr('dy', -5)
.style('fill', 'white')
.text(function (d) {return d3.format('.2')(d.value);});
svgbrush
.exit()
.append('g')
.attr('class', 'brush')
.remove();
function brushmove() {
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
if (d3.event.sourceEvent.type === "brush") return;
var d0 = d3.event.selection.map(scale.invert);
var d = d3.select(this).select('.selection');;
var d1 =[d0[0], 0];
d.datum().value = d0[0]; // Change the value of the original data
d3.select(this).call(d3.event.target.move, d1.map(scale));
svgbrush
.selectAll('text')
.attr('y', function (d){return scale(d.value) + 25;})
.text(function (d) {return d3.format('.2')(d.value);});
}
}
draw();
function upadateChartData() {
var newBarsToAdd = document.getElementById('charBarsCount').value;
var newBarData = function() {
return { index: _.uniqueId(), value: _.random(1, 20) }
};
newBarData = _.times(newBarsToAdd, newBarData);
barData = _.concat(barData, newBarData)
draw();
};
Is it also possible to remove cross pointer and leave only resize, when I'm dragging top bar border?
You're appending g elements twice. This:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.append('g')
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Should be:
svgbrush.enter()
.append('g')
.attr('class', 'brush')
.merge(svgbrush)
.call(brush)
.call(brush.move, function (d){return [d.value, 0].map(scale);});
Here is your updated Pen: http://codepen.io/anon/pen/VmavyX
PS: I also made other changes, declaring some new variables, just to organize your enter and update selections and solving the texts problem.

d3 v4 scaleBand ticks

I have data like the following
date,values
2016-10-01,10
2016-10-02,20
2016-10-03,30
2016-10-04,5
2016-10-05,50
2016-10-06,2
2016-10-07,7
2016-10-08,17
and am generating a bar chart using the following code
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 800 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var parseDate = d3.timeParse("%Y-%m-%d");
var x = d3.scaleBand().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Month of " + d.date + ":</strong> <span style='color:red'>" + d.value + " sales</span>";
})
var svg = d3.select("#barg").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 + ")");
svg.call(tip);
data = d3.csvParse(d3.select("pre#data2").text());
data.forEach(function(d) {
d.date = parseDate(d.date);
d.value = +d.value;
});
x.domain(data.map(function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)" )
svg.append("g")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value ($)");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.date); })
.attr("width", x.bandwidth() - 5)
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
So the problem I am having is that I have ordinal data, but for large cardinality (for instance, 120 data points) The x axis has way too many ticks. I have tried a few things like tickValues, but when I use this, my x axis tick points all show up on top of each other. Ideally I would like 10 tick points or so, when the cardinality is high. Any ideas?
This can be done using tickValues indeed. For instance, in this demo, we have 200 values, so the axis is absolutely crowded:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 100);
var data = d3.range(200);
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d}))
.range([10, 490]);
var xAxis = d3.axisBottom(xScale);
var gX = svg.append("g").call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
Now, the same code using tickValues:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 100);
var data = d3.range(200);
var xScale = d3.scaleBand()
.domain(data.map(function(d){ return d}))
.range([10, 490]);
var xAxis = d3.axisBottom(xScale)
.tickValues(xScale.domain().filter(function(d,i){ return !(i%10)}));
var gX = svg.append("g").call(xAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
In this last snippet, tickValues uses the remainder operator to show only 1 in every 10 ticks:
.tickValues(xScale.domain().filter(function(d,i){
return !(i%10)
}));
Here is a general solution to this problem using tickFormat(...). We can define a minimum acceptable width for our ticks, then skip every nth tick based on this minimum.
d3
.axisBottom(xScale)
.tickFormat((t, i) => {
const MIN_WIDTH = 30;
let skip = Math.round(MIN_WIDTH * data.length / chartWidth);
skip = Math.max(1, skip);
return (i % skip === 0) ? t : null;
});
let skip = ... is a rearrangement of the inequality ChartWidth / (NumTicks / N) > MinWidth. Here N represents the tick "step size", so we are asserting that the width of every nth tick is greater than the minimum acceptable width. If we rearrange the inequality to solve for N, we can determine how many ticks to skip to achieve our desired width.

Drawing a radial tree from node and edges rather than children

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))

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