Stacked Bar Chart With Smaller Bar Value
Solution 1:
I have converted the code to a D3v5 version.
No need to transform the data, you can work with objects.
I have no clue what you want with the layer1
and layer2
.
You have to add the mouseover
and mouseout
.
With logarithmic Y-axis. Using a 2-log is not common so I used 10-log. But now the Insolvencies look very big.
for the JavaScript (health
renamed to month
)
var data = [{
"month": "JAN",
"INSOLVENCIES": 1,
"SPV/ASSETBACKED": 67,
"OPERATINGCOMPANIES": 13,
"Bank": 15
}, {
"month": "FEB",
"INSOLVENCIES": 60,
"SPV/ASSETBACKED": 9,
"OPERATINGCOMPANIES": 20,
"Bank": 5
}, {
"month": "MAR",
"INSOLVENCIES": 40,
"SPV/ASSETBACKED": 22,
"OPERATINGCOMPANIES": 21,
"Bank": 99
}, {
"month": "APR",
"INSOLVENCIES": 60,
"SPV/ASSETBACKED": 1,
"OPERATINGCOMPANIES": 99,
"Bank": 90
}, {
"month": "MAY",
"INSOLVENCIES": 2,
"SPV/ASSETBACKED": 27,
"OPERATINGCOMPANIES": 43,
"Bank": 82
}, {
"month": "JUN",
"INSOLVENCIES": 17,
"SPV/ASSETBACKED": 52,
"OPERATINGCOMPANIES": 79,
"Bank": 9
}, {
"month": "JUL",
"INSOLVENCIES": 37,
"SPV/ASSETBACKED": 24,
"OPERATINGCOMPANIES": 35,
"Bank": 51
}, {
"month": "AUG",
"INSOLVENCIES": 16,
"SPV/ASSETBACKED": 17,
"OPERATINGCOMPANIES": 53,
"Bank": 38
}, {
"month": "SEP",
"INSOLVENCIES": 15,
"SPV/ASSETBACKED": 32,
"OPERATINGCOMPANIES": 5,
"Bank": 31
}];
let xKey = "month";
let keys = d3.keys(data[0]).filter( e => e != xKey );
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var margin = {
top: 20,
right: 50,
bottom: 30,
left: 50
},
width = 400,
height = 300,
padding = 100;
var x = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(.05);
var y = d3.scaleLog().range([height, 0]);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var xAxis = d3.axisBottom().scale(x);
var y_axis = d3.axisLeft().scale(y).ticks(6); //.innerTickSize(-width).tickPadding(10);var svg = d3.select("#ashu")
.append("svg")
.attr("width", "100%")
.attr("height", "100%");
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dataStackLayout = d3.stack().keys(keys)(data);
dataStackLayout[0].forEach(e => { e[0]=0.1; }); // do not start at 0
x.domain(dataStackLayout[0].map(function(d) { return d.data[xKey]; }));
var maximumY = d3.max(dataStackLayout[dataStackLayout.length - 1], function(d) { return d[1]; });
y.domain([0.1, maximumY]).base(10).nice();
g.append("g")
.selectAll("g")
.data(dataStackLayout)
.enter().append("g")
.attr("fill", function(d) { returncolor(d.key); })
.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { returnx(d.data[xKey]); })
.attr("y", function(d) { returny(d[1]); })
.attr("height", function(d) { returny(d[0]) - y(d[1]); })
.attr("width", x.bandwidth());
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(y_axis);
var legend = g.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return"translate(0," + Math.abs((i - 8) * 20) + ")"; });
legend.append("rect")
.attr("x", width + 10)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width + 32)
.attr("y", 10)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d, i) { return keys[i]; });
<scriptsrc="https://d3js.org/d3.v5.min.js"></script><divid="ashu"style="width: 700px; height:400px;"></div>
Edit
Instead of using a log-scale I have added a zoom functionality. The translation is done in the range
and that confused me. I found the needed equation but needed a tip on where to apply the limitation on translate from https://stackoverflow.com/a/44359905/9938317.
You have to place the labels conditionally when the height of the bar is tall enough or use tooltips when you do not place a label.
Adding the tooltips was a little puzzle. I don't know a way to get the data that is attached to a parent node so I copied the key of the group over to the individual bar rects. (Maybe https://stackoverflow.com/a/17459746/9938317 is a solution)
var data = [{
"month": "JAN",
"INSOLVENCIES": 1,
"SPV/ASSETBACKED": 67,
"OPERATINGCOMPANIES": 13,
"Bank": 15
}, {
"month": "FEB",
"INSOLVENCIES": 60,
"SPV/ASSETBACKED": 9,
"OPERATINGCOMPANIES": 20,
"Bank": 5
}, {
"month": "MAR",
"INSOLVENCIES": 40,
"SPV/ASSETBACKED": 22,
"OPERATINGCOMPANIES": 21,
"Bank": 99
}, {
"month": "APR",
"INSOLVENCIES": 60,
"SPV/ASSETBACKED": 1,
"OPERATINGCOMPANIES": 99,
"Bank": 90
}, {
"month": "MAY",
"INSOLVENCIES": 2,
"SPV/ASSETBACKED": 27,
"OPERATINGCOMPANIES": 43,
"Bank": 82
}, {
"month": "JUN",
"INSOLVENCIES": 17,
"SPV/ASSETBACKED": 52,
"OPERATINGCOMPANIES": 79,
"Bank": 9
}, {
"month": "JUL",
"INSOLVENCIES": 37,
"SPV/ASSETBACKED": 24,
"OPERATINGCOMPANIES": 35,
"Bank": 51
}, {
"month": "AUG",
"INSOLVENCIES": 16,
"SPV/ASSETBACKED": 17,
"OPERATINGCOMPANIES": 53,
"Bank": 38
}, {
"month": "SEP",
"INSOLVENCIES": 15,
"SPV/ASSETBACKED": 32,
"OPERATINGCOMPANIES": 5,
"Bank": 31
}];
let xKey = "month";
let keys = d3.keys(data[0]).filter( e => e != xKey );
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var margin = {
top: 20,
right: 50,
bottom: 30,
left: 50
},
width = 400,
height = 300,
padding = 100;
var x = d3.scaleBand()
.rangeRound([0, width])
.paddingInner(.05);
var y = d3.scaleLinear().range([height, 0]);
var color = d3.scaleOrdinal(d3.schemeCategory10);
var xAxis = d3.axisBottom().scale(x);
var yAxis = d3.axisLeft().scale(y).ticks(6); //.innerTickSize(-width).tickPadding(10);var svg = d3.select("#ashu")
.append("svg")
.attr("width", "100%")
.attr("height", "100%");
const clipPath = svg.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width)
.attr('height', height);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var dataStackLayout = d3.stack().keys(keys)(data);
// add the key to the bar elements
dataStackLayout.forEach(keyElem => {
var key = keyElem.key;
keyElem.forEach(d => { d.key = key; });
});
x.domain(dataStackLayout[0].map(function(d) { return d.data[xKey]; }));
var maximumY = d3.max(dataStackLayout[dataStackLayout.length - 1], function(d) { return d[1]; });
y.domain([0.1, maximumY]);
var gBars = g.append("g")
.attr("class", "bars")
.attr('clip-path', 'url(#clip)');
gBars.selectAll("g")
.data(dataStackLayout)
.enter().append("g")
.attr("fill", function(d) { returncolor(d.key); })
.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { returnx(d.data[xKey]); })
.attr("y", function(d) { returny(d[1]); })
.attr("height", function(d) { returny(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.append("title").text(function(d) { return d.key; });
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var gY = g.append("g")
.attr("class", "y axis")
.attr("transform", "translate(0,0)")
.call(yAxis);
var legend = g.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return"translate(0," + Math.abs((i - 8) * 20) + ")"; });
legend.append("rect")
.attr("x", width + 10)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width + 32)
.attr("y", 10)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d, i) { return keys[i]; });
var zoom = d3.zoom()
.scaleExtent([1, 40])
.on("zoom", zoomed);
svg.call(zoom);
d3.select("button")
.on("click", resetted);
functionzoomed() {
// https://stackoverflow.com/a/44359905/9938317var t = d3.event.transform;
t.y = d3.min([t.y, 0]);
t.y = d3.max([t.y, (1-t.k) * height]);
var yTransform = t.rescaleY(y);
gY.call(yAxis.scale(yTransform));
gBars.selectAll("rect")
.attr("y", function(d) { returnyTransform(d[1]); })
.attr("height", function(d) { returnyTransform(d[0]) - yTransform(d[1]); });
}
functionresetted() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity);
}
<scriptsrc="https://d3js.org/d3.v5.min.js"></script><button>Reset</button><divid="ashu"style="width: 700px; height:400px;"></div>
Solution 2:
It looks like your old code made some assumptions about the data format that your new data did not comply with. In the old code, the numbers were listed as strings. Also, the health
(month
) field was the first field in the dataset, not the second.
The following is one way to correct for these problems, but not the only way. I think it gets you moving in the right direction.
OLD ANSWER:
All I did was: 1) move month
to the first position in each object and 2) add a +
operator in your dataIntermediate
function for the y
value.
EDIT:
In light of your comment, I edited the code snippet to be tolerant of where the "month" field is.
These issues have as much to do with just JavaScript as D3. You can use any field you want for the y-axis of the graph, but you have to tell D3 what it will be.
I've updated the code snippet to set your yData
variable to "month"
manually. D3 doesn't make assumptions about the structure of your data, but your underlying JavaScript did with these lines:
let xData = d3.keys(data[0]);
const yData = xData.shift();
For xData
in my code snippet, those functions cycle through your data to gather up all the keys, then filter out any repeats, and then filter out the month
.
As for whether the each object has to have each field: it doesn't technically for the sake of D3, but at the same time D3 won't assume it is or isn't there. It all depends on how you code the chart. So, the short answer is that D3 isn't going to make assumptions about your data, and right now your code is telling D3 to attempt to draw points on the graph for data that isn't there.
If you look at your code snippet:
var dataIntermediate = xData.map(function(c) {
return data.map(function(d, yData) {
return {
x: d[Object.keys(d)[0]],
y: +d[c]
};
});
});
You're cycling through data
for every key listed as in your xData
and yData
and trying to access the data
for it. However, for some points in your data, that key/value pair won't necessarily exist. This means you'll end up with data you pass to D3 that looks like this:
[
{x:"7", y:4, y0:9}
{x:"3", y:NaN, y0:46}
{x:"6", y:27, y0:NaN}
{x:"54", y:15, y0:168}
]
D3 doesn't know how to translate and plot NaN
on a graph. I believe you could omit certain fields in your data, but you'd need to go through your code and ensure that you aren't attempting to process data that doesn't exist and pass it to D3. You also would need to go through and be sure you aren't try to access fields that aren't there in things like a tooltip.
var data = [
{
"Operating Company" : "7",
"month" : "March",
"Insolvency" : "2",
"SPV / Asset Backed" : "4",
"Operational Company" : "0",
"TBD": "0"
},
{
"Operating Company" : "3",
"month" : "July",
"Insolvency" : "43",
"TBD" : "1",
"Operational Company" : "0",
"SPV / Asset Backed" : "0",
}, {
"Operating Company" : "6",
"month" : "August",
"SPV / Asset Backed" : "27",
"Operational Company" : "0",
"Insolvency" : "0",
"TBD": "0"
},
{
"Operating Company" : "54",
"month" : "September",
"Insolvency" : "114",
"Operational Company" : "1",
"SPV / Asset Backed" : "15",
"TBD": "0"
}
];
let yData = "month";
const xData = data.reduce((accumulator, currentValue) => {
keys = Object.keys(currentValue);
keys.forEach(k => {
accumulator.push(k);
});
return accumulator;
}, [])
.filter((value, index, self) => self.indexOf(value) === index)
.filter(value => value !== "month");
var margin = {
top: 20,
right: 50,
bottom: 30,
left: 50
},
width = 400,
height = 300,
padding = 100;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .05);
var y = d3.scale.linear().range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var y_axis = d3.svg.axis().scale(y).orient("left").ticks(6).innerTickSize(-width)
.tickPadding(10);
var svg = d3.select("#ashu").append("svg").attr("width",
"100%").attr("height",
"100%").append("g").attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")");
var dataIntermediate = xData.map(function(c) {
return data.map(function(d, yData) {
return {
x: d[Object.keys(d)[0]],
y: +d[c]
};
});
});
var dataStackLayout = d3.layout.stack()(dataIntermediate);
x.domain(dataStackLayout[0].map(function(d) {
return d.x;
}));
var total = 0;
var maximumY = d3.max(dataStackLayout[dataStackLayout.length - 1], function(d) {
return (d.y + d.y0);
});
y.domain([0, maximumY]).nice();
var layer1 = svg.selectAll(".stack1").data(dataStackLayout).enter()
.append("g").attr("class", "stack").style("fill",
function(d, i) {
returncolor(i);
});
var layer2 = svg.selectAll(".stack2").data(dataStackLayout).enter()
.append("g")
.attr("class", "stack")
.style("fill", "black");
layer1.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d) {
returnx(d.x);
})
.attr("y", function(d) {
returny(d.y + d.y0);
})
.attr("height", function(d) {
returny(d.y0) - y(d.y + d.y0);
})
.attr("width", x.rangeBand())
.on("mouseover", function(d) {
// this code places your hover text at the bottom of the page// it is not required// you can modify it as you see fit, and even delete it
div.transition()
.duration(200)
.style("opacity", .9);
div.html("Total : " + (d.y + d.y0))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
// this code removes the text on the bottom of the screen when// you move the mouse off the bar
div.transition()
.duration(500)
.style("opacity", 0);
});
// this code places your text labels on the bars// it is not required// you can modify it as you see fit, and even just delete it
layer2.selectAll("text")
.data(function(d) {
return d;
})
.enter()
.append("text")
.text(function(d) {
return d.y;
})
.attr("x", function(d, i) {
returnx(d.x) + (x.rangeBand())/2;
})
.attr("y", function(d) {
returny(d.y0 + (d.y/2));
})
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.style("fill", "black");
svg.append("g").attr("class", "axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
svg.append("g").attr("class", "y axis").attr("transform", "translate(0,0)").call(y_axis);
var legend = svg.selectAll(".legend")
.data(color.domain().slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return"translate(0," + Math.abs((i - 8) * 20) + ")";
});
legend.append("rect")
.attr("x", width + 10)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width + 32)
.attr("y", 10)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d, i) {
return xData[i];
});
<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script><divid="ashu"style="width: 700px; height:400px;"></div>
Post a Comment for "Stacked Bar Chart With Smaller Bar Value"