Here's how's the D3.js look currently
What I want to achieve is that, when I resize the windows four tables inside needs resize accordingly. No new tables are added.
Currently it just keep adding new tables inside. How to correct this behavior?
The content of 1.json
[
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]],
[[1,3,3,5,6,7],[3,5,8,3,2,6],[9,0,6,3,6,3],[3,4,4,5,6,8],[3,4,5,2,1,8]]
]
The content of D3.js:
<!DOCTYPE html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.8/d3.min.js" type="text/JavaScript"></script>
<!--script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.9/d3.js" type="text/JavaScript"></script-->
<style>
rect {
stroke: #9A8B7A;
stroke-width: 1px;
fill: #CF7D1C;
}
svg{
width: 50%;
height: 50%;
}
</style>
<body>
</body>
<script>
function draw(){
d3.json("array_data/1.json", function(data){
for (i=0; i<data.length; ++i) {
main(data[i]);
}
})
}
function main(dataset){
var local = d3.local();
var svg = d3.select("body").append("svg"),
bBox = svg.node().getBoundingClientRect(),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = bBox.width - margin.left - margin.right,
height = bBox.height - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]);
var y = d3.scaleBand().rangeRound([0, height]);
y.domain(dataset.map(function(d,i) { return i; }));
var maxChildLength= d3.max(dataset, function(d) { return d.length; });
var xArr=Array.apply(null, {length: maxChildLength}).map(Function.call, Number);
x.domain(xArr);
var maxNum = d3.max(dataset, function(array) {
return d3.max(array);
});
var color=d3.scaleLinear().domain([0,maxNum]).range([0,1]);
svg.append("g")
.selectAll("g")
.data(dataset)//use top-level data to join g
.enter()
.append("g")
.selectAll("rect")
.data(function(d, i) {//for each <g>, use the second-level data (return d) to join rect
local.set(this, i);//this is the <g> parent
return d;
})
.enter()
.append("rect")
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
// return (local.get(this) * 20) + 40;
return y(local.get(this));
})
//.attr("width",20)
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill-opacity",function(d){console.log(color(+d));return color(+d);})
svg.append("g")
.selectAll("g")
.data(dataset)
.enter()
.append("g")
.selectAll("text")
.data(function(d, i) {
local.set(this, i)
return d;
})
.enter()
.append("text")
.text(function(d, i, j) {
return d;
})
.attr("x", function(d, i, j) {
// return (i * 20) + 40;
return x(i);
})
.attr("y", function(d) {
return y(local.get(this));
//return (local.get(this) * 20) + 40;
})
.attr("dx", x.bandwidth()/2)
.attr("dy", y.bandwidth()/2)
.attr("dominant-baseline", "central")//vertical - http://bl.ocks.org/eweitnauer/7325338
.attr("text-anchor", "middle")//horizontal - https://bl.ocks.org/emmasaunders/0016ee0a2cab25a643ee9bd4855d3464
.attr("font-family", "sans-serif")
.attr("font-size", "20px");
svg.append("g")
.append("text")
.attr("x", width/2)
.attr("y", height)
.attr("dominant-baseline", "text-before-edge")
.style("text-anchor", "middle")
//.attr("transform", "translate("+width/2+"," + height+ ")")
.text("Units sold");
}
draw();
window.addEventListener("resize", draw);
</script>
Using your method, you'll need to clear out the HTML first, then redraw. So, at the beginning of your main() function:
var element = d3.select('body');
element.innerHTML = '';
svg = d3.select(element).append('svg');
There are other methods for resizing (viewport, having a resize function), but this fits your code as it exists now.
Related
I tried to migrate D3js V2 to V4 of below example:
https://jasonneylon.wordpress.com/2013/09/05/two-sided-horizontal-barchart-using-d3-js/
But getting error while migrating:
Error: attribute y: Expected length, "NaN".
at line no 201:
.attr("y", function(d, z){ return y(z) + y.bandwidth()/2; } )
and
line no 223:
.attr("y", function(d){ return y(d) + y.bandwidth()/2; }
Please advice.
<!DOCTYPE html>
<html>
<head>
<title>Bar Chart</title>
<script src="http://d3js.org/d3.v4.js"></script>
<style type="text/css">
.chart {
background: #00ccff;
margin: 10px;
padding-top: 10px;
}
.chart .right {
stroke: white;
fill: indianred;
}
.chart .left {
stroke: white;
fill: steelblue;
}
.chart rect:hover {
fill: #64707d;
}
.chart text {
fill: white;
}
.chart text.name {
fill: black;
}
</style>
</head>
<body>
<h1>Two sided horiztontal bar chart</h1>
<script type="text/javascript">
var randomNumbers = function() {
var numbers = [];
for (var i = 0; i < 20; i++) {
numbers.push(parseInt(Math.random() * 19) + 1);
}
return numbers;
};
var randomNames = function() {
var names = [];
for (var i = 0; i < 20; i++) {
names.push(String.fromCharCode(65 + Math.random() * 25) + String.fromCharCode(65 + Math.random() * 25) + String.fromCharCode(65 + Math.random() * 25));
}
return names;
};
var names = randomNames();
var leftData = randomNumbers();
var rightData = randomNumbers();
for (var i= 0; i< names.length; i++) {
console.log(names[i] + " from: " + leftData[i] + " to: " + rightData[i]);
}
var labelArea = 160;
var chart,
width = 400,
bar_height = 20,
height = bar_height * (names.length);
var rightOffset = width + labelArea;
var chart = d3.select("body")
.append('svg')
.attr('class', 'chart')
.attr('width', labelArea + width + width)
.attr('height', height);
var xFrom = d3.scaleLinear()
.domain([0, d3.max(leftData)])
.range([0, width]);
var y = d3.scaleBand()
.domain(names)
.rangeRound([10, height]);
console.log('Y Range: '+y.range());
console.log('y.bandwidth(): '+y.bandwidth()); // 33
var yPosByIndex = function(d, index){ return y(index); }
chart.selectAll("rect.left")
.data(leftData)
.enter().append("rect")
.attr("x", function(pos) { return width - xFrom(pos); })
.attr("y", yPosByIndex)
.attr("class", "left")
.attr("width", xFrom)
.attr("height", y.bandwidth());
chart.selectAll("text.leftscore")
.data(leftData)
.enter().append("text")
.attr("x", function(d) { return width - xFrom(d); })
.attr("y", function(d, z){ return y(z) + y.bandwidth()/2; } )
.attr("dx", "20")
.attr("dy", ".36em")
.attr("text-anchor", "end")
.attr('class', 'leftscore')
.text(String);
chart.selectAll("text.name")
.data(names)
.enter().append("text")
.attr("x", (labelArea / 2) + width)
.attr("y", function(d){ return y(d) + y.bandwidth()/2; } )
.attr("dy", ".20em")
.attr("text-anchor", "middle")
.attr('class', 'name')
.text(String);
var xTo = d3.scaleLinear()
.domain([0, d3.max(rightData)])
.range([0, width]);
chart.selectAll("rect.right")
.data(rightData)
.enter().append("rect")
.attr("x", rightOffset)
.attr("y", yPosByIndex)
.attr("class", "right")
.attr("width", xTo)
.attr("height", y.bandwidth());
chart.selectAll("text.score")
.data(rightData)
.enter().append("text")
.attr("x", function(d) { return xTo(d) + rightOffset; })
.attr("y", function(d,z){ console.log(y(z)); return y(z) + y.bandwidth()/2; } )
.attr("dx", -5)
.attr("dy", ".36em")
.attr("text-anchor", "end")
.attr('class', 'score')
.text(String);
</script>
</body>
</html>
You call console.log(y(z)); and you get 40 undefined. And you don't investigate why?
What is the domain of y? Strings
So if you give it a number it most likely gives you a wrong answer.
The same reason why your function
var yPosByIndex = function(d, index){ return y(index); }
is wrong.
The main reason you have all these problems is that you have multiple arrays of information that are related based on the index. Create 1 array with objects that contain all the related data.
var data = d3.range(20).map(i => { return {name: randomName(), left:randomNumber(), right:randomNumber()}; } );
Now adjust your program to use d.name, d.left, d.right.
Don't use parseInt if you want to calculate the integer part of a number, it is slow and unclear what you want, use Math.floor()
Better to use the same xScale for the left and right bars. Why should a bar with value 10 be smaller on one of the sides?
How can I assign id base on its name?
the link of the screenshot of the console is down below.
Thanks
serie.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("class","bar")
.attr("x", function(d) { return x(d.data.Company); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("id",function(d,i){
return "id"+ d.name;
})
I discourage assigning ids to every bar as ids need to be unique. Its better to have a class instead. But as requested, I have posted a fiddle below. Hover on the bars to get the id. And you also said you need the id for animating so I've also added some animations when the chart loads (If that's what you meant by animation).
// Setup svg using Bostock's margin convention
var margin = {
top: 20,
right: 160,
bottom: 35,
left: 30
};
var width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
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 + ")");
/* Data in strings like it would be if imported from a csv */
var data = [{
mystate: "Intuit",
name: "Asian",
y0: 53,
y1: 1824,
value: "1771"
}, {
mystate: "Intuit",
name: "Latino",
y0: 2707,
y1: 1231,
value: "1771"
}, {
mystate: "Intuit",
name: "Black_or_African_American",
y0: 2060,
y1: 1824,
value: "1771"
}, {
mystate: "Intuit",
name: "Caucasian",
y0: 355,
y1: 1024,
value: "1771"
}];
// Transpose the data into layers
var dataset = d3.layout.stack()(['y0', 'y1'].map(function(values) {
return data.map(function(d) {
return {
x: d.name,
y: +d[values],
};
});
}));
// Set x, y and colors
var x = d3.scale.ordinal()
.domain(dataset[0].map(function(d) {
return d.x;
}))
.rangeRoundBands([10, width - 10], 0.02);
var y = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d3.max(d, function(d) {
return d.y0 + d.y;
});
})])
.range([height, 0]);
var colors = ["b33040", "#d25c4d", "#f2b447", "#d9d574"];
// Define and draw axes
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width, 0, 0)
.tickFormat(function(d) {
return d
});
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Create groups for each series, rects for each segment
var groups = svg.selectAll("g.cost")
.data(dataset)
.enter().append("g")
.attr("class", "cost")
.style("fill", function(d, i) {
return colors[i];
});
var rect = groups.selectAll("rect")
.data(function(d) {
return d;
})
.enter()
.append("rect")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return height;
})
.attr("height", function(d) {
return 0;
})
.attr("width", x.rangeBand())
.attr('id', function(d) {
return 'id' + d.x;
})
.on("mouseover", function() {
console.log(d3.select(this).attr('id'));
tooltip.style("display", null);
})
.on("mouseout", function() {
tooltip.style("display", "none");
})
.on("mousemove", function(d) {
var xPosition = d3.mouse(this)[0] - 15;
var yPosition = d3.mouse(this)[1] - 25;
tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")");
tooltip.select("text").text(d.y);
}).transition().duration(1000)
.delay(function(d, i) {
return i * 300;
})
.attr("height", function(d) {
return y(d.y0) - y(d.y0 + d.y);
}).attr("y", function(d) {
return y(d.y0 + d.y);
});
// Prep the tooltip bits, initial display is hidden
var tooltip = svg.append("g")
.attr("class", "tooltip")
.style("display", "none");
tooltip.append("rect")
.attr("width", 30)
.attr("height", 20)
.attr("fill", "white")
.style("opacity", 0.5);
tooltip.append("text")
.attr("x", 15)
.attr("dy", "1.2em")
.style("text-anchor", "middle")
.attr("font-size", "12px")
.attr("font-weight", "bold");
svg {
font: 10px sans-serif;
shape-rendering: crispEdges;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
}
path.domain {
stroke: none;
}
.y .tick line {
stroke: #ddd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.3.13/d3.min.js"></script>
Now coming back to your code, when you transform the data by using d3.layout.stack() there is no property called name in your new data (I suppose) and hence your ids are idundefined
This question is pretty such similar to this D3JS question but I am using latest D3 version(//d3js.org/d3.v4.min.js).
I am trying make this Normalized Stacked Bar Chart
chart horizontal. Is there any optimized way in latest version to achieve this?
I have swapped the x axis and y axis as below
var svg = d3.select("svg"),
margin = {top: 20, right: 60, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var y = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.align(0.1);
var x = d3.scaleLinear()
.rangeRound([height, 0]);
var z = d3.scaleOrdinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var stack = d3.stack()
.offset(d3.stackOffsetExpand);
d3.csv("data.csv", type, function(error, data) {
if (error) throw error;
data.sort(function(a, b) { return b[data.columns[1]] / b.total - a[data.columns[1]] / a.total; });
y.domain(data.map(function(d) { return d.State; }));
z.domain(data.columns.slice(1));
var serie = g.selectAll(".serie")
.data(stack.keys(data.columns.slice(1))(data))
.enter().append("g")
.attr("class", "serie")
.attr("fill", function(d) { return z(d.key); });
serie.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("y", function(d) { return y(d.data.State); })
.attr("x", function(d) { return x(d[1]); })
.attr("height", function(d) { return x(d[0]) - x(d[1]); })
.attr("width", y.bandwidth());
g.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(y));
g.append("g")
.attr("class", "axis axis--x")
.call(d3.axisLeft(x).ticks(10, "%"));
var legend = serie.append("g")
.attr("class", "legend")
.attr("transform", function(d) { var d = d[d.length - 1]; return "translate(" + (y(d.data.State) + y.bandwidth()) + "," + ((x(d[0]) + x(d[1])) / 2) + ")"; });
legend.append("line")
.attr("x1", -6)
.attr("x2", 6)
.attr("stroke", "#000");
legend.append("text")
.attr("y", 9)
.attr("dy", "0.35em")
.attr("fill", "#000")
.style("font", "10px sans-serif")
.text(function(d) { return d.key; });
});
function type(d, i, columns) {
for (i = 1, t = 0; i < columns.length; ++i) t += d[columns[i]] = +d[columns[i]];
d.total = t;
return d;
}
Referring the example
You will need to reverse the domains:
var y = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.align(0.1);
var x = d3.scaleLinear()
.rangeRound([height, 0]);
Swap the x to y as the domains are reverse so x will become y and y will become x, when you create the rectangles.
serie.selectAll("rect")
.data(function(d) {
return d;
})
.enter().append("rect")
.attr("y", function(d) {
return y(d.data.State);
})
.attr("x", function(d) {
return x(d[1]);
})
.attr("width", function(d) {
return x(d[0]) - x(d[1]);
})
.attr("height", y.bandwidth());
Change the legend position accordingly to position it on the top bar.
var legend = serie.append("g")
.attr("class", "legend")
.attr("transform", function(d) {
var d = d[0];//get the top data for placing legends on that.
return "translate(" + ((x(d[0]) + x(d[1])) / 2) + ", " +(y(d.data.State) - y.bandwidth())+ ")";
});
Finally position the legend lines:
legend.append("line")
.attr("y1", 5)
.attr("x1", 15)
.attr("x2", 15)
.attr("y2", 12)
.attr("stroke", "#000");
working code here
Below example also will help you
var initStackedBarChart = {
draw: function(config) {
me = this,
domEle = config.element,
stackKey = config.key,
data = config.data,
margin = {top: 20, right: 20, bottom: 30, left: 50},
parseDate = d3.timeParse("%m/%Y"),
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
xScale = d3.scaleLinear().rangeRound([0, width]),
yScale = d3.scaleBand().rangeRound([height, 0]).padding(0.1),
color = d3.scaleOrdinal(d3.schemeCategory20),
xAxis = d3.axisBottom(xScale),
yAxis = d3.axisLeft(yScale).tickFormat(d3.timeFormat("%b")),
svg = d3.select("#"+domEle).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 + ")");
var stack = d3.stack()
.keys(stackKey)
/*.order(d3.stackOrder)*/
.offset(d3.stackOffsetNone);
var layers= stack(data);
data.sort(function(a, b) { return b.total - a.total; });
yScale.domain(data.map(function(d) { return parseDate(d.date); }));
xScale.domain([0, d3.max(layers[layers.length - 1], function(d) { return d[0] + d[1]; }) ]).nice();
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) { return color(i); });
layer.selectAll("rect")
.data(function(d) { return d; })
.enter().append("rect")
.attr("y", function(d) { return yScale(parseDate(d.data.date)); })
.attr("x", function(d) { return xScale(d[0]); })
.attr("height", yScale.bandwidth())
.attr("width", function(d) { return xScale(d[1]) - xScale(d[0]) });
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + (height+5) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "axis axis--y")
.attr("transform", "translate(0,0)")
.call(yAxis);
}
}
var data = [{"date":"4/1854","total":8571,"disease":1,"wounds":0,"other":5},{"date":"5/1854","total":23333,"disease":12,"wounds":0,"other":9},{"date":"6/1854","total":28333,"disease":11,"wounds":0,"other":6},{"date":"7/1854","total":28772,"disease":359,"wounds":0,"other":23},{"date":"8/1854","total":30246,"disease":828,"wounds":1,"other":30},{"date":"9/1854","total":30290,"disease":788,"wounds":81,"other":70},{"date":"10/1854","total":30643,"disease":503,"wounds":132,"other":128},{"date":"11/1854","total":29736,"disease":844,"wounds":287,"other":106},{"date":"12/1854","total":32779,"disease":1725,"wounds":114,"other":131},{"date":"1/1855","total":32393,"disease":2761,"wounds":83,"other":324},{"date":"2/1855","total":30919,"disease":2120,"wounds":42,"other":361},{"date":"3/1855","total":30107,"disease":1205,"wounds":32,"other":172},{"date":"4/1855","total":32252,"disease":477,"wounds":48,"other":57},{"date":"5/1855","total":35473,"disease":508,"wounds":49,"other":37},{"date":"6/1855","total":38863,"disease":802,"wounds":209,"other":31},{"date":"7/1855","total":42647,"disease":382,"wounds":134,"other":33},{"date":"8/1855","total":44614,"disease":483,"wounds":164,"other":25},{"date":"9/1855","total":47751,"disease":189,"wounds":276,"other":20},{"date":"10/1855","total":46852,"disease":128,"wounds":53,"other":18},{"date":"11/1855","total":37853,"disease":178,"wounds":33,"other":32},{"date":"12/1855","total":43217,"disease":91,"wounds":18,"other":28},{"date":"1/1856","total":44212,"disease":42,"wounds":2,"other":48},{"date":"2/1856","total":43485,"disease":24,"wounds":0,"other":19},{"date":"3/1856","total":46140,"disease":15,"wounds":0,"other":35}];
var key = ["wounds", "other", "disease"];
initStackedBarChart.draw({
data: data,
key: key,
element: 'stacked-bar'
});
.axis text {
font: 10px sans-serif;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.path-line {
fill: none;
stroke: yellow;
stroke-width: 1.5px;
}
svg {
background: #f0f0f0;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id='stacked-bar'></div>
I am trying to make a bar chart out of some grouped data. This is dummy data, but the structure is basically the same. The data: election results includes a bunch of candidates, organized into the districts they were running in, and the total vote count:
district,candidate,votes
Dist 1,Leticia Putte,3580
Dist 2,David Barron,1620
Dist 2,John Higginson,339
Dist 2,Walter Bannister,2866
[...]
I'd like to create a bar or column chart (either, honestly, though my end goal is horizontal) that groups the candidates by district.
Mike Bostock has an excellent demo but I'm having trouble translating it intelligently for my purposes. I started to tease it out at https://jsfiddle.net/97ur6cwt/6/ but my data is organized somewhat differently -- instead of rows, by group, I have a column that sets the category. And there might be just one candidate or there might be a few candidates.
Can I group items if the groups aren't the same size?
My answer is similar to #GerardoFurtado but instead I use a d3.nest to build a domain per district. This removes the need for hardcoding values and cleans it up a bit:
y0.domain(data.map(function(d) { return d.district; }));
var districtD = d3.nest()
.key(function(d) { return d.district; })
.rollup(function(d){
return d3.scale.ordinal()
.domain(d.map(function(c){return c.candidate}))
.rangeRoundBands([0, y0.rangeBand()], pad);
}).map(data);
districtD becomes a map of domains for your y-axis which you use when placing the rects:
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d,i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
.attr("height", function(d){
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
I'm off to a meeting but the next step is to clean up the axis and get the candidate names on there.
Full running code:
var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {top: 10, right: 10, bottom: 50, left: 110},
w = 800 - m.left - m.right,
h = 500 - m.top - m.bottom,
pad = .1;
var x = d3.scale.linear().range([0, w]);
y0 = d3.scale.ordinal().rangeRoundBands([0, h], pad);
var color = d3.scale.category20c();
var yAxis = d3.svg.axis()
.scale(y0)
.orient("left");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickFormat(d3.format("$,.0f"));
var svg = d3.select("#chart").append("svg")
.attr("width", w + m.right + m.left + 100)
.attr("height", h + m.top + m.bottom)
.append("g")
.attr("transform",
"translate(" + m.left + "," + m.top + ")");
// This moves the SVG over by m.left(110)
// and down by m.top (10)
d3.csv(url, function(error, data) {
data.forEach(function(d) {
d.votes = +d.votes;
});
y0.domain(data.map(function(d) { return d.district; }));
districtD = d3.nest()
.key(function(d) { return d.district; })
.rollup(function(d){
console.log(d);
return d3.scale.ordinal()
.domain(d.map(function(c){return c.candidate}))
.rangeRoundBands([0, y0.rangeBand()], pad);
})
.map(data);
x.domain([0, d3.max(data, function(d) {
return d.votes;
})]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text");
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d,i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate); })
.attr("height", function(d){
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
svg.selectAll(".label")
.data(data)
.enter().append("text")
.text(function(d) {
return (d.votes);
})
.attr("text-anchor", "start")
.attr("x", function(d) { return x(d.votes)})
.attr("y", function(d) { return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand()/2;})
.attr("class", "axis");
});
.axis {
font: 10px sans-serif;
}
.axis path, .axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>
An alternate version which sizes the bars the same and scales the outer domain appropriately:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<style>
.label {
font: 10px sans-serif;
}
.axis {
font: 11px sans-serif;
font-weight: bold;
}
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var url = "https://gist.githubusercontent.com/amandabee/edf73bc0bbe131435c952f5ed47524a6/raw/99febb9971f76e36af06f1b99913fcaa645ecb3e/election.csv"
var m = {
top: 10,
right: 10,
bottom: 50,
left: 110
},
w = 800 - m.left - m.right,
h = 500 - m.top - m.bottom,
pad = .1, padPixel = 5;
var x = d3.scale.linear().range([0, w]);
var y0 = d3.scale.ordinal();
var color = d3.scale.category20c();
var yAxis = d3.svg.axis()
.scale(y0)
.orient("left");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(5)
.tickFormat(d3.format("$,.0f"));
var svg = d3.select("#chart").append("svg")
.attr("width", w + m.right + m.left + 100)
.attr("height", h + m.top + m.bottom)
.append("g")
.attr("transform",
"translate(" + m.left + "," + m.top + ")");
// This moves the SVG over by m.left(110)
// and down by m.top (10)
d3.csv(url, function(error, data) {
data.forEach(function(d) {
d.votes = +d.votes;
});
var barHeight = h / data.length;
y0.domain(data.map(function(d) {
return d.district;
}));
var y0Range = [0];
districtD = d3.nest()
.key(function(d) {
return d.district;
})
.rollup(function(d) {
var barSpace = (barHeight * d.length);
y0Range.push(y0Range[y0Range.length - 1] + barSpace);
return d3.scale.ordinal()
.domain(d.map(function(c) {
return c.candidate
}))
.rangeRoundBands([0, barSpace], pad);
})
.map(data);
y0.range(y0Range);
x.domain([0, d3.max(data, function(d) {
return d.votes;
})]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "middle");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text");
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", function(d, i) {
return color(d.district);
})
.attr("x", 0)
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate);
})
.attr("height", function(d) {
return districtD[d.district].rangeBand();
})
.attr("width", function(d) {
return x(d.votes);
});
var ls = svg.selectAll(".labels")
.data(data)
.enter().append("g");
ls.append("text")
.text(function(d) {
return (d.votes);
})
.attr("text-anchor", "start")
.attr("x", function(d) {
return x(d.votes)
})
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
})
.attr("class", "label");
ls.append("text")
.text(function(d) {
return (d.candidate);
})
.attr("text-anchor", "end")
.attr("x", -2)
.attr("y", function(d) {
return y0(d.district) + districtD[d.district](d.candidate) + districtD[d.district].rangeBand() / 2;
})
.style("alignment-baseline", "middle")
.attr("class", "label");
});
</script>
</body>
</html>
This is a partial solution: https://jsfiddle.net/hb13oe4v/
The main problem here is creating a scale for each group with a variable domain. Unlike Bostock's example, you don't have the same amount of bars(candidates) for each group(districts).
So, I had to do a workaround. First, I nested the data in the most trivial way:
var nested = d3.nest()
.key(function(d) { return d.district; })
.entries(data);
And then created the groups accordingly:
var district = svg.selectAll(".district")
.data(nested)
.enter()
.append("g")
.attr("transform", function(d) { return "translate(0," + y(d.key) + ")"; });
As I couldn't create an y1 (x1 in Bostock's example) scale, I had to hardcode the height of the bars (which is inherently bad). Also, for centring the bars in each group, I created this crazy math, that puts one bar in the center, the next under, the next above, the next under and so on:
.attr("y", function(d, i) {
if( i % 2 == 0){ return (y.rangeBand()/2 - 10) + (i/2 + 0.5) * 10}
else { return (y.rangeBand()/2 - 10) - (i/2) * 10}
})
Of course, all this can be avoided and coded way more elegantly if we could set a variable scale for each group.
This example of a stacked-to-grouped bar transition is beautiful. However, it's functioning off of a random number generator and for the life of me, I cannot figure how to replace that with my own data set.
http://bl.ocks.org/mbostock/3943967
How do get this stacked-to-grouped bar transition to import and work with a .csv file instead of the random data generator?
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 14px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
</style>
<form>
<label><input type="radio" name="mode" value="grouped"> Grouped</label>
<label><input type="radio" name="mode" value="stacked" checked> Stacked</label>
</form>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 1000 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function() { return bumpLayer(m, .1); })),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.rangeRound([height, 0]);
var color = d3.scale.ordinal()
.range(["#aa0000", "#ffff66", "#99ff99", "#00aa00"]);
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(0)
.tickPadding(6)
.orient("bottom");
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("data.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Year"; }));
data.forEach(function(d) {
var y0 = 0;
d.power = color.domain().map(function(name) { return {name: name, y0: y0, y1: y0 += +d[name]}; });
d.total = d.power[d.power.length - 1].y1;
});
data.sort(function(b, a) { return b.total - a.total; });
x.domain(data.map(function(d) { return d.Year; }));
y.domain([0, d3.max(data, function(d) { return d.total; })]);
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("y", -8)
.attr("x", 40)
.attr("dy", "0em")
.style("text-anchor", "end")
.text("Power (Mw)");
var year = svg.selectAll(".year")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x(d.Year) + ",0)"; });
year.selectAll("rect")
.data(function(d) { return d.power; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 800)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 770)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "begin")
.text(function(d) { return d; });
});
rect.transition()
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
d3.selectAll("input").on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"grouped\"]").property("checked", true).each(change);
}, 2000);
function change() {
clearTimeout(timeout);
if (this.value === "grouped") transitionGrouped();
else transitionStacked();
}
function transitionGrouped() {
y.domain([0, yGroupMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("x", function(d, i, j) { return x(d.x) + x.rangeBand() / n * j; })
.attr("width", x.rangeBand() / n)
.transition()
.attr("y", function(d) { return y(d.y); })
.attr("height", function(d) { return height - y(d.y); });
}
function transitionStacked() {
y.domain([0, yStackMax]);
rect.transition()
.duration(500)
.delay(function(d, i) { return i * 10; })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.transition()
.attr("x", function(d) { return x(d.x); })
.attr("width", x.rangeBand());
}
</script>
I think you might be missing a level in your data. It looks like data in your example is an array of objects. But in Mike's example he's using an array of arrays of objects:
[
[{x:, y:}, {x:, y:},...],
[{x:, y:}, {x:, y:},...],
...
]
The first level in the array is the "layers", which represent the number of bars in each stack or group. There is a <g> element created per layer that will contain all 58 bars in that layer.
The second level in the array represents the bars themselves within each layer.
You are probably going to have a difficult time representing this structure as a CSV. You might be better off storing the data on the server as JSON. If you need to use CSV for some reason you'll need to figure out how to represent the inner array without using commas. One option is to add a column called "layer" and assign a layer index value to each row/object that can be used to turn the flat CSV data into a nested array on the client.
Old question, but been working on the same problem; hope this helps someone. Really just expanding on what Scott responded with below. Mike Bostock, this is some amazing work BTW.
1) Comment out the current layers array that is generated using the test generator functions (you can also delete/comment-out the test generator functions)
2) Here is a simple example of what a new nested layers array would look like:
layers = [
[
{"x":0,"y":1.5,"y0":0}
],
[
{"x":0,"y":1.5,"y0":1.5}
]
];
3) Whatever data you use, you still need to someone populate the n [# of layers/columns per sample period] and m [number of samples total OR number of periods] variables to match the array(s) that you loaded into layers
4) y0 governs the difference between the array[0] and array[1] for the stacking view - haven't quite figured out how function(d) returns the values needed for y0 so working around it with my source data for now
5) As you start adding new periods or what Mike calls "samples" in the comments, you group each "layer" in the separate period array (which is equal to the number of layers you are plotting). Notice how y0 is just the aggregation of all the prior y coordinates. Example for n = 4, m=2:
layers = [
[
{"x":0,"y":0.5,"y0":0},
{"x":1,"y":1.5,"y0":0}
],
[
{"x":0,"y":2.5,"y0":0.5},
{"x":1,"y":1.5,"y0":1.5}
],
[
{"x":0,"y":0.5,"y0":3.0},
{"x":1,"y":1.5,"y0":3.0}
],
[
{"x":0,"y":2.5,"y0":3.5},
{"x":1,"y":1.5,"y0":4.5}
]
];
Let us assume this is your json data:
[
{"a":"uno", "b":11, "c":21},
{"a":"duo", "b":12, "c":22},
{"a":"tre", "b":13, "c":23}
]
The structure of array layers is then as follows:
[
[
{x:"uno", y:11, y0:0},
{x:"duo", y:12, y0:0},
{x:"tre", y:13, y0:0}
],
[
{x:"uno", y:21, y0:11},
{x:"duo", y:22, y0:12},
{x:"tre", y:23, y0:13}
]
}
The two inner arrays correspond to the two layers in the graph.
To properly transform the data from json to stack, you need to go layer by layer. It the code below, cats is an array containing the names of the groups or categories ["uno", "duo", "tre"] and m is their number(3); vals is an array of layer labels ["b", "c"] and n is their number (2). The function readLayer gets called twice, once per layer.
var stack = d3.layout.stack(),
layers = stack(d3.range(n).map(function(i) { return readLayer(m,i);})),
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y;});}),
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y;});});
function readLayer(m,r){
var a = [], i;
for (i = 0; i < m; ++i) { a[i] = data[i][vals[r]]; }
return a.map(function(d, i) { return {x: cats[i], y: a[i]};});
};
Please note that the value y0 is not returned by readLayer; it gets created in the stack function.