D3 Categorical Area Chart - Scale Issue - javascript

I'm trying to create an area chart for statistics on each US state. I have a single number statistic for each state; an element of my data list looks like the following:
{'state':'CA','count':4000}
Currently, my area chart looks like this. The task is mainly complete, but you may notice how the very last category (in this case, UTAH) isn't filled. I'm not quite sure how to get around this. close_up
I am using a scaleBand axis; this felt appropriate. Perhaps it is not the correct approach. Here is the JS behind the chart:
var svg_area = d3.select("#area")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom),
g_area = svg_area.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scaleBand().range([0, width]),
y = d3.scaleLinear().range([height, 0]);
var area = d3.area()
.x(function(d) { return x(d.state); })
.y0(height)
.y1(function(d) { return y(d.count); });
d3.csv('data/states.csv', function(data) {
data.forEach(function(d) {
d.count = +d.count;
});
data.sort(function(a, b){
return b.count-a.count;
});
data = data.slice(0,30);
x.domain(data.map(function(d) { return d.state; }));
y.domain([0, d3.max(data, function(d) { return d.count; })]);
g_area.append('path')
.datum(data)
.attr('fill', solar[1])
.attr("class", "area")
.attr('d', area);
g_area.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
g_area.append("g")
.attr("class", "y-axis")
.attr("transform", "translate(0," + 0 + ")")
.call(d3.axisLeft(y));
});
Any suggestions on how I can fix this? Thanks for any feedback!

Contrary to your question's title (now edited), the area chart is not "leaving out the last data point".
What you're seeing is the expected result, since you are using a band scale. Actually, that value just above the horizontal axis (just in the "edge" of the area chart) is Utah value! Try to understanding it with this explanation: Imagine a bar chart with your data. Each bar has, of course, a given width. Now, draw a path going from the top left corner of one bar to the top left corner of the next bar, starting at the first bar and, when reaching the last bar, going down from the top left corner to the axis. That's the area you have right now.
There are two solutions here. The first one is using a point scale instead:
var x = d3.scalePoint().range([0, width])
However, this will trim the "margins" of the area path, before the first state and after the last state (Utah). That means, the area chart will start right over California tick and end right over Utah tick.
If you don't want that there is a second solution, which is hacky, but will keep those "margins": add the bandwidth() to the last state in the area generator:
var area = d3.area()
.x(function(d, i) {
return i === data.length - 1 ?
x(d.state) + x.bandwidth() : x(d.state)
})
It may be worth noting that, using a band scale, your chart is technically incorrect: the values in the area for each state are not over the tick for that state.

Related

D3.js v3 Dot Plot Histogram Duplicating Values

I am building a dot plot histogram with d3.js v3 and I have pretty much finished everything up - except for whatever reason some of my data points are duplicating (certain circles repeating themselves - not all of them, just some). I tried tweaking the axis parameters, as well as the data itself [deleted rows with null values, etc]- however sadly to no avail.
Any help would be immensely appreciated.
Here's my relevant code:
<div id="dotHappy"></div>
var data = d3.csv('happy_dot_modified.csv', function(data) {
data.forEach(function(d) {
d["city"] = d["city"];
d["Happy"] = +d["Happy"];
d["thc"] = +d["thc"];
});
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 1560 - margin.left - margin.right,
height = 1260 - margin.top - margin.bottom;
I tried this coder block but it wasn't working. (Not sure if this is even what's giving me the issue anyways - perhaps not).
// var x = d3.scale.linear()
// .range([0, width]);
So I went with this:
var x = d3.scale.ordinal()
.rangePoints([0, width])
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");
var svg = d3.select("#dotHappy")
.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 chart = svg.append("g")
.attr("id", "chart");
Also tried tweaking this, which may or may not even be part of the problem.
x.domain(data.map(d => d.Happy));
y.domain([5, 33]);
// y.domain(data.map(d => d.city));
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("Happy");
svg.append("g")
.attr("class", "y axis")
// .attr("transform", "translate(0," + width + ")")
.call(yAxis)
// .append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("THC");
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
var dots = groups.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
.transition().duration(1000)
.attr("class", "dot")
.attr("r", 10)
.attr("cy", function(d) {
return y(d)
})
.style("fill", "blue")
.style("opacity", 1);
})
Here is a snapshot of my csv file:
city. |. Happy. | thc
Boston. 37. 23
NYC. 22. 30
Chicago. 88. 5
Following is a screenshot of what it currently looks like. So in this case, the tooltip displaying the text box 'The Sister' should be only for one circle (because it should only be one data point), however if you hover over the other 10 orange circles below it, it's all the same - indicating it has been repeated 11 times total:
Actually, all of the circles are repeating vertically. You may not see them all because the repeated circles are being overlapped by other colored circles as these other circles get drawn. For example, the yellow data point "The Sister" is repeating all the way down to the bottom, but the data points below the yellow ones, in blue, pink, green, blue, etc., drew themselves on top of the yellow repeats.
The culprit is this code:
.selectAll("circle")
.data(function(d) {
return d3.range(1, +d.thc + 1)
// return d3.range(d.thc)
})
.enter().append("circle")
which, if you don't want it to repeat, should have been just one line:
.append("circle")
To explain what happened, this code:
var groups = svg.selectAll(".groups")
.data(data)
.enter()
.append("g")
.attr("class", "groups") //NOTE: you should add this line since you have 'selectAll(".groups")'
.attr("transform", function(d) {
return "translate(" + x(d.Happy) + ".0)";
});
already creates a g element for every row in the csv file. And for every g, you created an array using d3.range(1, +d.thc + 1), and appended a circle for each item in that array.
As an example, let's take the row representing "The Sister" data point that has a THC of 33. For that one data point, the code creates one <g>, inside of which it binds the array [1, 2, 3, ..., 33], and therefore appends 33 circles to the <g> element, with the cy attribute between y(1) and y(33).
Now, the question that follows is that, you specified a domain with a minimum of 5 with y.domain([5, 33]). Yet the data-bounded array, generated with d3.range, always begins with 1 and increments up to the value of THC. So some of the values in the array (1,2,3, and 4) always fall outside the y-axis, but d3 was able to translate it to a proper y-position. Is that possible? By default, yes, d3.scale extrapolates when the data is outside of the domain.
By default, clamping is disabled, such that if a value outside the input domain is passed to the scale, the scale may return a value outside the output range through linear extrapolation. For example, with the default domain and range of [0,1], an input value of 2 will return an output value of 2.

