I have a stacked bar chart. The jsfiddle is here.
I have added a text which is actually the sum of all stacks on top of each bar.
layer.selectAll("rect")
.data(data)
.enter().append("text")
.attr("text-anchor", "middle")
.attr("x", function (d) { return x(d[chartType]) + x.rangeBand() / 2; })
.attr("y", function(d)
{
return y(sumOfStack(chartType, data, d[chartType], xData)) - 5;
})
.text(function(d)
{
return sumOfStack(chartType, data, d[chartType], xData);
})
.style("fill", "4682b4");
And the sumOfStack() method returns the value of y actually. It sums up all the stack values.
function sumOfStack(dataType, data, xValue, sources)
{
var sum = 0;
for(var i=0; i<data.length;i++){
if(data[i][dataType] != xValue) continue;
for(var j=0; j<sources.length; j++)
{
sum+= data[i][sources[j]];
}
console.log(sum);
return sum;
}
}
What is happening here is,
So what is happening here, first time, the monthly chart is drawn and the value on the top is correct. But, when I click on the quarter/year, the values are not recalculating.
What's the problem here?
You are only using the append part of the selection, which takes care of new elements, while the texts that you need to update already exists, so they don't belong to the enter() part but to the existing selection.
Replace the totals with this code and it should work:
var totals = svg.selectAll('text.totalLabels')
.data(data);
totals
.transition()
.duration(500)
.attr("x", function (d) { return x(d[chartType]) + x.rangeBand() / 2; })
.attr("y", function(d)
{
return y(sumOfStack(chartType, data, d[chartType], xData)) - 5;
})
.text(function(d)
{
return sumOfStack(chartType, data, d[chartType], xData);
});
totals.enter().append('text')
.attr('class', 'totalLabels')
.attr("text-anchor", "middle")
.attr("x", function (d) { return x(d[chartType]) + x.rangeBand() / 2; })
.attr("y", function(d)
{
return y(sumOfStack(chartType, data, d[chartType], xData)) - 5;
})
.text(function(d)
{
return sumOfStack(chartType, data, d[chartType], xData);
})
.style("fill", "4682b4");
totals.exit().remove();
Related
I'm doing a visual project to show natural disaster in 1900-2018 using d3. I want add an interactive action that one can choose the first year and last year to show.
Originally I create the picture as the following:
d3.csv("output.csv", rowConventer, function (data) {
dataset = data;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([padding, width - padding])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0,
d3.max(dataset, function (d) {
return d.AllNaturalDisasters;
})])
.range([height - padding, padding])
.nice();
stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
series = stack(dataset);
gr = svg.append("g");
groups = gr.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
})
.attr("class", "groups");
rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
d3.select("button")
.on("click", choosePeriod);
I have simplified some code to make my question simple. At the last row, I add an event listener to achieve what I described above. And the update function is choosePeriod. Now it is as following:
function choosePeriod() {
firstYear = parseInt(document.getElementById("FirstYear").value);
lastYear = parseInt(document.getElementById("LastYear").value);
d3.csv("output.csv", rowConventer, function (newdata) {
dataset = newdata;
series=stack(dataset);
groups.data(series);
groups.selectAll("rect")
.data(function (d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
});
groups.selectAll("rect")
.data(function (d) {
return d;
})
.exit()
.remove();
})
}
The change of dataset is achieved by rowConventer, which is not important in this question. Now the functionchoosePeriod is not running as envisioned! Theenter and the exit and update are all not work well! The whole picture is a mess! What I want is, for instance, if I input the firstYear=1900 and the lastYear=2000, then the picture should be updated with the period 1900-2000 to show. How can I achieve it?
I am unfamiliar the arrangement of the entire structure, I mean, at some place using d3.select() by class or id instead of label is better, right?
It looks like you've dealt with the enter and the exit selections. The only bit you're missing is the update selection, which will deal with the rectangles that already exist and don't need adding or removing. To do this copy your update pattern but just remove the enter().append() bit, e.g.:
groups.selectAll("rect")
.data(function (d) {
return d;
})
.attr("x", function(d, i) {
return xScales(i);
})
.attr("y", function(d) {
return yScales(d[1]);
})
.attr("height", function(d) {
return yScales(d[0]) - yScales(d[1]);
})
.attr("width", xScales.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;// the rectangle, parent of the title
var g = rect.parentNode;// the g, parent of the rect.
return d.data.Year + ", " + d3.select(g).datum().key + "," + (d[1]-d[0]);
})
I am new to d3. I created a bar chart. Append text and percentage in the bar chart with animation. When bar chart draw then the number and percentage go from bottom to top to the desired location. Here is the code
svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr("class", "g rect")
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.label); })
.attr("y", h)
.on("mouseover", onMouseOver) //Add listener for the mouseover event
... // attach other events
.transition()
.ease(d3.easeLinear)
.duration(2000)
.delay(function (d, i) {
return i * 50;
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)); })
.attr("width", x.bandwidth() - 15) // v4’s console.log(bands.bandwidth()) replaced v3’s console.log(bands.rangeband()).
.attr("height", function(d) { return h - y(d.percentage.slice(0, -1)); }) // use .slice to remove percentage sign at the end of number
.attr("fill", function(d) { return d.color; });
var legend = svg.append("g");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.value })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 15; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1) / 2);}) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.style("stroke", "papayawhip")
.style("fill", "papayawhip");
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Now I want to apply text transition. Like instead of just printing say 90%(d.percentage). I want that it starts from 0 and goes to d.percentage gradually. How can I apply text transition in this case. I tried the following but it didn't work
svg.selectAll(".g.rect").append("text")
.text(function(d) { return d.percentage; })
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
d3.select(this).text(i(t));
};
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
The problem is the this value.
Save it in the closure (that).
Use a number interpolator so you can round the result to a number of decimals.
var ptag = d3.select('body').append('p');
ptag.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, 90); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(2));
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
Your problem is that you return function(t) { ... } and try to access this of parent function inside. The solution is to return arrow function, which does not shadow this value:
return t => d3.select(this).text(i(t));
(by the way, you may also want to round percentage before printing)
-------------Edit --------------
Here is the working code
var numberFormat = d3.format("d");
svg.selectAll(".g.rect").append("text")
.attr("x", function(d) { return x(d.label) + x.bandwidth() / 2 - 20; })
.attr("y", h)
.transition()
.ease(d3.easeLinear)
.duration(2000)
.tween("text", function(d) {
var element = d3.select(this);
var i = d3.interpolate(0, d.percentage.slice(0, -1));
return function(t) {
var percent = numberFormat(i(t));
element.text(percent + "%");
};
//return t => element.text(format(i(t)));
})
.attr("y", function(d) { return y(d.percentage.slice(0, -1)) - 10; }) // use slice to remove percentage sign from the end of number
.attr("dy", ".35em")
.attr("fill", function(d) { return d.color; });
Thanks :)
I have displayed the count of the bar graphs here. But when the value in the dataset of the bar is 0, it displays 0 and disrupts the y label. i want to display the values appropriately and only when it's non zero. The values shouldnot mess with the axes. How can i do that?
Here is the working fiddle
https://jsfiddle.net/84hp8m6p/1/
Here's what i am using to append text
groups.selectAll('.bartext')
.data(function(d) {
return d;
})
.enter()
.append("text")
.attr("class", "bartext")
.attr("text-anchor", "end")
.attr("fill", "black")
.attr('x', function(d) {
return xScale(d.x0) + xScale(d.x);
})
.attr('y', function(d, i) {
return yScale(d.y) + yScale.rangeBand() / 2;
})
.text(function(d) {
return d.x;
});
Use a ternary operator to show only values greater than zero (or any other value):
.text(function(d) {
return (d.x > 0) ? d.x : "";
});
Here is your updated fiddle: https://jsfiddle.net/1gx3kcz4/
I would like to reuse the general update pattern III for a project and
want to know how to make the text labels line up better with the circle elements. My experiment is to attach circle elements and text to the "g", but I cannot place the text labels correctly.
Here is how I modified the block:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
text {
font: bold 28px monospace;
}
.enter {
fill: green;
}
.update {
//fill: #333;
fill: red;
}
.exit {
//fill: brown;
fill: blue;
}
</style>
<body>
<script src="../d3.v3.js"></script>
<script>
function randomData(){
return d3.range(~~(Math.random()*50)+1).map(function(d, i){return ~~(Math.random()*100);});
}
var alphabet = "";
var numlist = [];
var randomEntry;
for (i = 0; i< 2; i++) {
randomEntry = randomData();
numlist.push( randomEntry);
}
var temp = numlist.toString();
var temp2 = temp.split('"');
alphabet = temp2.pop();
console.log("alphabet", alphabet);
var temp3 = alphabet.toString();
console.log("temp3", temp3);
console.log("temp3 type", typeof(temp3));
var temp4 = alphabet.split(",");
alphabet = temp4;
console.log("alphabet", alphabet);
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(32," + (height / 2) + ")");
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data, function(d) { return d; });
var circles = svg.selectAll("circle")
.data(data, function(d) { return d; });
// UPDATE
// Update old elements as needed.
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d,i) { return (Math.random(i))*100;})
.attr("cy", function(d,i) { return (Math.random(i))*100;})
.attr("transform", "translate(200," + (-100) + ")");
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d,i) { return (Math.random(i))*100; })
.attr("y", function(d,i) { return (Math.random(i))*100; })
.attr("transform", "translate(200," + (-100) + ")");
// ENTER
// Create new elements as needed.
circles.enter().append("circle")
.attr("class", "enter")
.attr("opacity", 0.3)
.attr("r", 25)
.attr("cy", function(d,i) { return (Math.random(i))*270;})
.attr("cx", function(d,i) { return (Math.random(i))*270;})
.style("fill-opacity", 1e-6)
.transition()
.duration(750)
.attr("r", 30)
.style("fill-opacity", 1);
text.enter().append("text")
.attr("class", "enter")
.attr("dy", ".25em")
.attr("x", function(d) { return (Math.random(i))*100; })
.attr("y", function(d) { return (Math.random(i))*100; })
.style("fill-opacity", 1e-6)
.text(function(d) { return d; })
.transition()
.duration(750)
.style("fill-opacity", 1);
// EXIT
// Remove old elements as needed.
text.exit()
.attr("class", "exit")
.transition()
.duration(750)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.remove();
circles.exit()
.attr("class", "exit")
.transition()
.duration(750)
.style("fill-opacity", 1e-6)
.remove();
}
// The initial display.
update(alphabet);
// Grab a random sample of letters from the alphabet, in alphabetical order.
setInterval(function() {
update(shuffle(alphabet)
.slice(0, Math.floor(Math.random() * 26))
.sort());
}, 1500);
// Shuffles the input array.
function shuffle(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m], array[m] = array[i], array[i] = t;
}
return array;
}
</script>
How can I change this so the text labels appear next to the circle elements? Thanks for any assistance.
You seem to making a random data for determining circle DOM's cx and cy of the circle:
.attr("cy", function(d,i) { return (Math.random(i))*270;})
.attr("cx", function(d,i) { return (Math.random(i))*270;})
In text DOM you make random points for determining x and y of text.
.attr("x", function(d) { return (Math.random(i))*100; })
.attr("y", function(d) { return (Math.random(i))*100; })
So as a fix you can have a common data which will decide the x/y for text and cx/cy for circle.
BY making a function like this:
function randomData() {
return (Math.random() * 500);//his generates a single random point
}
var alphabet = [];
function randomEntry() {
var numlist = [];
var randomEntry;
for (i = 0; i < 5; i++) {
//generate 5 random coordinate
//here first element willdecide the x and second element decide the y.
numlist.push([randomData(), randomData()]);
}
//this will contain 5 coordinate points.
return numlist;
}
Then set the 5 coordinates point data in the text and circel like this:
var text = svg.selectAll("text")
.data(data, function(d) {
return d;
});
var circles = svg.selectAll("circle")
.data(data, function(d) {
return d;
});
Then in the update
circles.attr("class", "update")
.transition()
.duration(750)
.attr("opacity", 0.3)
.attr("cx", function(d, i) {
return d[0];//here d[0] is the x coordinate which determine the circle center x
})
.attr("cy", function(d, i) {
return d[1];//here d[1] is the y coordinate which determine the circle center y
})
text.attr("class", "update")
.transition()
.duration(750)
.attr("x", function(d, i) {
return d[0];
})
.attr("y", function(d, i) {
return d[1];
})
Working code here
Hope this helps!
I am updating a pie chart with a dynamic JSON feed. My update function is below
function updateChart(data) {
arcs.data(pie(data)); // recompute angles, rebind data
arcs.transition()
.ease("elastic")
.duration(1250)
.attrTween("d", arcTween)
sliceLabel.data(pie(data));
sliceLabel.transition()
.ease("elastic").duration(1250)
.attr("transform", function (d) {
return "translate(" + arc2.centroid(d) + ")";
})
.style("fill-opacity", function (d) {
return d.value == 0 ? 1e-6 : 1;
});
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
When the JSON has 0 values for all objects, the arcs and labels disappear. Exactly what I want to happen.
The problem is when I pass a new JSON after one that was full of zeros, the labels come back and tween etc but the arcs never redraw.
Any suggestions on correcting my update function so that the arcs redraw correctly after they their d values have been pushed to zero?
-- edit --
Lars suggested below that I use the .enter() exactly in the same way as I did when I created the graph. I tried doing this but the results did not change. See new update function below.
this.updatePie = function updateChart(data) {
arcs.data(pie(data))
.enter()
.append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d
})
arcs.transition()
.ease("elastic")
.duration(1250)
.attrTween("d", arcTween)
sliceLabel.data(pie(data));
sliceLabel.transition()
.ease("elastic").duration(1250)
.attr("transform", function (d) {
return "translate(" + arc2.centroid(d) + ")";
})
.style("fill-opacity", function (d) {
return d.value == 0 ? 1e-6 : 1;
});
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
You've actually hit a bug in D3 there -- if everything is zero, the pie layout returns angles of NaN, which cause errors when drawing the paths. As a workaround, you can check whether everything is zero and handle this case separately. I've modified your change function as follows.
if(data.filter(function(d) { return d.totalCrimes > 0; }).length > 0) {
path = svg.selectAll("path").data(pie(data));
path.enter().append("path")
.attr("fill", function(d, i) { return color(d.data.crimeType); })
.attr("d", arc)
.each(function(d) { this._current = d; });
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
} else {
path.remove();
}
Complete jsbin here.