I'm developing a stacked chart application.
http://jsfiddle.net/NYEaX/174/
I've placed it inside a jquery plugin to create multiple instances etc... different properties and eventually different data sources.
For now I am having problems animating the chart bars and the axis.
Animate bar code
animateBars: function(selector, data){
var w = $(selector).data("width");
var h = $(selector).data("height");
var margin = methods.getMargin(h);
methods.setDimensions(w, h, margin);
//methods.setX();
//methods.setY();
//methods.setDomain(data);
var initialHeight = 0;
//var svg = d3.select(selector + " .stackedchart");
var barholder = d3.select(selector + " .barholder");
var state = barholder.selectAll(".state")
.data(data)
.enter()
.append("g")
.attr("class", "g")
.attr("x", function(d) {
return methods.x(d.Label);
})
.attr("transform", function(d) {
return "translate(" + methods.x(d.Label) + ",0)";
});
var bar = state.selectAll("rect")
.data(function(d) {
return d.blocks;
});
// Enter
bar.enter()
.append("rect")
.attr("width", methods.x.rangeBand())
.attr("y", function(d) {
return methods.y(d.y1);
})
.attr("height", function(d) {
return methods.y(d.y0) - methods.y(d.y1);
})
.style("fill", function(d) {
return methods.color(d.name);
});
// Update
bar
.attr("y", function(d) {
return methods.y(d.y1);
})
.attr("height", function(d) {
return methods.y(d.y0) - methods.y(d.y1);
})
.transition()
.duration(500)
.attr("x", function(d) {
return methods.x(d.Label);
})
.attr("width", methods.x.rangeBand())
.attr("y", function(d) {
return methods.y(d.y1);
})
.attr("height", function(d) {
return methods.y(d.y0) - methods.y(d.y1);
});
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", function(d) {
return methods.y(d.y1);
})
.attr("height", function(d) {
return methods.y(d.y0) - methods.y(d.y1);
})
.remove();
}
One problem is that "state" is generated from the "enter()" method, so all your "bar" calls are only being executed when your "g.class" is being generated, not on update. Change this:
var state = barholder.selectAll(".state")
.data(data)
.enter()
.append("g")...
to this:
var state = barholder.selectAll(".state")
.data(data);
state.enter().append("g")...
See if that helps a bit. It doesn't seem to affect your fiddle, but you might be having problems other than d3. Try simplifying your fiddle and get the d3 stuff working by itself first.
Related
I'm newbie with D3.js and I have created a grouped bar chart. And I would like to put the value at the top of echa bar. But, I'm not able to do it :(
I have found several solutiones but I cannot do it works fine.
You can find all the code of my development here:
The function where I create all the bars is:
setBars(canvas, data, scales, keys, colors) {
let height = HEIGHT - OFFSET_TOP - OFFSET_BOTTOM;
let bar = canvas
.append("g")
.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + scales.x0Scale(d.shoot) + ",0)";
})
.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return { key: key, value: d[key] };
});
})
.enter()
.append("rect")
.attr("class", "rect")
.attr("x", function(d) {
return scales.x1Scale(d.key);
})
.attr("y", function(d) {
return scales.yScale(d.value);
})
.attr("width", scales.x1Scale.bandwidth())
.attr("height", function(d) {
return height - scales.yScale(d.value);
})
.attr("fill", function(d) {
return colors(d.key);
});
//set label over bar
bar
.selectAll("g")
.data(function(d) {
return d.value;
})
.enter()
.append("text")
.attr("class", "bar-text")
.attr("fill", "#000")
.attr("text-anchor", "middle")
.text(function(d) {
console.log("d.value: " + d.value);
return d.value;
})
.attr("x", function(d, i) {
return scales.x1Scale.bandwidth() * (i + 0.5);
})
.attr("y", function(d, i) {
return scales.yScale(d.value) + 8;
});
}
What am I doing wrong?
If you look at the chain in your bar selection you'll see that you're trying to append text elements to rects: bar is a selection of rectangles, not a selection of groups. Appending texts to rectangles, of course, won't work in an SVG.
The solution is breaking that selection. For instance:
//This is the group selection:
let bar = canvas
.append("g")
.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + scales.x0Scale(d.shoot) + ",0)";
});
//Here, you append rects to the groups:
bar.selectAll("rect")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
.enter()
.append("rect")
//etc...
//Finally, here, you append texts to the groups:
bar.selectAll("text")
.data(function(d) {
return keys.map(function(key) {
return {
key: key,
value: d[key]
};
});
})
.enter()
.append("text")
//etc...
As you can see, you'll have to change the data() method as well.
I'm trying to figure out the correct way to displays labels that will sit on top of each bar in my bar chart. I'd also like them to display a % after the number.
Here is my Plunker: https://plnkr.co/edit/FbIquWxfLjcRTg7tiX4E?p=preview
I experimented with using this code from the question found in the link below. However, I wasnt able to get it to work properly (meaning the whole chart failed to display)
Adding label on a D3 bar chart
var yTextPadding = 20;
svg.selectAll(".bartext")
.data(data)
.enter()
.append("text")
.attr("class", "bartext")
.attr("text-anchor", "top")
.attr("fill", "white")
.attr("x", function(d,i) {
return x(i)+x.rangeBand()/2;
})
.attr("y", function(d,i) {
return height-y(d)+yTextPadding;
})
.text(function(d){
return d;
});
This is the most straight forward way given your existing code:
// keep a reference to the g holding the rects
var rectG = g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter();
// append the rects the same way as before
rectG.append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", function(d) { return z(d.key); });
// now add the text to the g
rectG.append("text")
.text(function(d){
return d.value + '%';
})
.attr("x", function(d) { return x1(d.key) + (x1.bandwidth()/2); })
.attr("y", function(d) { return y(d.value); })
.style("text-anchor", "middle");
Updated plunker.
I have created group bar chart by using D3.js. Each group has 2 bars. When any bar is clicked it must show some data using custom alert box. Now the bar can click and it shows data.
var state = svg.selectAll(".TestSuite")
.data(data)
.enter().append("g")
.attr("class", "TestSuite")
.on("click", function(d,i) {
if(i==0){
Alert.render(d3.select(this).data()[0].FalseStatements);
}else{
Alert.render(d3.select(this).data()[0].TrueStatements);
}
})
.attr("transform", function (d) {
return "translate(" + x0(d.TestSuite) + ",0)";
});
But data is vary according to clicked bars. So how to identify each single bar within a single group uniquely.
Here "if condition" that I used does not do the thing I want.How do I correct it?
Thank you.
(Suppose one group of bar consists two bars, one shows true count and other shows false count for a particular scenario. When we click the bar which shows true count then it should appear "TrueStatements" which is already have in data.using d3.select(this).data()[0].TrueStatements can do this. And also when someone click the bar which shows false count then it should appear "FalseStatements" which is already have in data.using d3.select(this).data()[0].FalseStatements can do this. My question is how do we identify the bar which shows true count and the bar which shows false count uniquely for do this task.)
EDITED:
How I get the data for bar chart(This is inside a for loop)
originalDataSetForBarChart.push({
TestSuite: "TS"+treeIndex,
Pass: trueAppear,
Fail: falseAppear,
FalseStatements : falseStatement,
TrueStatements : trueStatement
});
Bar chart code
var margin = {
top: 20,
right: 10,
bottom: 30,
left: 40
},
width = 890 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .5);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.ordinal()
.range(["#4169E1", "#800080"]);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickFormat(d3.format(""));
var w = width + margin.left + margin.right;
var h = height + margin.top + margin.bottom;
var svg = d3.select(".chart1").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
//svg.call(tip);
var xg = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
var yg = svg.append("g")
.attr("class", "y axis");
yg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Count");
I append bars to this chart inside a setInterval function using following method.
function update() {
startTime_barChart = new Date().getTime();
for (var i = 0; i < data.length; i++) {
var testSuite = d3.keys(data[i]).filter(function (key) {
return key !== "TestSuite";
});
}
data.forEach(function (d) {
d.trueFalseCount = testSuite.map(function (name) {
return {
name: name,
value: +d[name]
};
});
});
x0.domain(data.map(function (d) {
return d.TestSuite;
}));
x1.domain(testSuite).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function (d) {
return d3.max(d.trueFalseCount, function (d) {
return d.value;
});
})]);
//making the x axis/y axis
xg.call(xAxis);
yg.call(yAxis);
//removing all the rectangles
svg.selectAll(".TestSuite").remove();
var tip_word;
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
tip_word= "<strong style='color:white'>"+
"Pass count :"+
"</strong>"+
" <span style='color:white'>" + d.True +
"</span></br>"+
"<strong style='color:white'>"+
"Fail count :"+
"</strong>"+
" <span style='color:white'>" + d.False +
"</span>";
return word;
});
var state = svg.selectAll(".TestSuite")
.data(data)
.enter().append("g")
.attr("class", "TestSuite")
.on("click", function(d,i) {
if(i%2 == 0){//How to set this condition
Alert.render(d3.select(this).data()[0].FalseStatements);
}else{
Alert.render(d3.select(this).data()[0].TrueStatements);
}
})
.attr("transform", function (d) {
return "translate(" + x0(d.TestSuite) + ",0)";
});
svg.call(tip);
state.selectAll("rect")
.data(function (d) {
return d.trueFalseCount;})
.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);
});
if(barChartLegentController==1){
var legend = svg.selectAll(".legend")
.data(testSuite.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; });
barChartLegentController=2;
}
endTime_barChart = new Date().getTime();
var totalbar = (endTime_barChart-startTime_barChart)/1000;
//alert('Total bar time : '+ totalbar+' seconds');
}
I'm not sure I fully understand what you are asking yet but the best way to identifying any element/entity is with an id, something like the following:
d3.select(this).attr(id, function(d, i) {return 'bar_' + i});
Add this inside the iterative function where you are creating your bars. In this way you will be able to select them from anywhere in your code with a d3.select('#bar_1).
If you only want to identify each bar it would be something like this:
var state = svg.selectAll(".TestSuite")
.data(data)
.enter().append("g")
.attr("id", function(d,i) {return 'bar_' + i})
.attr("class", "TestSuite")
.on("click", function(d,i) {
if(i==0){
Alert.render(d3.select(this).data()[0].FalseStatements);
}else{
Alert.render(d3.select(this).data()[0].TrueStatements);
}
})
.attr("transform", function (d) {
return "translate(" + x0(d.TestSuite) + ",0)";
});
In the case that you would like to identify each bar with an Id related to its contents (true or false statements) I would suggest something like the following:
var state = svg.selectAll(".TestSuite")
.data(data)
.enter().append("g")
.attr("class", "TestSuite")
.on("click", function(d,i) {
var barId;
if(i==0){
barId = 'falseBar_' + i;
Alert.render(d3.select(this).data()[0].FalseStatements);
}else{
barId = 'trueBar_' + i;
Alert.render(d3.select(this).data()[0].TrueStatements);
}
d3.select(this).attr('id', barId);
})
.attr("transform", function (d) {
return "translate(" + x0(d.TestSuite) + ",0)";
});
In any case, this will assign an unique Id to every bar (i.e. "bar_25" or "falseBar_14") to each bar, giving you an ideal way to identify each bar.
EDIT: After OP showed me the actual code they are working with, the following are my suggestions for a solution (which are actually on the same lines as the code above).
The code you should actually be tinkering with is the one below the code you posted. It is where the actual bars are rendered:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.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); });
My suggestion to add an id attribute to each bar would be the following:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("id", function(d, i) {return 'bar_' + i}) // <-- Edited line
.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); });
It is important that you understand why this, and not the code block you provided initially, is the pertinent one. As you well said, the first block renders each group of bars (hence the append("g") which stands for svg group). The second block starts with a append("rect") which means svg rectangle. This and other lines (i.e. style("fill")..., attr("x")... and attr("y")...) clearly give away that this block is the one dealing with the actual bars and not the groups.
When I add or remove a data element, the axes update correctly. It also looks like the "bars" object is updated with the correct the "x" and "height" values, but changes don't show up in the DOM.
Here's the update function:
updateChart: function() {
var chart = this,
xScale = this.scales.x,
yScale = this.scales.y,
data = this.collection.toJSON()
padding = this.config.barPadding;
xScale.domain(chart.collection.pluck(chart.config.xAttr));
yScale.domain([0, d3.max(chart.collection.pluck(chart.config.yAttr))]).nice();
var bars = this.svg.select(".bar").data(data);
bars.exit().remove();
bars.enter().append("rect")
.attr("class", "bar")
.attr("id", function(d) { return "bar-" + d[chart.config.xAttr]; })
.attr("data-x", function(d) { return d[chart.config.xAttr]; })
.attr("data-y", function(d) { return d[chart.config.yAttr]; });
chart.svg.select(".x.axis").transition().duration(1000).call(chart.xAxis);
chart.svg.select(".y.axis").transition().duration(1000).call(chart.yAxis);
bars.transition().duration(1000)
.attr("x", function(d) { return xScale(d[chart.config.xAttr]); })
.attr("y", function(d) { return yScale(d[chart.config.yAttr]); })
.attr("width", xScale.rangeBand())
.attr("height", function(d) { return chart.height - yScale(d[chart.config.yAttr]); });
console.log(bars);
}
I'm working on this bar chart application.
http://jsfiddle.net/NYEaX/166/
How do I
a) animate the bars so they grow from the bottom
b) morph the axis accordingly to the new data set
animateBars: function(data){
var svg = d3.select(methods.el["selector"] + " .barchart");
var barrects = d3.select(methods.el["selector"] + " .barrects");
var initialHeight = 0;
var bar = barrects.selectAll("rect")
.data(data);
// Enter
bar.enter()
.append("rect")
.attr("class", "bar")
.attr("y", initialHeight);
// Update
bar
.attr("height", initialHeight)
.transition()
.duration(500)
.attr("x", function(d) { return methods.x(d.letter); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.frequency); })
.attr("height", function(d) { return methods.height - methods.y(d.frequency); })
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", initialHeight)
.attr("height", initialHeight)
.remove();
},
For the former, set the y attribute to be the max height instead of 0:
.attr("y", methods.height)
For the latter, recompute the x domain and call the axis component again:
methods.x.domain(data.map(function(d) { return d.letter; }));
svg.select("g.x").call(methods.xAxis);
Complete example here.