I'm trying to build a basic, responsive bar chart with data loaded from a CSV. My chart isn't reading the CSV and gives me NaNs. Here's a screenshot of what my chart looks like. Here is my current code:
<script type="text/javascript">
var url = "data/data.csv",
margin = {top: 30, right: 10, bottom: 30, left: 10},
width = parseInt(d3.select('#chart').style('width'), 10),
width = width - margin.left - margin.right,
height = 200, // placeholder
barHeight = 20,
spacing = 5,
total = d3.format('n');
// scales and axes
var x = d3.scale.linear()
.range([0, width])
.domain([0, 4000]); // hard-coding this because I know the data
var y = d3.scale.ordinal();
var xAxis = d3.svg.axis()
.scale(x)
.ticks(3);
// create the chart
var chart = d3.select('#chart').append('svg')
.style('width', (width + margin.left + margin.right) + 'px')
.append('g')
.attr('transform', 'translate(' + [margin.left, margin.top] + ')');
d3.csv(url).row(function(d) {
d.total = +d.total;
d.name = +d.name;
return d;
}).get(function(err, data) {
// sort
data = _.sortBy(data, 'total').reverse();
// set y domain
y.domain(d3.range(data.length))
.rangeBands([0, data.length * barHeight]);
// set height based on data
height = y.rangeExtent()[1];
d3.select(chart.node().parentNode)
.style('height', (height + margin.top + margin.bottom) + 'px')
// render the chart
// add top and bottom axes
chart.append('g')
.attr('class', 'x axis top')
.call(xAxis.orient('top'));
chart.append('g')
.attr('class', 'x axis bottom')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis.orient('bottom'));
var bars = chart.selectAll('.bar')
.data(data)
.enter().append('g')
.attr('class', 'bar')
.attr('transform', function(d, i) { return 'translate(0,' + y(i) + ')'; });
bars.append('rect')
.attr('class', 'background')
.attr('height', y.rangeBand())
.attr('width', width);
bars.append('rect')
.attr('class', 'total')
.attr('height', y.rangeBand())
.attr('width', function(d) { return x(d.total); })
bars.append('text')
.text(function(d) { return d.name; })
.attr('class', 'name')
.attr('y', y.rangeBand() - 5)
.attr('x', spacing);
bars.append('line')
.attr('class', 'median')
.attr('x1', x(median))
.attr('x2', x(median))
.attr('y1', 1)
.attr('y2', y.rangeBand() - 1);
});
// resize
d3.select(window).on('resize', resize);
function resize() {
// update width
width = parseInt(d3.select('#chart').style('width'), 10);
width = width - margin.left - margin.right;
// resize the chart
x.range([0, width]);
d3.select(chart.node().parentNode)
.style('height', (y.rangeExtent()[1] + margin.top + margin.bottom) + 'px')
.style('width', (width + margin.left + margin.right) + 'px');
chart.selectAll('rect.background')
.attr('width', width);
chart.selectAll('rect.total')
.attr('width', function(d) { return x(d.total); });
// update axes
chart.select('.x.axis.top').call(xAxis.orient('top'));
chart.select('.x.axis.bottom').call(xAxis.orient('bottom'));
}
// highlight code blocks
hljs.initHighlighting();
</script>
Here's the data:
name, total
Brian, 1514
Frankie, 1439
Jeffery, 1615
Jerry, 685
Kenneth, 3233
Michael, 116
Roy, 817
Timothy, 2184
I can see a couple of issues with your code and data:
For the issues with the data, you can either clean up the rows by removing the spaces, eg:
name,total
Brian,1514
Frankie,1439
Jeffery,1615
Jerry,685
Kenneth,3233
Michael,116
Roy,817
Timothy,2184
Or you can reference them in your code using the [] notation, eg:
d[' total'] = +d[' total'];
d3 is adding the leading space to the attribute name for each record. When you reference the attributes as d.total, it's returning undefined and undefined coerced to a number is NaN.
Eg. data[0]['total'] is undefined but data[0][' total'] has "1514" as you'd expect. Removing the spaces from the data is the easiest way of dealing with this.
In your code, you're doing this:
d.name = +d.name;
This will translate the name value into NaN, as it's a text string beginning with an alpha character, so javascript doesn't know how to coerce it to a number.
You're also referencing a variable named median which isn't declared in your code.
I've created a jsfiddle to help: http://jsfiddle.net/BenLyall/eh1r6j2e/12/
Note: I've changed the d3.csv... line to work with an element in the DOM to store the data.
Why are you making name into Number
below code
d3.csv(url).row(function(d) {
d.total = +d.total;
d.name = +d.name;
return d;
})
remove conversion of name.Okay
Related
When building an area chart in D3.js, when you have only a single value the chart does not render.
For demonstration purposes, I modified the following example: https://d3-graph-gallery.com/graph/area_basic.html to illustrate the problem.
<script>
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 50},
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.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 + ")");
//Read the data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered_comma.csv",
// When reading the csv, I must format variables:
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
},
// Now I can use this dataset:
function(data) {
data = [data[0]]
// Add X axis --> it is a date format
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.range([ 0, width ]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })])
.range([ height, 0 ]);
svg.append("g")
.call(d3.axisLeft(y));
// Add the area
svg.append("path")
.datum(data)
.attr("fill", "#cce5df")
.attr("stroke", "#69b3a2")
.attr("stroke-width", 1.5)
.attr("d", d3.area()
.x(function(d) { return x(d.date) })
.y0(y(0))
.y1(function(d) { return y(d.value) })
)
})
</script>
I would expect that chart to look something like:
If you inspect the element the path element you can see it is rendering, just 0 width/height:
I am trying to make simple chart right now importing data from a CSV. Everything on the chart is working great except for the labels. In element inspect I can see that they are being appended and that their x and y coordinates are even correct, but for some reason they are all trapped in the top left corner in the SVG itself.
I have tried changing the x placement function at first because I thought it just wasn't giving the labels a x position, but upon further inspection the labels have the correct metadata.
//Graph Dimensions
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 1000 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//Set Ranges
var x_scale = d3.scaleBand()
.range([0, width])
.padding(0.1);
var y_scale = d3.scaleLinear()
.range([height, 0]);
//Create SVG object
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 + ")");
//Retrieve data
d3.csv('sales.csv').then(function(data){
//Set domains based on data
x_scale.domain(data.map(function(d) { return d.month; }));
y_scale.domain([0, d3.max(data, function(d) { return d.sales; })]);
//Create bars
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x_scale(d.month); })
.attr("width", x_scale.bandwidth())
.attr("y", function(d) { return y_scale(d.sales); })
.attr("height", function(d) { return height - y_scale(d.sales); });
//Create labels
svg.selectAll('text')
.data(data)
.enter().append('text')
.attr('class', 'label')
.attr("x", function(d) { return x_scale(d.month); })
.attr("y", function(d) { return y_scale(d.sales); })
.attr( 'font-size', 14 )
.attr( 'fill', '#555555' )
.attr( 'text-anchor', 'middle' );
//Add Axes
svg.append("g") //X Axis
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x_scale));
svg.append("g") //Y Axis
.call(d3.axisLeft(y_scale));
})
The only thing im looking for is the labels actually appearing. I can change their location later if needed.
I'm using Polymer to render some d3 charts. When the Polymer is initially rendered I only draw a graph with axes and no data, since the data comes later once the API calls succeed. However, when I get around to adding the 'rect' elements in the svg, despite them showing up in the Chrome devtools element inspector, they don't show up in the chart itself.
dataChanged: function() {
var data = this.data;
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = this.width - margin.left - margin.right,
height = this.height - margin.top - margin.bottom;
// format the data
data.forEach(function(d) {
d.date = d3.isoParse(d.date);
});
// set the ranges
var x = d3.scaleTime()
.domain(d3.extent(data, function(d) { return d.date; }))
.rangeRound([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
var svg = d3.select(this.$.chart);
var histogram = d3.histogram()
.value(function(d) { return d.date; })
.domain(x.domain())
.thresholds(x.ticks(d3.timeMonth));
var bins = histogram(data);
y.domain([0, d3.max(bins, function(d) { return d.length; })]);
var t = d3.transition()
.duration(750)
.ease(d3.easeLinear);
svg.selectAll("rect")
.data(bins)
.enter().append("rect")
.attr("class", "bar")
.attr("x", 1)
.attr("transform", function(d) {
return "translate(" + x(d.x0) + "," + y(d.length) + ")";
})
.attr("width", function(d) { return x(d.x1) - x(d.x0) -1 ; })
.attr("height", function(d) { return height - y(d.length); });
svg.select(".xAxis")
.transition(t)
.call(d3.axisBottom(x));
svg.select(".yAxis")
.transition(t)
.call(d3.axisLeft(y));
},
ready: function() {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = this.width - margin.left - margin.right,
height = this.height - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleTime()
.domain([new Date(2010, 6, 3), new Date(2012, 0, 1)])
.rangeRound([0, width]);
var y = d3.scaleLinear()
.range([height, 0]);
// Add the SVG to my 'chart' div.
var svg = d3.select(this.$.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 + ")");
// Add the X Axis
svg.append("g")
.attr("class","xAxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.attr("class","yAxis")
.call(d3.axisLeft(y));
}
ready() gets called upon rendering, dataChanged() when the parent component passes a chunk of data down.
The axes get rendered correctly, with the right transitions and the right dimensions, but the rects don't. They show up in the chrome element inspector with a 0x17 size, even though this is what they look like: <rect class="bar" x="1" transform="translate(0,24.06417112299465)" width="101" height="275.93582887700535"></rect>
In your ready function, you are grabbing your div creating an svg element adding a g element and then appending your axis to that g.
In your dataChanged function, you are grabbing your div and appending rects to it.
See the disconnect? You can't parent svg to HTML.
In ready do this:
var svg = d3.select(this.$.chart).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("id", "canvas")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
In dataChanged:
var svg = d3.select("#canvas");
This will allow you to "find" the appropriate g to append your rects to.
I'm probably doing something wrong but the following fiddle is displaying some really strange behavior:
https://jsfiddle.net/pkerpedjiev/42w01t3e/8/
Before I explain it, here's the code:
function skiAreaElevationsPlot() {
var width = 550;
var height = 400;
var margin = {
'top': 30,
'left': 30,
'bottom': 30,
'right': 40
};
function chart(selection) {
selection.each(function(data) {
// Select the svg element, if it exists.
var svg = d3.select(this).selectAll("svg").data([data]);
// Otherwise, create the skeletal chart.
var gEnter = svg.enter().append("svg").append("g");
svg.attr('width', width)
.attr('height', height);
var zoom = d3.behavior.zoom()
.on("zoom", draw);
data = Object.keys(data).map(function(key) {
return data[key];
}).sort(function(a, b) {
return b.max_elev - a.max_elev;
});
svg.insert("rect", "g")
.attr("class", "pane")
.attr("width", width)
.attr("height", height)
.attr('pointer-events', 'all')
.call(zoom);
var yScale = d3.scale.linear()
.domain([0, d3.max(data.map(function(d) {
return d.max_elev;
}))])
.range([height - margin.top - margin.bottom, 0]);
var xScale = d3.scale.linear()
.domain([0, data.length])
.range([0, width - margin.left - margin.right]);
var widthScale = d3.scale.linear()
.domain(d3.extent(data.map(function(d) {
return d.area;
})))
.range([10, 30]);
zoom.x(xScale).scaleExtent([1, data.length / 30]);
var gMain = gEnter.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
gMain.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width - margin.left - margin.right)
.attr("height", height - margin.top - margin.bottom);
function skiAreaMouseover(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
function skiAreaMouseout(d) {
gMain.select('#n-' + d.uid)
.attr('visibility', 'visible');
}
// the rectangle showing each rect
gMain.selectAll('.resort-rect')
.data(data)
.enter()
.append('rect')
.classed('resort-rect', true)
.attr("clip-path", "url(#clip)")
.attr('id', function(d) {
return 'n-' + d.uid;
})
.on('mouseover', skiAreaMouseover)
.on('mouseout', skiAreaMouseout);
var gYAxis = svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (width - margin.right) + "," + margin.top + ")");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("right")
.tickSize(-(width - margin.left - margin.right))
.tickPadding(6);
gYAxis.call(yAxis);
draw();
function draw() {
function scaledX(d, i) {
console.log('xd', d);
return xScale(i);
}
function rectWidth(d, i) {
return widthScale(d.area);
}
gMain.selectAll('.resort-rect')
.attr('x', scaledX)
.attr('y', function(d) {
console.log('d', d);
return yScale(d.max_elev);
})
.attr('width', 20)
.attr('height', function(d) {
console.log('d:', d)
return yScale(d.min_elev) - yScale(d.max_elev);
})
.classed('resort-rect', true);
}
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
return chart;
}
var elevationsPlot = skiAreaElevationsPlot()
.width(550)
.height(300);
data = [{
"min_elev": 46,
"max_elev": 54,
"uid": "9809641c-ab03-4dec-8d51-d387c7e4f114",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 1354,
"max_elev": 1475,
"uid": "93eb6ade-8d78-4923-9806-c8522578843f",
"num_lifts": 1,
"area": "0.00"
}, {
"min_elev": 2067,
"max_elev": 2067,
"uid": "214fdca9-ae62-473b-b463-0ba3c5755476",
"num_lifts": 1,
"area": "0.00"
}];
d3.select('#ski-area-elevations')
.datum(data)
.call(elevationsPlot)
So, when the page is first loaded, a rectangle will be visible in the middle. If you try scrolling on the graph, the console.log statements in the draw function will produce output. Notice that the xd: and d: statements all consist of just one object from the data set.
Now, if you mouseover the rectangle and try zooming again (using the scroll wheel). A bunch of NaN errors will be displayed. Now some of the d: and xd: statements will now print lists of objects.
Why is this happening? The underlying bound data never changed.
What puzzles me is that if these statements:
gMain.select('#n-' + d.uid)
Are changed to:
gMain.selectAll('#n-' + d.uid)
The fiddle behaves properly. Why does this make a difference? Is this a bug, or am I missing something?
For googleability, here's the error I get:
Error: Invalid value for <rect> attribute y="NaN"
The simple solution is to replace gMain.select/gMain.selectAll in the mouse event routines with d3.select(this)
The complicated solution seems to be that a single select binds a parents data to whatever is selected if you're acting on an existing selection. gMain is an existing selection and has the 3 data values as an array bound to it - console.log (gMain.datum()) to see - so when you do a gMain.select("#oneoftherects") you replace the single object in #oneoftherects with that array, thus knackering the x,y,width,height etc routines that expect one object. (Using d3.select doesn't do the same as d3 isn't a selection)
http://bost.ocks.org/mike/selection/#non-grouping
I'm having some issues trying to make my treemap resizable on window resize:
I started from this example:
https://secure.polisci.ohio-state.edu/faq/d3/zoomabletreemap_code.php
This is my resize() function, I do nothing more than remove the graph, redefine div#graph width and height and rerun the init code you can see in the link above. (drawGraph() do the d3 treemap work and json is a global var containing the data):
function resize(){
d3.select("#graph svg")
.remove();
margin = {top: 50, right: 0, bottom: 0, left: 0},
width = $('#graph').innerWidth(),
height = $('#graph').height() - margin.top - margin.bottom
x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
treemap = d3.layout.treemap()
.children(function(d, depth) { return depth ? null : d._children; })
.sort(function(a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false)
.value(function(d) { return d.size; })
svg = d3.select("#graph").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top)
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", "30px")
drawGraph(json)
}
Here the problem: the graph is redrawn correctly, but without any children! My treemap have height 3, after the resize it's like all the nodes become leafs.
SOLUTION
I edited the accumulate function:
function accumulate(d) {
if (d._children === undefined) {
return (d._children = d.children)
? d.value = d.children.reduce(function(p, v) { return p + accumulate(v); }, 0)
: d.value;
} else {
return d.value;
}
}
And on window resize event I update width and height of the svg element and re-execute:
initialize(), accumulate(), layout(), display()