How to convert D3js V2 to V4? - d3.js

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?

Related

How to get day of week on left side of D3 calendar heat map

I'm trying to create a calendar heatmap with D3, very similar to the Github contribution calendar.
I can't get the day of week to align correctly. It seems to repeat for every month and doesn't have correct margins or alignment. I only want the days to display once, on the left side of the calendar.
Just like this:
Here is what mine looks like:
Here is my code:
<style>
#calendar {
margin: 20px;
}
.month {
margin-right: 8px;
}
.month-name {
font-size: 85%;
fill: #777;
font-family: Muli, san-serif;
}
.day.hover {
stroke: #6d6E70;
stroke-width: 2;
}
.day.focus {
stroke: #ffff33;
stroke-width: 2;
}
</style>
<div style="text-align:center;" id="calendar"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script>
function drawCalendar(dateData){
var weeksInMonth = function(month){
var m = d3.timeMonth.floor(month)
return d3.timeWeeks(d3.timeWeek.floor(m), d3.timeMonth.offset(m,1)).length;
}
//var minDate = new Date(2018, 12, 31);
var minDate = d3.min(dateData, function(d) { return new Date(2018, 12, 1 ) });
//var minDate = d3.min(dateData, function(d) { return new Date(d.day) });
console.log(minDate);
//var maxDate = new Date(2019, 11, 30);
var maxDate = d3.max(dateData, function(d) { return new Date(2019, 11, 30 ) });
console.log(maxDate);
var cellMargin = 2,
calY=10,//offset of calendar in each group
xOffset=-5,
dayName = ['Su','Mo','Tu','We','Th','Fr','Sa'],
cellSize = 20;
var day = d3.timeFormat("%w"),
week = d3.timeFormat("%U"),
format = d3.timeFormat("%Y-%m-%d"),
titleFormat = d3.utcFormat("%a, %d-%b"),
monthName = d3.timeFormat("%B"),
months= d3.timeMonth.range(d3.timeMonth.floor(minDate), maxDate);
var svg = d3.select("#calendar").selectAll("svg")
.data(months)
.enter().append("svg")
.attr("class", "month")
.attr("height", ((cellSize * 7) + (cellMargin * 8) + 20) ) // the 20 is for the month labels
.attr("width", function(d) {
var columns = weeksInMonth(d);
return ((cellSize * columns) + (cellMargin * (columns + 1)));
})
.append("g")
svg.append("text")
.attr("class", "month-name")
.attr("y", (cellSize * 7) + (cellMargin * 8) + 15 )
.attr("x", function(d) {
var columns = weeksInMonth(d);
return (((cellSize * columns) + (cellMargin * (columns + 1))) / 2);
})
.attr("text-anchor", "middle")
.text(function(d) { return monthName(d); })
//create day labels
var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
var dayLabels=svg.append("g").attr("id","dayLabels")
days.forEach(function(d,i) {
dayLabels.append("text")
.attr("class","dayLabel")
.attr("x",xOffset)
.attr("y",function(d) { return calY+(i * cellSize); })
.text(d);
})
var rect = svg.selectAll("rect.day")
.data(function(d, i) { return d3.timeDays(d, new Date(d.getFullYear(), d.getMonth()+1, 1)); })
.enter().append("rect")
.attr("class", "day")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("rx", 3).attr("ry", 3) // rounded corners
.attr("fill", '#eaeaea') // default light grey fill
.attr("y", function(d) { return (day(d) * cellSize) + (day(d) * cellMargin) + cellMargin; })
.attr("x", function(d) { return ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellSize) + ((week(d) - week(new Date(d.getFullYear(),d.getMonth(),1))) * cellMargin) + cellMargin ; })
.on("mouseover", function(d) {
d3.select(this).classed('hover', true);
})
.on("mouseout", function(d) {
d3.select(this).classed('hover', false);
})
.datum(format);
rect.append("title")
.text(function(d) { return titleFormat(new Date(d)); });
var lookup = d3.nest()
.key(function(d) { return d.day; })
.rollup(function(leaves) {
return d3.sum(leaves, function(d){ return parseInt(d.count); });
})
.object(dateData);
var scale = d3.scaleLinear()
.domain(d3.extent(dateData, function(d) { return parseInt(d.count); }))
.range([0.2,1]); // the interpolate used for color expects a number in the range [0,1] but i don't want the lightest part of the color scheme
rect.filter(function(d) { return d in lookup; })
.style("fill", function(d) { return d3.interpolateYlGn(scale(lookup[d])); })
.select("title")
.text(function(d) { return titleFormat(new Date(d)) + ": " + lookup[d]; });
}
d3.csv("dates.csv", function(response){
drawCalendar(response);
})
</script>
There is also an input csv file that contains the following values:
day,count
2019-05-12,171
2019-06-17,139
2019-05-02,556
2019-04-10,1
2019-05-04,485
2019-03-27,1
2019-05-26,42
2019-05-25,337
2019-05-23,267
2019-05-05,569
2019-03-31,32
2019-03-25,128
2019-05-13,221
2019-03-30,26
2019-03-15,3
2019-04-24,10
2019-04-27,312
2019-03-20,99
2019-05-10,358
2019-04-01,15
2019-05-11,199
2019-07-06,744
2019-05-08,23
2019-03-28,98
2019-03-29,64
2019-04-30,152
2019-03-21,148
2019-03-19,20
2019-05-07,69
2019-04-29,431
2019-04-25,330
2019-04-28,353
2019-04-18,9
2019-01-10,1
2019-01-09,2
2019-03-26,21
2019-05-27,18
2019-04-19,10
2019-04-06,1
2019-04-12,214
2019-05-03,536
2019-07-03,3
2019-06-16,1
2019-03-24,138
2019-04-26,351
2019-04-23,14
2019-05-01,19
2019-07-05,523
2019-05-22,3
2019-05-09,430
2019-05-24,472
2019-04-11,172
2019-03-17,7
2019-05-14,10
2019-05-06,449
2019-07-04,295
2019-05-15,12
2019-03-23,216
2019-03-18,47
2019-03-22,179
Typically you allow for a margin in your SVG, something like this:
const margin = { top: 10, right: 20, bottom: 10, left: 5 }
const svg = d3
.select('#chart')
.append('svg')
.attr('width', 900 + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
Basically you create an SVG element that is bigger than your drawing area, and then you move (translate) the chart in by the margins. Then your axis can appear in the margin

Resize the SVGs according to windows size

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.

Generate multiple random paths and animate circles along paths

I am attempting to animate 3 circles along 3 paths, these paths are randomly generated.Only two paths are generating and only one circle is animating along its allocated path. I have tried pairing the dataset and circle, this has no effect.
One of the paths seems to be blending two dataset's to generate a monster path. How do i stop this? How do i get each circle to its allocated path?
There is probably a more elegant way to do this.
var w = 2000, h = 2000;
var dataset1 = [] ;
for (var i = 0; i < 5; i++) {
var x = Math.floor((Math.random()*900)+1);
var y = Math.floor((Math.random()*900)+1);
dataset1.push({"x":x, "y":y});
};
var dataset2 = [] ;
for (var i = 0; i < 4; i++) {
var x = Math.floor((Math.random()*700)+1);
var y = Math.floor((Math.random()*600)+1);
dataset2.push({"x":x, "y":y});
};
var dataset3 = [] ;
for (var i = 0; i < 3; i++) {
var x = Math.floor((Math.random()*800)+1);
var y = Math.floor((Math.random()*400)+1);
dataset2.push({"x":x, "y":y});
};
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate ("cardinal-closed")
.tension(0)
;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
;
var path1 = svg.append("path")
.datum( dataset1 )
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none")
;
var circle1 = svg.append("circle")
.attr("r", 130)
.attr("transform", "translate(" + [0] + ")")
;
var path2 = svg.append("path")
.datum( dataset2 )
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none")
;
var circle2 = svg.append("circle")
.attr("r", 30)
.attr("transform", "translate(" + [0] + ")")
;
var path3 = svg.append("path")
.datum( dataset2 )
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none")
;
var circle3 = svg.append("circle")
.attr("r", 10)
.attr("transform", "translate(" + [0] + ")")
;
transition();
function transition() {
circle1.transition()
.duration(10000)
.attrTween("transform", translateAlong(path1.node()))
.each("end", transition);
}
transition();
function transition() {
circle2.transition()
.duration(10000)
.attrTween("transform", translateAlong(path2.node()))
}
transition();
function transition() {
circle3.transition()
.duration(10000)
.attrTween("transform", translateAlong(path3.node()))
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<style>
path {
fill: none;
stroke: red;
stroke-width: 3px;
}
circle {
fill: red;
stroke: #fff;
stroke-width: 3px;
opacity: 0.7;
}
</style>
<script src="https://d3js.org/d3.v3.min.js"></script>
You had 3 problems with your code:
You were pushing dataset3 values into dataset2 array.
You were using dataset2 array for painting path3 element.
This is the most important problem: you had 3 functions with the same name. The last one simply overwrites the other ones. They should have different names. Alternatively, for DRY, write a general function and pass both the circle and the path as arguments.
Here is your code with those changes.
var w = 2000,
h = 2000;
var dataset1 = [];
for (var i = 0; i < 5; i++) {
var x = Math.floor((Math.random() * 900) + 1);
var y = Math.floor((Math.random() * 900) + 1);
dataset1.push({
"x": x,
"y": y
});
};
var dataset2 = [];
for (var i = 0; i < 4; i++) {
var x = Math.floor((Math.random() * 700) + 1);
var y = Math.floor((Math.random() * 600) + 1);
dataset2.push({
"x": x,
"y": y
});
};
var dataset3 = [];
for (var i = 0; i < 3; i++) {
var x = Math.floor((Math.random() * 800) + 1);
var y = Math.floor((Math.random() * 400) + 1);
dataset3.push({
"x": x,
"y": y
});
};
var lineFunction = d3.svg.line()
.x(function(d) {
return d.x;
})
.y(function(d) {
return d.y;
})
.interpolate("cardinal-closed")
.tension(0);
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
var path1 = svg.append("path")
.datum(dataset1)
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none");
var circle1 = svg.append("circle")
.attr("r", 130)
.attr("transform", "translate(" + [0] + ")");
var path2 = svg.append("path")
.datum(dataset2)
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none");
var circle2 = svg.append("circle")
.attr("r", 30)
.attr("transform", "translate(" + [0] + ")");
var path3 = svg.append("path")
.datum(dataset3)
.attr("d", lineFunction)
.attr("stroke", "black")
.attr("stroke-width", 3)
.attr("fill", "none");
var circle3 = svg.append("circle")
.attr("r", 10)
.attr("fill", "blue")
.attr("transform", "translate(" + [0] + ")");
transition();
function transition() {
circle1.transition()
.duration(10000)
.attrTween("transform", translateAlong(path1.node()))
.each("end", transition);
}
transition2();
function transition2() {
circle2.transition()
.duration(10000)
.attrTween("transform", translateAlong(path2.node()))
.each("end", transition2);
}
transition3();
function transition3() {
circle3.transition()
.duration(10000)
.attrTween("transform", translateAlong(path3.node()))
.each("end", transition3);
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
path {
fill: none;
stroke: red;
stroke-width: 3px;
}
circle {
fill: red;
stroke: #fff;
stroke-width: 3px;
opacity: 0.7;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
First and foremost, you are likely having a problem with the code because you are redefining the same function. You have 3 different definitions for the transition() function (one for each of the circles). Once that code is fixed, then we can move on to the next problem.

Making a grouped bar chart when my groups are varied sizes?

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.

D3.JS - how do I add gridlines to my pie chart

I have extended the pie-chart example at:
with pies that vary in radius depending on a percentage. I would like to add gridlines (circles) every 20 percent, but I can't figure out how.
here is the updated csv:
age,population,percent
<5,2704659,67
5-13,4499890,38
14-17,2159981,91
18-24,3853788,49
25-44,14106543,71
45-64,8819342,88
=65,612463,64
and here is the updated code with pie-parts of different radius:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
background: #333;
}
.arc path {
stroke: #fff;
stroke-width: 2px;
}
.arc grid {
stroke: #fff;
stroke-width: 1;
stroke-dasharray: 5,5;
}
.arc text {
fill:#fff;
font-size:12px;
font-weight:bold;
}
.arc line {
stroke: #fff;
}
</style>
<body>
<script src="d3.js"></script>
<script>
var width = 960,
height = 500,
radius = Math.min(width, height) / 2 - 10;
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var arc = d3.svg.arc()
.outerRadius(function(d) { return 50 + (radius - 50) * d.data.percent / 100; })
.innerRadius(20);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
var grid = d3.svg.area.radial()
.radius(150);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.population = +d.population;
d.percent = d.percent;
});
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.age); });
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
});
</script>
First set the number of ticks:
var numTicks = 5; // Each tick is 20%
Then create the data to create the gridlines:
var sdat = [];
for (i=0; i<=numTicks; i++) {
sdat[i] = (radius/numTicks) * i;
}
And then you can use a function to create the radial gridlines, and you can call it from within the d3.csv block:
addCircleAxes = function() {
var circleAxes, i;
svg.selectAll('.circle-ticks').remove();
circleAxes = svg.selectAll('.circle-ticks')
.data(sdat)
.enter().append('svg:g')
.attr("class", "circle-ticks");
// radial tick lines
circleAxes.append("svg:circle")
.attr("r", String)
.attr("class", "circle")
.style("stroke", "#CCC")
.style("opacity", 0.5)
.style("fill", "none");
// Labels for each circle
circleAxes.append("svg:text")
.attr("text-anchor", "center")
.attr("dy", function(d) { return d - 5 })
.style("fill", "#fff")
.text(function(d,i) { return i * (100/numTicks) });
};
An example is here: http://bl.ocks.org/3994129
(Borrowed from: http://kreese.net/blog/2012/08/26/d3-js-creating-a-polar-area-diagram-radial-bar-chart/)

Resources