What I'm trying to do is create a bar chart using D3.js (v4) that will show 2 or 3 bars that have a big difference in values.
Like shown on the picture below yellow bar has a value 1596.6, whereas the green bar has only 177.2. So in order to show the charts in elegant way it was decided to cut the y axis at a certain value which would be close to green bar's value and continue closer to yellow bar's value.
On the picture the y axis is cut after 500 and continues after 1500.
How one would do that using D3.js?
What you want is, technically speaking, a broken y axis.
It's a valid representation in data visualization (despite being frowned upon by some people), as long as you explicitly tell the user that your y axis is broken. There are several ways to visually indicate it, like these:
However, don't forget to make this clear in the text of the chart or in its legend. Besides that, have in mind that "in order to show the charts in elegant way" should never be the goal here: we don't make charts to be elegant (but it's good if they are), we make charts to clarify an information or a pattern to the user. And, in some very limited situations, breaking the y axis can better show the information.
Back to your question:
Breaking the y axis is useful when you have, like you said in your question, big differences in values. For instance, have a look at this simple demo I made:
var w = 500,
h = 220,
marginLeft = 30,
marginBottom = 30;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var data = [{
name: "foo",
value: 800
}, {
name: "bar",
value: 720
}, {
name: "baz",
value: 10
}, {
name: "foobar",
value: 17
}, {
name: "foobaz",
value: 840
}];
var xScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.name
}))
.range([marginLeft, w])
.padding(.5);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.value
})])
.range([h - marginBottom, 0]);
var bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
return xScale(d.name)
})
.attr("width", xScale.bandwidth())
.attr("y", function(d) {
return yScale(d.value)
})
.attr("height", function(d) {
return h - marginBottom - yScale(d.value)
})
.style("fill", "teal");
var xAxis = d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0," + (h - marginBottom) + ")"));
var yAxis = d3.axisLeft(yScale)(svg.append("g").attr("transform", "translate(" + marginLeft + ",0)"));
<script src="https://d3js.org/d3.v4.js"></script>
As you can see, the baz and foobar bars are dwarfed by the other bars, since their differences are very big.
The simplest solution, in my opinion, is adding midpoints in the range and domain of the y scale. So, in the above snippet, this is the y scale:
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, function(d) {
return d.value
})])
.range([h - marginBottom, 0]);
If we add midpoints to it, it become something like:
var yScale = d3.scaleLinear()
.domain([0, 20, 700, d3.max(data, function(d) {
return d.value
})])
.range([h - marginBottom, h/2 + 2, h/2 - 2, 0]);
As you can see, there are some magic numbers here. Change them according to your needs.
By inserting the midpoints, this is the correlation between domain and range:
+--------+-------------+---------------+---------------+------------+
| | First value | Second value | Third value | Last value |
+--------+-------------+---------------+---------------+------------+
| Domain | 0 | 20 | 700 | 840 |
| | maps to... | maps to... | maps to... | maps to... |
| Range | height | heigh / 2 - 2 | heigh / 2 + 2 | 0 |
+--------+-------------+---------------+---------------+------------+
You can see that the correlation is not proportional, and that's exactly what we want.
And this is the result:
var w = 500,
h = 220,
marginLeft = 30,
marginBottom = 30;
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var data = [{
name: "foo",
value: 800
}, {
name: "bar",
value: 720
}, {
name: "baz",
value: 10
}, {
name: "foobar",
value: 17
}, {
name: "foobaz",
value: 840
}];
var xScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.name
}))
.range([marginLeft, w])
.padding(.5);
var yScale = d3.scaleLinear()
.domain([0, 20, 700, d3.max(data, function(d) {
return d.value
})])
.range([h - marginBottom, h/2 + 2, h/2 - 2, 0]);
var bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
return xScale(d.name)
})
.attr("width", xScale.bandwidth())
.attr("y", function(d) {
return yScale(d.value)
})
.attr("height", function(d) {
return h - marginBottom - yScale(d.value)
})
.style("fill", "teal");
var xAxis = d3.axisBottom(xScale)(svg.append("g").attr("transform", "translate(0," + (h - marginBottom) + ")"));
var yAxis = d3.axisLeft(yScale).tickValues([0, 5, 10, 15, 700, 750, 800])(svg.append("g").attr("transform", "translate(" + marginLeft + ",0)"));
svg.append("rect")
.attr("x", marginLeft - 10)
.attr("y", yScale(19.5))
.attr("height", 10)
.attr("width", 20);
svg.append("rect")
.attr("x", marginLeft - 10)
.attr("y", yScale(19))
.attr("height", 6)
.attr("width", 20)
.style("fill", "white");
<script src="https://d3js.org/d3.v4.js"></script>
As you can see, I had to set the y axis ticks using tickValues, otherwise it'd be a mess.
Finally, I can't stress this enough: do not forget to show that the y axis is broken. In the snippet above, I put the symbol (see the first figure in my answer) between the number 15 and the number 700 in the y axis. By the way, it's worth noting that in the figure you shared its author didn't put any symbol in the y axis, which is not a good practice.
Related
I'm learning d3js and I need help in creating line charts. I'm fairly successful plotting simple objects and bar charts. Line charts seems to be a steep hill to climb.
const data = [{
"LINE1": [
10,
11,
12,
15
]
},
{
"LINE2": [
21,
22,
23,
32
]
},
{
"LINE3": [
11,
12,
13,
15
]
}
]
// set the dimensions and margins of the graph
var margin = {
top: 50,
right: 100,
bottom: 130,
left: 120
},
width = 900 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#ca")
.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 X axis
var x = d3.scaleLinear()
.domain(d3.extent(data, (d) => d.length))
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
// I need help in this area, how can I get the min and max values set in the domain?
var y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.values)])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// Draw the line
// I need help in this area, how can I get the lines plotted, js gives error in this!
svg.selectAll(".line")
.data(data)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1.5)
.attr("d", (d) => {
console.log(d)
var k = d3.line()
.x((d) => x(d))
.y((d) => y(d))
(d.values);
console.log(k);
return k;
});
<div id="ca">
</div>
<script src="https://d3js.org/d3.v6.min.js"></script>
How can I get the line charts plotted with data format I have?
I've made your example work, but there are several things you need to know here:
Your data structure was very chaotic. If you want to have an array of objects as data, make sure all objects have the same keys. It's fine if you use [{y: [...]}, {y: [...]}], but [{LINE1: [...]}, {LINE2: [...]}] is very difficult to work with. I changed your data to be more like [[...], [...], [...]] as a structure.
Don't create a separate d3.line for every line, just create it once and call it. It's a line factory, which means it is a function that, when called, returns a line. If it's not shared, it might use different domains and ranges, making the chart difficult or even useless.
If the first argument in a function is d, the second is i, the index of the node in the array. In this case, you use that to start at x=0, and go to x=3. You used d to try to get that value.
Keep in mind the structure of your data. You kept wanting to access d.values, but that never existed!
const data = Object.values({
"LINE1": [
10,
11,
12,
15
],
"LINE2": [
21,
22,
23,
32
],
"LINE3": [
11,
12,
13,
15
]
});
var line = d3.line()
.x((d, i) => x(i))
.y((d) => y(d));
// set the dimensions and margins of the graph
var margin = {
top: 50,
right: 100,
bottom: 130,
left: 120
},
width = 900 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#ca")
.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 X axis
var x = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.length)])
.range([0, width]);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).ticks(5));
// Add Y axis
// I need help in this area, how can I get the min and max values set in the domain?
var y = d3.scaleLinear()
.domain([0, d3.max(data, (d) => Math.max(...d))])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// Draw the line
// I need help in this area, how can I get the lines plotted, js gives error in this!
svg.selectAll(".line")
.data(data)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 1.5)
.attr("d", (d) => line(d));
<div id="ca">
</div>
<script src="https://d3js.org/d3.v6.min.js"></script>
I want to create a barchart displaying C02 emission.
The Problem (see picture below):
Why are the bars "pushed" to the right? Why are the years in the x-axis displayed without the first integer?
I am using Version 3 of d3.
Given some JSON data like this:
[
{
"Cement": 0.0,
"Gas Flaring": 0.0,
"Gas Fuel": 0.0,
"Liquid Fuel": 0.0,
"Per Capita": null,
"Solid Fuel": 3.0,
"Total": 3.0,
"Year": 1751
},
and so on…
]
To prepare for scaling I did:
var minDate = dataset[0].Year;
var maxDate = dataset[dataset.length - 1].Year;
var maxValue = d3.max(dataset, function(d) {
return d["Per Capita"];
});
I append the svg
var svg = d3
.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
I sacled the xAxis and the yAxis:
var xAxisScale = d3.time
.scale()
.domain([minDate, maxDate])
.range([0, w]);
var yAxisScale = d3.scale
.linear()
.domain([0, maxValue])
.range([h, 0]);
The I finally builded these axisses…
var xAxis = d3.svg
.axis()
.scale(xAxisScale)
.orient("bottom");
var yAxis = d3.svg
.axis()
.scale(yAxisScale)
.orient("left");
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(92," + (h - padding) + ")")
.call(xAxis);
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",-90)")
.call(yAxis);
I also than addeded the rects…
svg
.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.style("fill", "teal")
.attr({
x: function(d, i) {
return i * (w / dataset.length);
},
y: function(d) {
return yAxisScale(d["Per Capita"]);
},
width: w / dataset.length,
height: function(d) {
return h - yAxisScale(d["Per Capita"]);
}
});
The result is not the intended one.
Could you please elaborate what went wrong?
Why are the bars "pushed" to the right?
Why are the years in the x-axis displayed without the first integer?
I am using Version 3 of d3.
Thank you very much!
The main problem here is that this...
"Year": 1751
... is not a date object. That's just a number. If you look at your axis you'll realise that.
So, you have to parse it. For instance:
const format = d3.time.format("%Y");
dataset.forEach(function(d){
d.Year = format.parse(d.Year);
});
Also, when you do this...
var minDate = dataset[0].Year;
var maxDate = dataset[dataset.length - 1].Year;
... you're blindly trusting that the array is sorted. Don't do that. Instead, do:
var minDate = d3.max(dataset, function(d){
return d.Year
});
var maxDate = d3.min(dataset, function(d){
return d.Year
});
Or, if you want to use destructuring:
var [minDate, maxDate] = d3.extent(dataset, d => d.Year);
Finally, now that you have a proper scale, don't use the indices for the x position. Use the scale:
x: function(d) {
return xAxisScale(d.Year);
},
This covers the problem regarding the x position. For fixing the y position, just set a proper margin.
I want to show the axisBottom value as an integer, with nothing in between the whole numbers. In the below image I use tickFormat which switched the values to integers, but now I need to only have the values of '1, 2, 3..etc" rather than the duplicate integer values. I need the number of ticks to be dynamically generated, meaning I can't statically say there are 3 ticks. The data I pass may have a max value of 3 or any other number, but they will all be whole numbers.
Data (JSON)
[ { yAxis: '15.1.1', xAxis: 2 },
{ yAxis: '15.1.2', xAxis: 2 },
{ yAxis: '15.1.3', xAxis: 1 },
{ yAxis: '15.1.4', xAxis: 3 },
{ yAxis: '15.1.5', xAxis: 0 },
{ yAxis: '15.1.6', xAxis: 1 },
{ yAxis: '15.1.7', xAxis: 0 },
{ yAxis: '15.1.8', xAxis: 0 } ]
Images and code below.
var data = !{dataObj}; //using Jade as template engine
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width]);
var svg = d3.select(".barChartContainer").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 + ")");
// format the data
data.forEach(function(d) {
d.xAxis = +d.xAxis;
});
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.xAxis; })])
y.domain(data.map(function(d) { return d.yAxis; }));
//y.domain([0, d3.max(data, function(d) { return d.prereqs; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
//.attr("x", function(d) { return x(d.prereqs); })
.attr("width", function(d) {return x(d.xAxis); } )
.attr("y", function(d) { return y(d.yAxis); })
.attr("height", y.bandwidth());
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
You have two options here.
The first one is showing only the integers, but keeping the ticks. This can be done testing if the number is an integer inside tickFormat:
.tickFormat(function(d) {
return d % 1 ? null : d;
});
Here is a demo:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.range([20, 480])
.domain([0, 3]);
var axis = d3.axisBottom(scale)
.tickFormat(function(d) {
return d % 1 ? null : d;
});
var gX = svg.append("g")
.attr("transform", "translate(0, 50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="100"></svg>
However, if you want to show only the ticks for the integers, the solution is using tickValues:
.tickValues(d3.range(scale.domain()[0], scale.domain()[1] + 1, 1))
Here is a demo:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.range([20, 480])
.domain([0, 3]);
var axis = d3.axisBottom(scale)
.tickValues(d3.range(scale.domain()[0], scale.domain()[1] + 1, 1))
.tickFormat(function(d) {
return ~~d;
});
var gX = svg.append("g")
.attr("transform", "translate(0, 50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="100"></svg>
I have a barchart which is populated by values from a JSON variable. The chart is dynamic because the values are retrieved from an SQL query by doing a count. So my data gets fed back in like :
[{"Fruit":"Apple","COUNT( Fruit )":"12"},{"Fruit":"Orange","COUNT( Fruit )":"6"},{"Fruit":"Pear","COUNT( Fruit )":"1"},{"Fruit":"Blank","COUNT( Fruit )":"1"},{"Fruit":"Pineapple","COUNT( Fruit )":"1"},{"Fruit":"Kiwi","COUNT( Fruit )":"1"}]
For the most part my graphs seem to be displaying properly. However some are returning values that exceed the Y Axis, I dont think it's the values that are causing the issues I believe its the axis that isnt calculating the right height. For instance
If Orange count is 14, sometimes the Y axis stops at a number less than this and that column extends the graph.
By viewing it in google chrome developer console, I can see the height of the bar is
<rect id="Orange" y="-520px" x="94px" height="1040px" width="162"></rect>
which far extends my SVG height of 600px - Margins(top + bottom) of 80px!
Does anyone know why my Y Axis isn't getting the right Max value?
Code here:
var canv = document.getElementById("exportCanvas");
canv.width = screen.width;
var margin ={top:40, right:0, bottom:40, left:40},
width=screen.width - 250,
height=600-margin.top-margin.bottom;
var jsplit = jdata.split('"');
var keyX = jsplit[1];
var keyY = "";
var data = JSON.parse(jdata);
for (k in data[0]) {
if (k!=keyX) keyY=k;
}
console.log("keyX = " + keyX);
console.log(keyY);
console.log(data[0]);
// scale to ordinal because x axis is not numerical
var x = d3.scale.ordinal()
.domain(['Orange','Apple','Pear']) //Added this in temporarilly. this should be calculated from the data.
.rangeRoundBands([0, width], 0.25,0.25);
//scale to numerical value by height
// var y = d3.scale.linear().range([height, 0]);
var y = d3.scale.linear()
.range([height, 0]);
console.log(data);
x.domain(data.map(function(d){ return d[keyX]}));
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
var chart = d3.select("#chart")
.append("svg") //append svg element inside #chart
.attr("width", width+ margin.left+margin.right) //set width
// .attr("width", width+(2*margin.left)+margin.right) //set width
.attr("height", height+margin.top+margin.bottom); //set height
// .attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom"); //orient bottom because x-axis will appear below the bars
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10).tickFormat(function(d) {
if (d % 1 == 0) {
return d3.format('.f')(d)
} else {
return ""
}
});
var bar = chart.selectAll("g")
.data(data)
.enter()
.append("g");
//you're moving the group and then moving the rect below as well
//one or the other not both need to be moved.
//.attr("transform", function(d, i){
// return "translate("+x(d[keyX])+", 0)";
//});
bar.append("rect")
.attr("id", function(d) {
return d[keyX];
})
.attr("y", function(d) {
return y(d[keyY]) + "px";
})
.attr("x", function(d,i){
//AB - Adjusted this so it correcly places the bar along the X
//x.range is an array of x values for each bar
//calculated in the var x = line above , with the .rangeRoundBands([0, width], 0.25,0.25);
//setting the width of the bars (an equal division of width) with margins of 0.25 at the start
//and end of the graph and 0.25 between each bar.
return x.range()[i] + margin.left + "px";
})
.attr("height", function(d) {
return height - y(d[keyY]) +"px";
})
.attr("width", x.rangeBand()); //set width base on range on ordinal data
bar.append("text")
.attr("x",function(d,i){
//similar to above but adding half the width of the bar to the x position
//to roughly center it on the bar. only rough as doesnt take account of length of text.
return x.range()[i] + margin.left + (x.rangeBand()/2)+ "px";
})
.attr("y", function(d) { return y(d[keyY]) +20; })
.attr("dy", ".75em")
.style("fill","white")
.style("font-weight", "bold")
.text(function(d) { return d[keyY]; });
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate("+margin.left+","+ height+")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.attr("transform", "translate("+margin.left+",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text(keyY);
Apologies for commented out code, I have been playing with it alot to try and suss this out.
You need to recalculate y.domain() when your dataset refreshes. So when you update your data, you can try something like:
y.domain([0, d3.max(data, function(d){return d[keyY]})]);
chart.select(".y.axis")
.call(yAxis.scale(y));
First time using D3 and I'm stuck on something I think should be simple:
I'm trying to make a percentage width bar chart SVG and I'd like to set the width of each bar to be the number of 'bars' it will be creating.
svg.selectAll(".bar")
.data(function(d) {return d.values;})
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.browser); })
.attr("width", function(d) { return (100 / d.browser.length);})
.attr("y", function(d) { return y(d.time); })
.attr("height", function(d) { return height - y(d.time); })
.attr("fill", function(d) {return "#"+d.colour;})
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
The key part that isn't working being this: .attr("width", function(d) { return (100 / d.browser.length);})
Which doesn't work. Here is a sample of my TSV data:
test browser time colour
1 - attr selector Chrome 65 fad009
1 - attr selector Firefox 125 dd8e27
1 - attr selector Opera 72 cc0f16
1 - attr selector IE9 140 27b7ed
1 - attr selector Android-4 120 80bd01
2 - attr qualified Chrome 64 fad009
2 - attr qualified Firefox 132 dd8e27
2 - attr qualified Opera 78 cc0f16
2 - attr qualified IE9 120 27b7ed
2 - attr qualified Android-4 145 80bd01
Both column 1 and 2 are constants in that they are only ever one of 5 values and whilst I can hard-code 20% in there I'd like to set it properly so that is I add another set of results it still spaces correctly.
My JS-fu is weak so feel free to patronise me like you were teaching a child ;)
Any thoughts?
Further to request for for code:
// Adapted from http://bl.ocks.org/officeofjane/7315455
var margin = {top: 45, right: 20, bottom: 20, left: 200},
width = 850 - margin.left - margin.right,
height = 90 - margin.top - margin.bottom;
var formatPercent = d3.format(".0%");
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.1);
// Scales. Note the inverted domain fo y-scale: bigger is up!
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")
.tickFormat(formatPercent);
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>" + d.test + "\t" + d.browser + "</strong><br/><span style='color:#fff'>" + d.time + " ms</span>";
});
// csv loaded asynchronously
d3.tsv("data2.tsv", type, function(data) {
// Data is nested by country
var selectorTests = d3.nest()
.key(function(d) { return d.test; })
.entries(data);
// Compute the minimum and maximum year and percent across symbols.
x.domain(data.map(function(d) { return d.browser; }));
y.domain([0, d3.max(selectorTests, function(s) { return s.values[0].time; })]);
// Add an SVG element for each country, with the desired dimensions and margin.
var svg = d3.select("#selectors").selectAll("svg")
.data(selectorTests)
.enter().append("svg")
.attr({
"width": "100%",
"height": "100%"
})
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("pointer-events", "all")
.append("g").attr("transform", "translate(0,0)");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.append("text")
.attr("x", "0")
.attr("y", '-100%')
.attr("dy", ".71em")
.attr("text-anchor", "start")
.attr("font-size", "1.1em")
.text(function(d) { return d.key;});
// Accessing nested data: https://groups.google.com/forum/#!topic/d3-js/kummm9mS4EA
svg.selectAll(".bar")
.data(function(d) {return d.values;})
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.browser); })
.attr("width", data.length)
.attr("y", function(d) { return y(d.time); })
.attr("height", function(d) { return height - y(d.time); })
.attr("fill", function(d) {return "#"+d.colour;})
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
svg.call(tip);
});
function type(d) {
d.time = +d.time;
return d;
}
If I just use data.length I get '10' which is the total number of 'entries' (apologies I probably have completely incorrect terminology here) - I want/expect to see '5' as that is the number of bars that get created with each instance give the data. Hope that makes sense?
I think it's useful to visualize how you're data is being formatted and "passed around":
d3.nest returns your data in this format:
[
{key: "1 - attr selector", values: [
{test: "1 - attr selector", browser: "Chrome", time: 65, colour: "fad009"},
{test: "1 - attr selector", browser: "Firefox", time: 125, colour: "dd8e27"},
...
]},
{key: "2 - attr qualified", values: [
{test: "2 - attr qualified", browser: "Chrome", time: 64, colour: "fad009"},
{test: "2 - attr qualified", browser: "Firefox", time: 132, colour: "dd8e27"},
....
]},
...
]
Then you have two subsequent calls to data: one in the svg definition and another one in the first code snippet you posted. After the first one the d element will be of the form
{key: "1 - attr selector", values: [
{test: "1 - attr selector", browser: "Chrome", time: 65, colour: "fad009"},
{test: "1 - attr selector", browser: "Firefox", time: 125, colour: "dd8e27"},
...
]}
And this is where you should call d.values.length to get the number of browsers for that specific test.
After the second call to data, where you do .data(function(d) {return d.values;}) each d element is in the form
{test: "1 - attr selector", browser: "Chrome", time: 65, colour: "fad009"}
And you see that at this point calling d.browser.length will return the length of the string with the browser name - not really what you want.
You have to get the number of browser before calling data for the second time, but you have to use it after calling it - that's the problem. You need to get the parent datum.
I hope I made the problem clear to you.
Talking about solutions, there are several. I'm not sure but d3.select(this.parentNode).datum().length or d3.select(this).node().parentNode.__data__.length or something similar instead of d.browser.length could work. Or you could remember the length in a variable (better, an array: each test could have a different number of browsers it was tested on) out of the selection and then use it.