line d3.js graph shifting wrongly (to right)

Because I am having an issue with my own code, I am studying below link:
https://bost.ocks.org/mike/path/
I think I get below
// push a new data point onto the back
data.push(random());
// redraw the line, and then slide it to the left
path
.attr("d", line)
.attr("transform", null)
.transition()
.attr("transform", "translate(" + x(-1) + ")");
// pop the old data point off the front
data.shift();
But the part that doesn't work for me and I don't understand is
.attr("transform", "translate(" + x(-1) + ")");
My code's x is like below:
var x = d3.time.scale().range([-5, width])
domain is not known at this time and later domain is defined as
x.domain(d3.extent(callbackData, function(d){return d.reg_date;}));
I try to use x(-1) but entire graph(except axis) disappears when I call for update so I use something like below and initially, it seems to work.(graph shifts to the left). But as more data comes in(btw, my data comes in and updates the data properly(shift off the front data and push latest into the back) graph starts to shifting towards the right. I can see graph start taking points from the front(which is correct) but problem is, graph is starting to shift to right(instead of left).
Really beating my head w/ this issue so hopefully someone can kindly advise.
path
.attr("d",line)
.attr('transform', null)
.transition()
.duration(300)
.ease('linear')
.attr("transform", "translate(" + -2 + ")");
This is a quick implementation : https://jsfiddle.net/thatOneGuy/j2eovk9k/8/
I have added this to work out distance between the x ticks :
var testtickArr = y.ticks(10);
var testtickDistance = y(testtickArr[testtickArr.length - 2]) - y(testtickArr[testtickArr.length - 1]);
Then used this as the distance to move the path :
path
.attr("d", line)
.transition()
.duration(1000)
.ease('linear')
.attr("transform", function(d) {
console.log(d)
return "translate(" + (pathTrans) + ")";
})
Also, update the translation value :
pathTrans -= testtickDistance;
The way you're updating your data can be improved. But this should help you with the transition. This is still not working ok at all as the distances between your points are not equal. For yours to work like the example you need equal distances between each points so it transitions to the left smoothly.

Smoothing arcs/plot points in D3.js/GeoJSON/TopoJSON/Shapefile (somewhere along the way)

I've been looking around a while for an answer to this, and I haven't been able to figure it out.
I'm ultimately creating a TopoJSON file from grid based data (GRIB files).
I can pretty easily interpolate the data down to a finer resolution grid so the plot points appear smoother when zoomed out, but when zoomed in, it's inevitable to see the blocky grid points.
I've also looked into simplification, which does help a bit but its not quite smoothing.
I'm using D3 to render the data.
Is this something that can be done on the front end or should/can it be done in the raw TopoJSON data?
I essentially don't want you to be able to tell that it's a grid, even if you zoom in 10,000%.
Here's an example of what I'm after:
Is this something that can be done on the front end or should/can it be done in the raw TopoJSON data?
This is something that should be done on the front end. If you were to smooth the data before you wrote it to the JSON file, the file would be needlessly big.
If you're using D3.js, and you're working with lines, the built-in interpolate() function is the way to go.
Here is a working example of D3's line.interpolate() using "cardinal" smoothing:
http://codepen.io/gracefulcode/pen/doPmOK
Here's the code:
var margin = {
top: 30,
right: 20,
bottom: 30,
left: 50
},
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.time.format("%d-%b-%y").parse;
// Set the ranges
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5);
var yAxis = d3.svg.axis().scale(y).orient("left").ticks(5);
// Define the line
var valueline = d3.svg.line()
.interpolate("cardinal")
.x(function(d) {
return x(d.date);
})
.y(function(d) {
return y(d.close);
});
// Adds the svg canvas
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 + ")");
// Get the data
d3.json('https://api.myjson.com/bins/175jl', function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
// Starting with a basic graph 14
x.domain(d3.extent(data, function(d) {
return d.date;
}));
y.domain([0, d3.max(data, function(d) {
return d.close;
})]);
// Add the valueline path.
svg.append("path")
.attr("class", "line")
.attr("d", valueline(data));
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Maybe d3 line.interpolate() is what you're looking for?
More info:
http://www.d3noob.org/2013/01/smoothing-out-lines-in-d3js.html

d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median

I've built a d3.js scatter plot with zoom/pan functionality. You can see the full thing here (click 'Open in a new window' to see the whole thing):
http://bl.ocks.org/129f64bfa2b0d48d27c9
There are a couple of features that I've been unable to figure out, that I'd love a hand with it if someone can point me in the right direction:
I want to apply X/Y zoom/pan boundaries to the area, so that you can't drag it below a certain point (e.g. zero).
I've also made a stab at creating Google Maps style +/- zoom buttons, without any success. Any ideas?
Much less importantly, there are also a couple of areas where I've figured out a solution but it's very rough, so if you have a better solution then please do let me know:
I've added a 'reset zoom' button but it merely deletes the graph and generates a new one in its place, rather than actually zooming the objects. Ideally it should actually reset the zoom.
I've written my own function to calculate the median of the X and Y data. However I'm sure that there must be a better way to do this with d3.median but I can't figure out how to make it work.
var xMed = median(_.map(data,function(d){ return d.TotalEmployed2011;}));
var yMed = median(_.map(data,function(d){ return d.MedianSalary2011;}));
function median(values) {
values.sort( function(a,b) {return a - b;} );
var half = Math.floor(values.length/2);
if(values.length % 2)
return values[half];
else
return (parseFloat(values[half-1]) + parseFloat(values[half])) / 2.0;
};
A very simplified (i.e. old) version of the JS is below. You can find the full script at https://gist.github.com/richardwestenra/129f64bfa2b0d48d27c9#file-main-js
d3.csv("js/AllOccupations.csv", function(data) {
var margin = {top: 30, right: 10, bottom: 50, left: 60},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var xMax = d3.max(data, function(d) { return +d.TotalEmployed2011; }),
xMin = 0,
yMax = d3.max(data, function(d) { return +d.MedianSalary2011; }),
yMin = 0;
//Define scales
var x = d3.scale.linear()
.domain([xMin, xMax])
.range([0, width]);
var y = d3.scale.linear()
.domain([yMin, yMax])
.range([height, 0]);
var colourScale = function(val){
var colours = ['#9d3d38','#c5653a','#f9b743','#9bd6d7'];
if (val > 30) {
return colours[0];
} else if (val > 10) {
return colours[1];
} else if (val > 0) {
return colours[2];
} else {
return colours[3];
}
};
//Define X axis
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height)
.tickFormat(d3.format("s"));
//Define Y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(5)
.tickSize(-width)
.tickFormat(d3.format("s"));
var 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 + ")")
.call(d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom));
svg.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Create points
svg.selectAll("polygon")
.data(data)
.enter()
.append("polygon")
.attr("transform", function(d, i) {
return "translate("+x(d.TotalEmployed2011)+","+y(d.MedianSalary2011)+")";
})
.attr('points','4.569,2.637 0,5.276 -4.569,2.637 -4.569,-2.637 0,-5.276 4.569,-2.637')
.attr("opacity","0.8")
.attr("fill",function(d) {
return colourScale(d.ProjectedGrowth2020);
});
// Create X Axis label
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width)
.attr("y", height + margin.bottom - 10)
.text("Total Employment in 2011");
// Create Y Axis label
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("y", -margin.left)
.attr("x", 0)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("Median Annual Salary in 2011 ($)");
function zoom() {
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
svg.selectAll("polygon")
.attr("transform", function(d) {
return "translate("+x(d.TotalEmployed2011)+","+y(d.MedianSalary2011)+")";
});
};
}
});
Any help would be massively appreciated. Thanks!
Edit: Here is a summary of the fixes I used, based on Superboggly's suggestions below:
// Zoom in/out buttons:
d3.select('#zoomIn').on('click',function(){
d3.event.preventDefault();
if (zm.scale()< maxScale) {
zm.translate([trans(0,-10),trans(1,-350)]);
zm.scale(zm.scale()*2);
zoom();
}
});
d3.select('#zoomOut').on('click',function(){
d3.event.preventDefault();
if (zm.scale()> minScale) {
zm.scale(zm.scale()*0.5);
zm.translate([trans(0,10),trans(1,350)]);
zoom();
}
});
// Reset zoom button:
d3.select('#zoomReset').on('click',function(){
d3.event.preventDefault();
zm.scale(1);
zm.translate([0,0]);
zoom();
});
function zoom() {
// To restrict translation to 0 value
if(y.domain()[0] < 0 && x.domain()[0] < 0) {
zm.translate([0, height * (1 - zm.scale())]);
} else if(y.domain()[0] < 0) {
zm.translate([d3.event.translate[0], height * (1 - zm.scale())]);
} else if(x.domain()[0] < 0) {
zm.translate([0, d3.event.translate[1]]);
}
...
};
The zoom translation that I used is very ad hoc and basically uses abitrary constants to keep the positioning more or less in the right place. It's not ideal, and I'd be willing to entertain suggestions for a more universally sound technique. However, it works well enough in this case.
To start with the median function just takes an array and an optional accessor. So you can use it the same way you use max:
var med = d3.median(data, function(d) { return +d.TotalEmployed2011; });
As for the others if you pull out your zoom behaviour you can control it a bit better. So for example instead of
var svg = d3.select()...call(d3.behavior.zoom()...)
try:
var zm = d3.behavior.zoom().x(x).y(y).scaleExtent([1, 8]).on("zoom", zoom);
var svg = d3.select()...call(zm);
Then you can set the zoom level and translation directly:
function zoomIn() {
zm.scale(zm.scale()*2);
// probably need to compute a new translation also
}
function reset() {
zm.scale(1);
zm.translate([0,0]);
}
Restricting the panning range is a bit trickier. You can simply not update when the translate or scale is not to your liking inside you zoom function (or set the zoom's "translate" to what you need it to be). Something like (I think in your case):
function zoom() {
if(y.domain()[0] < 0) {
// To restrict translation to 0 value
zm.translate([d3.event.translate[0], height * (1 - zm.scale())]);
}
....
}
Keep in mind that if you want zooming in to allow a negative on the axis, but panning not to you will find you get into some tricky scenarios.
This might be dated, but check out Limiting domain when zooming or panning in D3.js
Note also that the zoom behaviour did have functionality for limiting panning and zooming at one point. But the code was taken out in a later update.
I don't like to reinvent the wheel. I was searching for scatter plots which allow zooming. Highcharts is one of them, but there's plotly, which is based on D3 and not only allows zooming, but you can also have line datasets too on the scatter plot, which I desire with some of my datasets, and that's hard to find with other plot libraries. I'd give it a try:
https://plot.ly/javascript/line-and-scatter/
https://github.com/plotly/plotly.js
Using such nice library can save you a lot of time and pain.

correctly sizing a D3 bar graph

I am attempting to add labels/axis/titles/etc to a D3 bar graph. I can get something close to the right size, however I end up clipping off part of the last bar (so the last bar is skinnier than the others).
Here is the pertinent code:
var x = d3.scale.linear()
.domain([0, object.data.length])
.range([0, object.width]);
var y = d3.scale.linear()
.domain([object.min-(object.max-object.min)*.15, object.max (object.max-object.min)*.15])
.rangeRound([ object.height - 30, 0]);
var vis = d3.select(object.ele)
.append("svg:svg")
.attr("width", object.width)
.attr("height", object.height)
.append("svg:g");
vis.selectAll("g")
.data(object.data)
.enter().append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("y", function(d) { return object.height - y(d.value); })
.attr("width", object.width/object.data.length - 1)
.attr("height", function(d) { return y(d.value); })
.attr("transform", "translate(30,-30)");
At the moment everything (labels, axis, and so on) is 30px. How do I correctly alter the graph to make room for whatever else I need?
You are cutting off your last bar because you use translate the x coordinate but your the range of your x is only to the width without the extra 30 pixels.
Also it may be easier to simplify your y domain to use .domain([object.min, object.max]) then have the "y" and "height" functions reversed. This way you start the rect at the y(d.value) and make it's height object.height - y(d.value).
I would create three groups initially, one for your y axis, one for x axis, and then another for the bars. Draw your bars inside the last group and then translate the whole group itself instead of each individual bar. Increase the size of your object.width and object.height to match the total space you want.

Categories