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; });
Related
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
below is the code for a horizontal bar chart in its current form. I would like to move the value to the left side of the bar chart. I tried .attr("text-anchor", "start") but it didn't work. Any help is appreciated, and hopefully you can provide some comments to help explain some of the principles in formatting labels/values. Thanks!
var data = [{
"xAxis": "72.1"
}];
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 80
},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width])
.domain([0, 100]);
var svg = d3.select(".barChartContainer").append("svg")
.attr("width", 500)
.attr("height", 200)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
y.domain(data.map(function(d) {
return d.yAxis;
}));
var backgroundBar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("class", "barBackground")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("height", y.bandwidth())
.attr("width", function(d) {
return x(100);
});
var bar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("width", function(d) {
return x(d.xAxis);
});
var labels = svg.selectAll(null)
.data(data)
.enter()
.append("text")
var values = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", function(d) {
return y(d.yAxis) + y.bandwidth() / 2;
})
.attr("x", 10)
.text(function(d) {
return +d.xAxis
})
.attr("x", function(d) {
return x(d.xAxis) + 10;
});
Current bar chart
What I would like
Change the last line:
.attr("x", function(d) {
return x(d.xAxis) + 10;
});
And replace with:
.attr("x", x(-10));
I have this single horizontal bar chart and I want to make the following adjustments:
Show the tick value to the right of the bar chart, instead of by the axis.
Show a background for the bar chart scale instead of left and bottom axis.
Current version:
What I'd like to get to:
JS
var data = [
{"yAxis":"score", "xAxis":"72"}
];
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width]);
var svg = d3.select(".barChartContainer").append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 960 500")
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.xAxis; })])
y.domain(data.map(function(d) { return d.yAxis; }));
//y.domain([0, d3.max(data, function(d) { return d.prereqs; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("y", function(d) { return y(d.yAxis); })
.attr("height", y.bandwidth())
.transition()
.duration(1000)
.delay(function(d, i) {
return i * 100
})
.attr("width", function(d) {return x(d.xAxis); } );
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickValues(d3.range(x.domain()[0], x.domain()[1] + 1, 1))
.tickFormat(d3.format("d"))
);
svg.append("g")
.attr("class", "yAxis")
.call(d3.axisLeft(y));
I got this code from a codepen and I've been trying to adapt it, but it has been breaking so I stopped and was hoping you could help.
Thanks.
For showing the background for the bars, just copy your selection and chose a value of 100% for the rectangles, in a light gray fill:
var backgroundBar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
//etc...
.attr("width", function(d) {
return x(100);
});
Of course, you'll have to change the domain of the x scale:
var x = d3.scaleLinear()
.domain([0, 100]);
Then, drop both axis and print the labels using a text selection.
Finally, use another text selection for the values:
var values = svg.selectAll(null)
.data(data)
.enter()
.append("text")
//etc...
.text(function(d) {
return +d.xAxis
})
If you want, you can tween the text:
.attrTween("text", function(d) {
var self = this
var i = d3.interpolateNumber(0, +d.xAxis);
return function(t) {
return d3.select(self).text(~~i(t));
}
});
This is the result:
var data = [{
"yAxis": "score",
"xAxis": "72"
}];
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 80
},
width = 500 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width])
.domain([0, 100]);
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 200)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
y.domain(data.map(function(d) {
return d.yAxis;
}));
var backgroundBar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("fill", "lightgray")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("height", y.bandwidth())
.attr("width", function(d) {
return x(100);
});
var bar = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) {
return y(d.yAxis);
})
.attr("height", y.bandwidth())
.transition()
.duration(2000)
.delay(function(d, i) {
return i * 100
})
.attr("width", function(d) {
return x(d.xAxis);
});
var labels = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", function(d) {
return y(d.yAxis) + y.bandwidth() / 2;
})
.attr("x", -10)
.attr("text-anchor", "end")
.text(function(d) {
return d.yAxis
});
var values = svg.selectAll(null)
.data(data)
.enter()
.append("text")
.attr("y", function(d) {
return y(d.yAxis) + y.bandwidth() / 2;
})
.attr("x", 10)
.text(function(d) {
return +d.xAxis
})
.transition()
.duration(2000)
.delay(function(d, i) {
return i * 100
})
.attr("x", function(d) {
return x(d.xAxis) + 10;
})
.attrTween("text", function(d) {
var self = this
var i = d3.interpolateNumber(0, +d.xAxis);
return function(t) {
return d3.select(self).text(~~i(t));
}
});
<script src="https://d3js.org/d3.v4.min.js"></script>
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.
I'm a d3 novice trying to create a simple, two-series bar chart that transitions when different buttons are clicked. The original chart is constructed:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.ordinal()
.range(["#d4d4d4", "#58bd5b",]);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(".2s"));
var svg = d3.select("div.d3space").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("/assets/data/data3.csv", function(error, data) {
var hourBuckets = d3.keys(data[0]).filter(function(key) { return key !== "Client"; });
data.forEach(function(d) {
d.hours = hourBuckets.map(function(name) { return {name: name, value: +d[name]}; });
});
x0.domain(data.map(function(d) { return d.Client; }));
x1.domain(hourBuckets).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) { return d3.max(d.hours, function(d) { return d.value; }); })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Hours");
var client = svg.selectAll(".client")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.Client) + ",0)"; });
client.selectAll("rect")
.data(function(d) { return d.hours; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(hourBuckets.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
The csv being accessed is in the following format:
Client,Planned,Actual
ICC,25,50
RNR,50,47.5
MB,10,2.5
This chart renders as desired. The piece I am struggling with is getting this graph to transition to reflect different data when a link is clicked (link has id="fourweeks"). I have tried this onclick function:
window.onload = function() {
var a = document.getElementById("fourweeks");
var b = document.getElementById("eightweeks");
var c = document.getElementById("twelveweeks");
a.onclick = function() {
d3.csv("/assets/data/data1.csv", function(error, data) {
var hourBuckets = d3.keys(data[0]).filter(function(key) { return key !== "Client"; });
data.forEach(function(d) {
d.hours = hourBuckets.map(function(name) { return {name: name, value: +d[name]}; });
});
var client = svg.selectAll(".client")
client.selectAll("rect")
.data(function(d) { return d.hours; })
.transition()
.duration(1000)
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
});
}
}
...no dice. I can get this to work when creating / transitioning simple one-series bar charts that use list inputs, but not the multi-series csv ones. data2.csv is the exact same file as data1.csv, with the values adjusted slightly.
Thanks for your time reading - any advice?
First svg.selectAll(".client") returns an empty selection, because you gave these elements the class 'g' instead of 'client'.
Secondly you need to update the data of the .client-elements:
var client = svg.selectAll(".client")
.data(data);
btw. you should use selection.classed() instead of selection.attr('class')