I created a bar chart with a simple .csv data. The first column is "Team", which contains team names as text; The second column is "Goals", which contains number of goals scored.
But then I want to sort the bars so that Teams with more goals comes first. I tried everything I can find on the web but none of them works in my case. I was very confused...
The code:
var svg = d3.select("svg"),
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
y = d3.scaleLinear().rangeRound([height, 0]);
svg = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("StatsByClub1.csv", function(d) {
d.Goals = +d.Goals;
return d;
}, function(error, data) {
if (error) throw error;
x.domain(data.map(function(d) { return d.Team; }));
y.domain([0, d3.max(data, function(d) { return d.Goals; })]);
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Goals");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
// sorting does not work!
.sort(function(a, b) {d3.descending(a.Goals, b.Goals)})
.attr("class", "bar")
.attr("x", function(d) { return x(d.Team); })
.attr("y", function(d) { return y(d.Goals); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.Goals); });
The data looks like:
Team Goals
Man. United 49
Man. City 71
Arsenal 65
Hotspur 69
Liverpool 63
Chelsea 59
Everton 59
Swansea 48
Stoke City 41
Sunderland 42
Sort the data array using d3.descending, which:
Returns -1 if a is greater than b, or 1 if a is less than b, or 0. This is the comparator function for reverse natural order, and can be used in conjunction with the built-in array sort method to arrange elements in descending order.
So, put this right after you load the data, before setting the domains:
data.sort((a, b) => d3.descending(a.Goals, b.Goals));
Related
Thank you for any help you can offer in advance, I am new to D3 and having a hard time following the multiline chart examples I've seen online. I have data that looks like:
country,year,average
United States,1970,51
United States,1971,50
United States,1972,54
United States,1973,56
United States,1974,53
United States,1975,57
United States,1976,60
Brazil,1970,23
Brazil,1971,25
Brazil,1972,24
Brazil,1973,21
Brazil,1974,25
Brazil,1975,26
Brazil,1976,24
for multiple countries and I would like to make a line for each of them.
var margin = {top: 10, right: 40, bottom: 150, left: 70},
width = 760 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var w = width + margin.left + margin.right;
var h = height + margin.top + margin.bottom;
var svg = d3.select("body").append("svg") // this appends a new SVG element to body
.attr("width", w) // set the width
.attr("height", h) // set the height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x scale will handle time
var xScale = d3.scaleBand().range([0, width]).padding(0.1);
// y scale will handle energy consumption values
var yScale = d3.scaleLinear().range([height,0]);
// Define X and Y AXIS
var yAxis = d3.axisLeft(yScale).ticks(5);
var xAxis = d3.axisBottom(xScale);
function rowConverter(data) {
return {
country : data.country,
year : +data.year,
average : +data.average // the + operator parses strings into numbers
};
}
// line generator function
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return xScale(d.year); })
.y(function(d) { return yScale(d.average); })
d3.csv("EvenMore.csv", rowConverter).then(function(data) {
var countries = d3.nest()
.key(function (d) { return d.country; })
.entries(data);
console.log(countries);
yScale.domain([0,d3.max(data, function(d) {return d.average; } )]);
xScale.domain(d3.extent(data, function(d) { return d.year; } ));
// Draw xAxis
svg.append("g") // add a new svg group element
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
// Draw yAxis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
svg.selectAll("path")
.data(countries)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
});
});
I do not know what these errors mean, Error: attribute d: Expected number, "….33333333333334LNaN,114.27777777…":
The Problem
You're not using the band scale correctly. A band scale is not a quantitative scale and therefore does not have an upper and lower bounds. Instead, every value of the domain needs to be specified:
The first element in domain will be mapped to the first band, the
second domain value to the second band, and so on. Domain values are
stored internally in a map from stringified value to index; the
resulting index is then used to determine the band (docs)
This explains your error, you've specified two values to the domain, the first year and the last year. We can see that the domain is only these two values a few ways, when looking at the scale (a band scale's axis by default includes all ticks, but even here we see the spacing is really odd if 1970 and 1976 are the start and end values):
The error message also helps in finding the error: if the first coordinate's x value was NaN the message would read "Expected Number, "MNan,1234..." when examining the path d attribute (especially without any curve applied), we can see the x value of every coordinate except the first and last are NaN.
The solution
You need to provide all values in the domain to the scale. We can get all values with:
xScale.domain(data.map(function(d) { return d.year; }))
The scale will weed out duplicates when setting the domain.
var margin = {top: 10, right: 40, bottom: 150, left: 70},
width = 760 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var w = width + margin.left + margin.right;
var h = height + margin.top + margin.bottom;
var svg = d3.select("body").append("svg") // this appends a new SVG element to body
.attr("width", w) // set the width
.attr("height", h) // set the height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// x scale will handle time
var xScale = d3.scaleBand().range([0, width]).padding(0.1);
// y scale will handle energy consumption values
var yScale = d3.scaleLinear().range([height,0]);
// Define X and Y AXIS
var yAxis = d3.axisLeft(yScale).ticks(5);
var xAxis = d3.axisBottom(xScale);
function rowConverter(data) {
return {
country : data.country,
year : +data.year,
average : +data.average // the + operator parses strings into numbers
};
}
// line generator function
var line = d3.line()
.curve(d3.curveBasis)
.x(function(d) { return xScale(d.year); })
.y(function(d) { return yScale(d.average); })
var data = d3.csvParse(d3.select("pre").remove().text())
data = data.map(rowConverter);
var countries = d3.nest()
.key(function (d) { return d.country; })
.entries(data);
yScale.domain([0,d3.max(data, function(d) {return d.average; } )]);
xScale.domain(countries[0].values.map(function(d) { return d.year; }));
// Draw xAxis
svg.append("g") // add a new svg group element
.attr("class", "x axis")
.attr("transform", "translate(0, " + height + ")")
.call(xAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
// Draw yAxis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("text-anchor", "end");
svg.selectAll(null)
.data(countries)
.enter()
.append("path")
.attr("class", "line")
.attr("d", function(d) {
return line(d.values);
});
.line {
stroke-width: 2px;
fill: none;
stroke:black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<pre>country,year,average
United States,1970,51
United States,1971,50
United States,1972,54
United States,1973,56
United States,1974,53
United States,1975,57
United States,1976,60
Brazil,1970,23
Brazil,1971,25
Brazil,1972,24
Brazil,1973,21
Brazil,1974,25
Brazil,1975,26
Brazil,1976,24</pre>
I'm having challenges in setting the bar (fixed width) position aligned correctly width the x-axis label.
The bars and the x-ticks are not aligned correctly and also the last bar is rendered after the max xscale range.
Appreciate any help in fixing this issue.
Please check the sample here - https://jsfiddle.net/sjselvan/wsy5frh2/29/ - updated and fixed version
function generateChart(){
const data = [{
label: 100,
value: 10
},
{
label: 200,
value: 20
},
{
label: 300,
value: 30
},
{
label: 400,
value: 40
},
{
label: 500,
value: 50
}];
let margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 600 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
const xScale = d3.scaleLinear().domain([0,500]).range([0,width]);
const yScale = d3.scaleLinear().domain([0,50]).range([height,0]);
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
console.log(d3.select('#chart'));
let svg = d3.select('#chart')
.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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)" );
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value ($)");
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", "steelblue")
.attr("x", function(d) { return xScale(d.label); })
.attr("width", 15)
.attr("y", function(d) { return yScale(d.value); })
.attr("height", function(d) { return height - yScale(d.value); });
}
generateChart();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>
You're setting the x value of the bars, which sets the left-hand edge of the bars, using the same scale as the x axis. So, it makes sense that the left-hand edge of the bar representing 100 is lined up with the 100 tick in the axis.
In order to line up the bars, you need to move them to the left by half of their width. You would need to make the bar width an even number so that the bars fit nicely.
const barWidth = 16;
svg.selectAll("bar")
.data(data)
.enter().append("rect")
.style("fill", "steelblue")
.attr("x", function(d) { return xScale(d.label) - (barWidth / 2); })
.attr("width", barWidth)
.attr("y", function(d) { return yScale(d.value); })
.attr("height", function(d) { return height - yScale(d.value); });
However, I would say that it doesn't really make sense for this chart to be a bar chart. Bars usually represent nominal (Banana, Apple, Pear), or ordinal values. Whereas your chart seems to more suit a line, or a scattergraph.
But if you do mean to use the numbers as labels, you will be better off using a band scale which will line up the bars above the labels nicely.
I am trying to show a barchart using d3.js. Y axis contains the speed and x axis contains time. I am using the following code:
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 400 - margin.left - margin.right,
height = 250 - margin.top;
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1);
var y = d3.scale.linear().range([ height, 0 ]);
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left").ticks(20);
x.domain(data.map(function(d) { return d.datetime; }));
y.domain([0, d3.max(data, function(d) { return d.speed; })]);
var svg=d3.select("#bar").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 transition = svg.transition().duration(750), delay = function(d, i) {
return i * 50;
};
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)" ).text("Time");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Speed");
svg.selectAll("rect")
.data(data)
.enter().append("rect").transition().delay(0)
.style("fill", "red")
.attr("x", function(d,i) { return 30*i+20; })
.attr("width", 25)
.attr("y", function(d) { return y(d.speed); })
.attr("height", function(d) { return height - y(d.speed); }); //function(d){return " "+d.datetime;}
transition.select(".y.axis").call(yAxis);
But unfortunately no ticks are showing in the x axis. Y axis ticks are working fine. Can anyone help me regarding this?
In your x-axis creation, this:
.attr("transform", "rotate(-90)" ).text("Time");
is just going to but the word "Time" on each tick. What you need is:
// define the time format you want
var format = d3.time.format("%Y-%m-%d");
// create x axis
...
.text(function(d){
return format(d);
});
Also in your rect placement:
svg.selectAll("rect")
...
.attr("x", function(d,i) { return 30*i+20; })
You are spacing them based on index. You need to match this to the axis position:
svg.selectAll("rect")
...
.attr("x", function(d, i) {
return x(d.datetime);
})
Here's an example.
Your problem may be where you set the domain for x:
x.domain(data.map(function(d) { return d.datetime; }));
You probably want something like:
x.domain([d3.min(data, function(d) { return d.datetime; }), d3.max(data, function(d) { return d.datetime; })]);
I try to make a bar chart on the basis of 2D data array (I din`t want to use 2D array initially, so there is a function "mergingAr", which merges them) using d3.js. Here is the code:
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
var arr1 = [399200,100000, 352108, 600150, 39000, 17005, 4278];
var arr2 = [839, 149, 146, 200, 200, 121, 63];
function mergingAr (array1, array2)
{
var i, out = [];//literal new array
for(i=0;i<array1.length;i++)
{
out.push([array1[i],array2[i]]);
}
return out;
}
var data = mergingAr(arr1, arr2);
margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) {
return d[0]; })])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d[1]; })])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
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("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d[0]); })
//.attr("width", x.rangeBand())
.attr("width", width/a1.length)
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return height - y(d[1]); });
Te problem is - the bars cover each other, there are no distance between them, even if I used rangeRoundBands.
There are 2 issues in your code.
The first one is that the data array is not sorted. In order to sort it you can do:
out = out.sort(function(a,b) { return d3.ascending(a[0],b[0]) })
before returning out in your mergeAt function. Sorting the array makes sure that you process bars in the right order.
The second issue is that your intervals are not equal. To remediate to this, I made the width of a block equal to the distance to the next one (but you might want to do something different):
.attr("width", function(d,i){
if(i!=(data.length-1)) {
return x(data[i+1][0])-x(data[i][0])
} else {
return 10; // the last block is of width 10. a cleaner way is to add a
// marker at the end of the array to know where to finish
// the axis
}
})
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/
Edit
In order to have the same interval between each bar and the same width, you have to change the scale to an ordinal one:
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(data.map(function(d){return d[0]}))
Then, you need to change the way you compute the width to:
.attr("width", x.rangeBand())
JsFiddle: http://jsfiddle.net/chrisJamesC/6WJPA/2/
I'm trying to do a scatter plot in D3.js with date on the x-axis. The code below is based on the scatter plot example on the d3 site. I must be doing something wrong in the attr('cx'
area...
var data =[
{
"title":"SUNY GENESEO COLLEGE STADIUM PHASE 2",
"stage":"Biddate Set",
"naples_update_date":"2/9/2014",
"value":7500000,
"value_type":"Confirmed",
"ownership":"State",
"work_type":"Alteration",
"record_date":"1/21/2014",
"floors":null,
"floor_area":null,
"floor_units":"",
"land_area":null,
"land_units":"",
"structures":null,
"units":0,
"contract_type":"Open Bidding",
"address":"1 College Cir",
"city":"Geneseo",
"state":"NY",
"county":"Livingston",
"date":1390911781
},
{
"title":"KENTUCKY FAIR & EXPOSITION CENTER FREEDOM HALL-ROOFING",
"stage":"Post Bid Results Pending",
"naples_update_date":"2/10/2014",
"value":2662903,
"value_type":"Confirmed",
"ownership":"State",
"work_type":"Alteration",
"record_date":"10/29/2013",
"floors":2,
"floor_area":null,
"floor_units":"",
"land_area":null,
"land_units":"",
"structures":1,
"units":0,
"contract_type":"Open Bidding",
"address":"937 Phillips Ln",
"city":"Louisville",
"state":"KY",
"county":"Jefferson",
"date":1383132359
}
];
var format = d3.time.format("%d/%m/%Y");
var dateMin = format.parse("20/03/2001");
var dateMax = format.parse("7/02/2001");
var margin = {top: 20, right: 20, bottom: 30, left: 120},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xValue = function(d) {
return format.parse(d.record_date);
}, // data -> value
xScale = d3.time.scale().domain([dateMin,dateMax]).range([0, width]), // value -> display
xMap = function(d) { return xScale(xValue(d));}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yValue = function(d) { return d.value;}, // data -> value
yScale = d3.scale.linear().range([height, 0]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return d.ownership;},
color = d3.scale.category10();
// add the graph canvas to the body of the webpage
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 + ")");
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// don't want dots overlapping axis, so add in buffer to data domain
xScale.domain([d3.min(data, xValue)-1, d3.max(data, xValue)+1]);
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]);
//x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Date");
// y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value");
// draw dots
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(cValue(d));})
.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", 0.9);
tooltip.html(d.title + "<br/> (" + xValue(d) + ", " + yValue(d) + ")")
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
})
.attr('data-title',function(e){
return e.title;
})
.attr('data-value',function(e){
return e.value;
})
.attr('data-date',function(e){
return e.record_date;
})
.attr('data-sqft',function(e){
return e.floor_area;
});
I've searched around and tried to follow the tips out there, making sure the dates for the .range() are objects of the same format at the dates inside attr(cx).
Demo: http://jsfiddle.net/EC6TL/
The problem was in line:
xScale.domain([d3.min(data, xValue) - 1, d3.max(data, xValue) + 1]);
You cannot add and subtract 1 from dates. :-)
Fix:
xScale.domain([d3.min(data, xValue), d3.max(data, xValue)]);