I'm working on a horizontal bar graph and would like to display the values on each bar with a specific format. I've tried to code it in many ways but have been unsuccessful in achieving the results I want. I'm using the following code:
data = d3.json('data.json')
.then(data => {data
.forEach(d => {
d.country = d.country;
d.population = +d.population * 1000;
});
console.log(data);
render(data);
});
const xValuesNumberFormat = number => d3.format(',.0f')(number);
const xValues = d => d.population;
svg.selectAll('text')
.data(data)
.enter()
.append('text')
.attr('class', 'bar-value')
.attr('x', xPos)
.attr('y', yPos)
.text(xValues)
.attr('transform',`translate(5,`+yScale.bandwidth()/1.5+`)`)
;
[
{"country":"China","population":1415046},
{"country":"India","population":1354052},
{"country":"United States","population":326767},
{"country":"Indonesia","population":266795},
{"country":"Brazil","population":210868},
{"country":"Pakistan","population":200814},
{"country":"Nigeria","population":195875},
{"country":"Bangladesh","population":166368},
{"country":"Russia","population":143965},
{"country":"Mexico","population":130759}
]
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('class', 'rect')
//.attr('x', xPos)
.attr('y', yPos)
.attr('width', xPos)
.attr('height', yScale.bandwidth())
.on('mouseover', function (d)
{tooltip.style('display', null);})
.on('mouseout', function (d)
{tooltip.style('display', 'none');})
.on('mousemove', function (d)
{
var xPos = d3.mouse(this)[0] ;
var yPos = d3.mouse(this)[1] - 20;
tooltip.attr("transform", "translate("+xPos + "," + yPos +")");
tooltip.select('text').text(d3.format(',.0f')(d.population));
})
;
As soon as I try to format it I get Nan, undefined and various other errors.
enter image description here
As you can see from the image, I want the number next to the bar to be formatted like the number on the bar. Basically, I just want comma separators. Thank you for your help.
I figured it out.
svg.selectAll('text')
.data(data)
.enter()
.append('text')
.attr('class', 'bar-value')
.attr('x', xPos)
.attr('y', yPos)
.text(d => d3.format(',.0f')(+d.population))
.attr('transform', `translate(5,`+yScale.bandwidth()/1.5+`)`);
Thank you for taking the time to contribute.
Related
I am using this script to create a d3 barplot using r2d3 in R.
Example:
I would like to rotate the x-axis text labels. Does anyone know where the rotation should be applied? I am not familiar with javascript, only R unfortunately.
I have tried to insert the following based on other stackoverflow posts:
var txt = svg.selectAll('text').data(data);
txt.enter().append('text')
.attr('x', function(d, i) {return (i * col_width()) + (svg_width()* layer_left) + (col_width() * 0.5); })
.attr('y', function(d) {return svg_height()* 0.95;})
.style('font-size', '10px')
** .attr('transform', 'rotate(90)')**
.text(function(d) {return d.label;})
.style('font-family', 'arial')
.attr('text-anchor', 'middle');
txt.exit().remove();
txt.transition()
.duration(500)
.attr('x', function(d, i) {return (i * col_width()) + (svg_width()* layer_left) + (col_width() * 0.5); })
.attr('y', function(d) {return svg_height()* 0.95;})
.style('font-size', '10px')
** .attr('transform', 'rotate(90)')**
.text(function(d) {return d.label;})
.style('font-family', 'arial')
.attr('text-anchor', 'middle');
This does not work; the labels are no longer visible.
Thank you!
I want to create a bar chart like this:
There are two chart bars one below the other, the first one grows upwards while the second one grows downwards.
They have different scales and data.
This is what I created:
var doublebarSvg1 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg1')
.attr('width', 700)
.attr('height', 400);
var doublebarSvg2 = d3.select('#doublebar')
.append('svg')
.attr('class', 'doublebarSvg2')
.attr('width', 700)
.attr('height', 400);
var margin = {top: 0, right: 0, bottom: 0, left: 50};
var width = doublebarSvg1.attr('width') - margin.left - margin.right;
var height = doublebarSvg1.attr('height') - margin.top - margin.bottom;
var x = d3.scaleBand()
.rangeRound([0, width])
.padding(0.1)
.domain(years);
var y1 = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, 100]);
var y2 = d3.scaleSqrt()
.rangeRound([height, 0])
.domain([813, 0.1]); // max value 812.05 but domain is [0, 100000]
var doublebarSvgG1 = doublebarSvg1.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
var doublebarSvgG2 = doublebarSvg2.append('g').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');
////////////////////////////////////////////////////////////////////////
// Tooltip.
////////////////////////////////////////////////////////////////////////
var svgTip = doublebarSvg1.append('svg').attr('id', 'tooltip');
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-5, 0])
.html(function(d) {
return '<div><span>Country:</span> <span style=\'color:white\'>' + d.country + '</span></div>' +
'<div><span>Perc:</span> <span style=\'color:white\'>' + d.perc + '%</span></div>' +
'<div><span>Rate:</span> <span style=\'color:white\'>' + d.rate + '%</span></div>';
});
svgTip.call(tip);
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar1();
function makeDoublebar1() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis1 = d3.axisLeft(y1);
// create x axis
doublebarSvgG1.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + height + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG1.append('g')
.attr('class', 'y axis')
.call(yAxis1)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG1.selectAll('.bar')
.data(testData1) //.data(covFiltered)
.enter().append('rect')
.attr('fill', 'steelblue')
.attr('class', 'bar')
.attr('x', function(d) {
return x(d.year);
})
.attr('y', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return y1(d.perc);
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.perc)) {
d.perc = 0;
}
return height - y1(d.perc);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'darkblue');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'steelblue');
tip.hide(d);
});
}
////////////////////////////////////////////////////////////////////////
// Draw a single double bar
////////////////////////////////////////////////////////////////////////
makeDoublebar2();
function makeDoublebar2() {
// define the axes
var xAxis = d3.axisBottom(x);
var yAxis2 = d3.axisLeft(y2);
// create x axis
doublebarSvgG2.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 0)')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '-.8em')
.attr('dy', '.15em')
.attr('transform', 'rotate(-65)');
// create y axis
doublebarSvgG2.append('g')
.attr('class', 'y axis')
.call(yAxis2)
.append('text')
.style('text-anchor', 'end');
// create bar rect
doublebarSvgG2.selectAll('.bar')
.data(testData2)
.enter().append('rect')
.attr('fill', 'tomato')
.attr('class', 'bar')
.attr('x', function(d) { // left start point
return x(d.year);
})
.attr('y', function(d) { // top start point
if(isNaN(d.rate)) {
d.rate = 0;
}
return 0;
})
.attr('width', x.bandwidth())
.attr('height', function(d) {
if(isNaN(d.rate)) {
d.perc = 0;
}
return y2(d.rate);
})
.on('mouseover', function(d) {
d3.select(this).attr('fill', 'red');
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(this).attr('fill', 'tomato');
tip.hide(d);
});
}
PLUNKER here.
There are some problem:
if I replace .axis {display: initial;} with .axis {display: none;}, all the axis disappear but I want the horizontal line between the two chart
I would like there to be only one tooltip, which when the user hovers over any bar, comes out with a tooltip that shows both perc and rate value.
And, more importantly, is this the smartest way to create a chart like that?
Regarding the axis, since you want to keep the horizontal line, just hide the ticks and the texts:
.x.axis text,.x.axis line {
opacity: 0;
}
The tooltip problem is a bit more complex. The issue is that you're binding different data arrays to each set of bars.
Because of that, the best idea is finding the desired object in each array when you hover over a given year and getting the respective properties:
var thisPerc = testData1.find(function(e){return e.year === d.year}).perc;
var thisRate = testData2.find(function(e){return e.year === d.year}).rate;
Then you use those properties for setting the tooltip's text.
Here is the updated Plunker: http://plnkr.co/edit/tfB4TpkETgzp5GF1677p?p=preview
Finally, for your last question ("And, more importantly, is this the smartest way to create a chart like that?"), the answer is no. There are a lot of things that can (and must) be changed here, but this involves a lot of refactoring and it's arguably off topic at Stack Overflow. However, this is an adequate question for Code Review. But please read their docs first for keeping your question on topic, asking there is not the same as asking here.
I'm still fairly new to D3.js. Right now, I'm working on making a responsive bar chart. I'm using the viewbox to make it responsive in a div and using the DOM to set the height and width to the offsetWidth/Height of the div.
But the bars seem to be incorrect in height when set to the yScale.
Here's a screenshot of how they end up appearing:
I'm also being thrown this error in the console:
"d3.min.js:2 Error: attribute height: A negative value is not valid. ("-78.69462281846592")"
I believe the error has to do with this part of the code where I set the height attr to the height subtracted by the yScale data value:
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(d) {
return xScale(d.Year);
})
.attr('y', function(d) {
return yScale(d.Total);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return height - yScale(d.Total);
});
Here's the entire function that's drawing the bar chart:
<style>
#barchart {
width: 75vw;
height: 50vh;
}
</style>
function draw(data) {
var width = document.getElementById('barchart').offsetWidth,
height = document.getElementById('barchart').offsetHeight;
var svg = d3.select('#barchart')
.append('svg')
.attr('width', '100%')
.attr('height', '100%')
.attr('viewBox', '0 0 ' + width + ' ' + height)
.append('g');
var xScale = d3.scaleBand()
.range([0, width])
.padding(0.4);
var yScale = d3.scaleLinear()
.range([height, 0]);
xScale.domain(data.map(function(d) {
return d.Year;
}));
yScale.domain(data.map(function(d) {
return d.Total;
}));
var x_xaxis = svg.append('g')
.attr('transform', 'translate(' + 0 + ',' + 370 + ')')
.call(d3.axisBottom(xScale));
var y_axis = svg.append('g')
.attr('transform', 'translate(' + 20 + ',' + 0 + ')')
.call(d3.axisLeft(yScale)
.ticks(100));
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', function(d) {
return xScale(d.Year);
})
.attr('y', function(d) {
return yScale(d.Total);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return height - yScale(d.Total);
});
}
Without seeing your data it's tough to give a full answer but I'm certain the problem is that your yScale domain is being set incorrectly. Its linear so it needs an array of [minValue, maxValue], you are giving it an array of all data values. Try setting it with extent:
yScale.domain(d3.extent(data, function(d) {
return d.Total;
}));
Looking through this you want to start at zero, so:
yScale.domain([0, d3.max(data, function(d) {
return d.Total;
})]);
Also, you need to define the margins a bit more concretely. Take a look at these modifications.
I have a codepen here - https://codepen.io/anon/pen/GybENz
I've created a simple stacked bar chart with a legend to filter the chart.
I'd like to animated the height of the bar from the bottom axis up.
Currently its animating from the left and down
let layersBar = layersBarArea.selectAll('.layer').data(stackedSeries)
.enter()
.append('g')
.attr('class', 'layer')
.style('fill', (d, i) => {
return colors[i];
});
layersBar.selectAll('rect')
.data((d) => {
return d
})
.enter()
.append('rect')
.attr('height', 100)
.transition()
.duration(400)
.attr('height', (d, i) => {
return y(d[0]) - y(d[1]);
})
.attr('y', 0)
.attr('y', (d) => {
return y(d[1]);
})
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth());
}
Set the x position, the width, the y position (as the baseline) and the height (as zero) before the transition:
.attr('height', 0)
.attr("y", h - margin.bottom - margin.top)
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth())
Here is the updated CodePen: https://codepen.io/anon/pen/ypdoMK?editors=0010
PS: It would be a good idea transitioning each rectangle individually. For instance, if the user clicked usedInf, you should transition only those rectangles... however, since you did this...
layersBarArea.selectAll('g.layer').remove();
... at the beginning of your drawChart function, which is a wrong approach, such suggestion will need a big refactor, out of the scope of this question/answer.
I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);