I have the following csv data,
date,scanned,unscanned,compid,sbu
01/2014,10,90,101,f&r
02/2014,55,40,101,f&r
03/2014,45,23,101,f&r
04/2014,65,35,101,f&r
05/2014,100,20,101,f&r
06/2014,50,30,101,f&r
07/2014,10,90,101,f&r
08/2014,22,48,101,f&r
09/2014,0,100,101,f&r
10/2014,3,97,101,f&r
11/2014,22,60,101,f&r
12/2014,57,37,101,f&r
01/2014,30,100,101,ip
02/2014,130,10,101,ip
Is there a way that we can combine the data for jan-2014 for both the f&r and ip sbu values and show the values in stacked bar. for e.g if i check a checkbox to group, i need to show scanned as 30+10=40 and unscanned as 100+90= 190 in a stack for jan 2014 in x-axis.
My code to build the stack bar is as follows:
var w = 960,
h = 500,
p = [20, 50, 30, 20],
x = d3.time.scale().range([1, 80]);
y = d3.scale.linear().range([0, h - p[0] - p[2]]),
z = d3.scale.ordinal().range(["#819FF7", "#CB491A"]),
parse = d3.time.format("%m/%Y").parse,
format = d3.time.format("%b-%y");
var xAxis=d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.month, 1)
//.ticks(12)
xAxis.tickFormat(d3.time.format("%b-%y"));
/*var yAxis = d3.svg.axis()
.scale(y)
.ticks(12)
.orient("left");*/
var svg = d3.select("#container").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + p[3] + "," + (h - p[2]) + ")");
d3.csv("scandata.csv", function(scan) {
// Transpose the data into layers by cause.
var scantypes = d3.layout.stack()(["scanned", "unscanned"].map(function(scans) {
return scan.map(function(d) {
return {x: parse(d.date), y: +d[scans],z:d.compid,typescan:scans};
});
}));
// Compute the x-domain (by date) and y-domain (by top).
x.domain(scantypes [0].map(function(d) { return d.x; }));
y.domain([0, d3.max(scantypes[scantypes .length - 1], function(d) { return d.y0 + d.y; })]);
// Add a group for each scan.
var cause = svg.selectAll("g.scan")
.data(scantypes)
.enter().append("svg:g")
.attr("class", "scan")
.style("fill", function(d, i) { return z(i); })
.style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); });
// Add a rect for each date.
var rect = cause.selectAll("rect")
.data(Object)
.enter().append("svg:rect")
.attr("id", function(d,i) { return i + " comp " + d.z; })
.attr("x", function(d,i) {
if (i ==0)
{
return x(d.x) ;
}
else
{
return x(d.x);
}} )
.attr("y", function(d) { return -y(d.y0) - y(d.y); })
.attr("height", function(d) { return y(d.y); })
.attr("width", 30)//x.rangeBand()/2
.on("mouseover", function(d){
return tooltip.style("visibility", "visible")
.text((d.y))//d.typescan + " - " +
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 20) + "px"); ;})
.on("mousemove", function(d){
return tooltip.style("visibility", "visible")
.text((d.y)) //d.typescan + " - " +
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 20) + "px"); ;})
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.on("click", function(d){});
var tooltip = d3.select("#container")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "visible")
.text("Scanned vs UnScanned")
.style("font", "Arial")
.style("color", "white")
.style("font-size", "14px");
//Add x-Axis
svg.append("g")
.attr("class", "x axis")
//.attr("transform", function(d) { return "translate(0,80)"; })
.call(xAxis)
// Add a label per date.
var label = svg.selectAll("text")
.data(x.domain())
.enter().append("svg:text")
.attr("x", function(d) { return x(d.x); })//x.rangeBand() / 4
.attr("y", 6)
.attr("text-anchor", "middle")
.attr("dy", ".71em")
.text(format);
// Add y-axis rules.
var rule = svg.selectAll("g.rule")
.data(y.ticks(5))
.enter().append("svg:g")
.attr("class", "rule")
.attr("transform", function(d) { return "translate(0," + -y(d) + ")"; });
rule.append("svg:line")
.attr("x2", w - p[1] - p[3])
.style("stroke", function(d) { return d ? "#fff" : "#000"; })
.style("stroke-opacity", function(d) { return d ? .7 : null; });
rule.append("svg:text")
.attr("x", -15)
.style("font-family","Arial 12px")
.attr("dy", ".25em")
.text(d3.format(",d"));
You seem to be confused about what the SVG should look like, and so don't know how to make it happen.
The bars in SVG are just rectangles. You need to tell them where they should be positioned (which is always defined by the top left corner of the bar) and how big they should be.
To get the bars to line up in a stacked graph, you need to figure out their position and size based on all the values for that stack.
I've created a very simplified example of a stacked bar chart using your data (just the scanned/unscanned data, I haven't separated things out by the sbu variable).
Here's the working example
Here's the code with comments:
var width = 400;
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scale.ordinal()
.rangeRoundBands([0,width], 0.1);
var yScale = d3.scale.linear()
.range([height, 0]);
//note the inverted range, so that small values
//scale to the bottom of the SVG
var data = d3.csv.parse( d3.select("pre#data").text() );
//this just grabs the text from the preformatted block
//and parses it as if it was a csv file
//in your real code, you would use d3.csv(filename, callbackFunction)
//and the rest would be inside your callback function:
xScale.domain( data.map(function(d){return d.date;}) );
//the xScale domain is the list of all categorical values.
//The map function grabs all the date values from the data
//and returns them as a new array.
//I'm not worrying about parsing dates, since
//strings work fine with an ordinal scale
//(although you'd want to parse them if you wanted to reformat them).
yScale.domain( [0,
d3.max(data,
function(d){
return +d.scanned + +d.unscanned;
})
]);
//The yScale domain starts at 0 (since it's a bar chart)
//and goes to the maximum *total* value for each date.
//The d3.max function finds the maximum for an array
//based on the result returned by the function for each
//element of the array. This function just finds the sum
//of the scanned and unscanned values
//(after converting them from strings to numbers with "+").
var dateGroups = svg.selectAll("g")
//create an empty selection of groups
.data(data); //join to the data, each row will get a group
dateGroups.enter().append("g")
//create the actual <g> elements for each row of data
.attr("class", "dateGroup");
//give them a meaningful class
//Now, within each group create a rectangle
//for each category (scanned and unscanned).
//If you had lots of categories, you'd want to
//use a nested selection and a second data join.
//However, to do that you'd need to do a lot of
//data manipulation to create an array of
//separate data objects for each category.
//
//With only two categories, it's easier to just
//do each one separately, and let them inherit
//the data from the parent <g> element.
//For the bottom of the stack:
var bottom = dateGroups.append("rect")
.attr("class", "data scanned");
bottom.attr("y", function(d){
return yScale(+d.scanned);
} )
//y is the TOP of the rectangle
//i.e., the position of this data value
//on the scale
.attr("height", function(d){
return Math.abs( yScale(+d.scanned) - yScale(0) );
//The height of the rectangle is the difference between
//its data value and the zero line.
//Note that the yScale value of zero is
//bigger than the yScale value of the data
//because of the inverted scale, so we use
//absolute value to always get a positive height.
} );
//For the top of the stack:
var top = dateGroups.append("rect")
.attr("class", "data unscanned");
top.attr("y", function(d){
return yScale(+d.unscanned + +d.scanned);
} )
//y is the TOP of the rectangle
//i.e., the position on the scale of
//the *total* of the two data categories
.attr("height", function(d){
return Math.abs( yScale(+d.unscanned) - yScale(0) );
//The height of this bar is just based on
//its value. However, this could also be
//written as
//Math.abs(+yScale(+d.scanned + +d.unscanned)
// - yScale(+d.scanned) )
//i.e., as the difference between the total
//(top of the bar) and the other category's
//value (bottom of the bar)
} );
//The x value and width are the same for both bars
//so we can re-select all the rectangles and
//set these attributes at the same time:
dateGroups.selectAll("rect.data")
.attr("x", function(d){
return xScale(d.date);
})
.attr("width", xScale.rangeBand() );
//don't need a function for width,
//since it doesn't depend on the data
Once you are sure you understand what is happening at every step of that program, then you can start to add extra features like axes or tooltips. You will also be in a good position to adapt the code to work with many categories, although in that case you will probably want to create a sub-array of representing the data for each category, and use a nested selection to create the rectangles. That's the approach used by most stacked bar graph examples; they will hopefully be easier to understand after working with this very simplified version.
Edit
The above solution works if you know that you only have two values in each stack, with the data for both values from the same row of the data table. If you might have many bars in each stack, and/or if they come from multiple rows of the data table, you will want to use a nested selection to match the data to individual bars.
In order to use the nested selection approach, you first have to do some manipulation to your data. You need to get it into nested array format. The outer array has to represent each stack, and each stack data object has to include a sub-array representing each bar.
How you make the nested array depends on your original data format. When the values that you want to stack are in different rows, the d3.nest operator can group them together into sub-arrays. When the stacked values are different numbers from the same row of the data table, you have to use a forEach() function to loop through all the rows of your data and construct an array from each.
In your example, you want to do both, so we're going to combine a nesting operation with a forEach operation. At the same time, we're going to calculate the running totals for the stack: in order to position each bar correctly, we need to know not only its own count, but also the total count of all values under it in the stack.
Here's a working fiddle
The data manipulation code is
/*Nest data by date string */
var nestFunction = d3.nest().key( function(d){return d.date;} );
var nestedData = nestFunction.entries(data);
var maxTotal = 0; //maximum count per date,
//for setting the y domain
nestedData.forEach(function(dateGroup) {
//for each entry in the nested array,
//each of which contains all the rows for a given date,
//calculate the total count,
//and the before-and-after counts for each category.
dateGroup.date = dateGroup.key;
//just using the original strings here, but you could
//parse the string date value to create a date object
dateGroup.bars = [];
//create an array to hold one value for each bar
//(i.e., two values for each of the original rows)
var total = 0; //total count per date
dateGroup.values.forEach(function(row) {
//the values array created by the nest function
//contians all the original row data objects
//that match this date (i.e., the nesting key)
//create an object representing the bar for
//the scanned count, and add to the bars array
dateGroup.bars.push(
{date:dateGroup.date,
type: "scanned",
count: +row.scanned,
compid: row.compid,
sbu: row.sbu,
y0: total, //total value *before* this bar
y1: (total = total + +row.scanned) //new total
}
);
//create an object representing the bar for
//the UNscanned count, and add to the bars array
dateGroup.bars.push(
{date:dateGroup.date,
type: "unscanned",
count: +row.unscanned,
compid: row.compid,
sbu: row.sbu,
y0: total, //total value *before* this bar
y1: (total = total + +row.unscanned) //new total
}
);
});
maxTotal = Math.max(maxTotal, total); //update max
});
If you didn't want to stack certain types of bars together -- for example, if you wanted to keep the values from different compids in different stacks -- then you would include that parameter as a second key to the nesting function. Values are only nested together if they match on all the nesting keys. Of course, then you'd also have to amend your x-scale to separate out the stacks by both keys. Look up examples of grouped bar charts for how to do that.
Once you have the properly nested data, you join the outer array (the array of nest objects) to <g> elements representing each stack, and then create a nested selection of rectangles within each group and join the inner array (the bar data) to it:
var dateGroups = svg.selectAll("g")
//create an empty selection of groups
.data(nestedData); //join to the data,
//each nested object (i.e., date) will get a group
dateGroups.enter().append("g")
//create the actual <g> elements for each row of data
.attr("class", "dateGroup");
//give them a meaningful class
//Now, within each group create a rectangle
//for each category from the "bars" array created earlier.
//This uses a nested selection, since we don't know
//how many bars there will be for a given date.
var bars = dateGroups.selectAll("rect")
.data( function(d) {return d.bars;})
//the 'd' value passed in is the data for each
//dateGroup, each of which will now have a
//nested selection of bars
bars.enter().append("rect"); //create the rectangles
bars.attr("class", function(d){
//assign classes for all the categorical values
//(after stripping out non-word characters)
//so they can be styled with CSS
var specialchars = /\W+/g;
//regular expression to match all non-letter, non-digit characters
return ["data",
"type-" + d.type.replace(specialchars, ""),
"compid-" + d.compid.replace(specialchars, ""),
"sbu-" + d.sbu.replace(specialchars, "")
].join(" "); //class list is space-separated
})
.attr("y", function(d){
return yScale(d.y1);
//y is the TOP of the rectangle
//i.e., the position of the *total* value
//for this bar and all others under it in the stack
} )
.attr("height", function(d){
return Math.abs( yScale(d.y1) - yScale(d.y0) );
//the height of the rectangle is the difference
//between the total *after*
//this value is added to the stack
// (top of the bar, y1)
//and the total *before* it is added
// (bottom of the bar, y0)
//Since this is a linear scale, this could also
//be written as
//Math.abs( yScale(d.count) - yScale(0) )
//i.e., as the difference between
//its data value and zero line.
//Note the use of absolute value to
//compensate for a possibly inverted scale.
} )
.attr("x", function(d){
return xScale(d.date);
})
.attr("width", xScale.rangeBand() )
//don't need a function for width,
//since it doesn't depend on the data
.append("title") //add a tooltip title
.text(function(d) {
return d.sbu + ", " +d.type +":" + d.count;
});
Related
Let me preface this by saying I am brand new to D3.js and coding in general. I am an infographic artist and I've been using QGIS to generate maps, but am trying to use D3.js to generate a choropleth map for a story about Opioid deaths. Basically I am trying to recreate this map.
map from the Economist
I have tried to start by using this map by Mike Bostock and changing some of the parameters but am getting stuck with the color range and scale. The measurement is 1 per 100,000 population. I have a domain that starts at 1.543385761 and ends at 131.0814217.
The code I'm struggling with is around the scale input and output:
var x = d3.scaleLinear()
.domain([0, 132])
.rangeRound([600, 860]);
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0, 40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d) { return x(d[0]); })
.attr("width", function(d) { return x(d[1]) - x(d[0]); })
.attr("fill", function(d) { return color(d[0]); });
I can see that I need some bit of code that will define everything 25 and over as the darkest color. Not even sure I want that to be my final legend but I'd love to know how to reproduce that. I am shocked I was able to get this far but feel a bit lost right now. thank you in advance!
Let's examine your scale:
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBlues[9]);
Your domain is an array of created like so:
d3.range(2,10) // [2,3,4,5,6,7,8,9]
These are your thresholds, colors will be mapped based on values that are less than or equal to 2, more than two up to three, more than three and up to four .... and over 9. This domain is mapped to nine values defined in the range:
d3.schemeBlues[9] // ["#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", #6baed6", #4292c6", "#2171b5", "#08519c", "#08306b"]
To set the thresholds for those colors so that values over 25 are one color, define the domain with array that has the appropriate threshold(s):
.domain([2,3,4,5,6,7,8,25]);
In the snippet below, this domain is applied. Rectangles have colors dependent on their location, all rectangles after the 25th (count left to right then line by line) one will be of one color.
var color = d3.scaleThreshold()
.domain([2,3,4,5,6,7,8,25])
.range(d3.schemeBlues[9]);
var svg = d3.select("body")
.append("svg")
.attr("width",500)
.attr("height",500);
var rects = svg.selectAll("rect")
.data(d3.range(100))
.enter()
.append("rect")
.attr("width",15)
.attr("height", 15)
.attr("y", function(d,i) { return Math.floor(i / 10) * 20 + 10 })
.attr("x", function(d,i) { return i % 10 * 20 })
.attr("fill", function(d) { return color(d); })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
I've got this legend:
As you can see, each legend entry is the same width. Instead, I'd like each legend entry's width to vary based upon the width of the entry's symbol and text. Ultimately, I want the same distance between the ends of the leading entry's text and the start of the following entry's symbol. In other words, I'd like the same distance between 'OA' and the plus sign as between the 'OI' and the diamond and the 'RARC' and the square. I need this to be based on pixels (string lengths won't suffice). I've been trying all sorts of stuff, but haven't been successful.
Here's my code:
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangle-down"]];
this.svg.selectAll('.legend').remove() //remove remnants of previous legend so new legend has clean slate...eliminates overlays during resizing
var legend = this.svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(' + (ScatterChart.Config.margins.left + (width * .008)) + ',' + (height += .40 * ScatterChart.Config.margins.bottom) + ')');
var legendRect = legend
.selectAll('g')
.data(legendData)
;
var labelLength = 0
var labelLengthPrevious = 0
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
//labelLength = labelLengthPrevious //Need to figure out pixel lengths
//labelLengthPrevious += (d[0].length) + 50
//return 'translate(' + labelLength + ', ' + 0 + ' )'; // y is constant and x growing
return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing
})
;
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => {
return d[2]
}
).size((d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier) * (d3.min([height, width]) * ScatterChart.Config.symbolSizeMultiplier)))
.style("fill", function (d) {
return d[1];
})
.attr('stroke', 'black')
;
//This asserts legendRectE as a node...I think. I do this so I can use the width and height measurements of legendRectE.
var node: SVGElement = <SVGElement>legendRectE.node()
legendRectE
.append("text")
.attr("x", function (d) {
return node.getBoundingClientRect().width
})
.attr("y", function (d) {
return node.getBoundingClientRect().height / 2.25
})
.text(function (d) {
return d[0];
})
.style('font-size', function () { return d3.min([height, width]) * ScatterChart.Config.axisFontMultiplier + "px" })
;
I think the answer would have something to do with this line: return 'translate(' + (i * (.15 * width)) + ', ' + 0 + ' )'; // y is constant and x growing. Right now, it just shifts to the right by multiplying the index by 15% of the chart's width. I figure I need to somehow substitute the width of the legendRectE (or of legendRect or legend) in place of (I * (.15 * width)). I can't figure out how to do that.
You can see that I use the following to get the width of legendRectE later in the code: var node: SVGElement = <SVGElement>legendRectE.node(), followed by node.getBoundingClientRect().width.
node.getBoundingClientRect().width gives me a width value where you see it being used now, but when I use this same approach to determine a value for the translate I mentioned, it chokes; and when I use legendRect or legend instead of legendRectE I only get '0'.
I thought I'd be able to edit the transform function something like this:
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function (d, i) {
var node: SVGElement = <SVGElement>legendRectE.node()
return 'translate(' + node.getBoundingClientRect().width + ', ' + 0 + ' )'; // y is constant and x growing
})
;
Obviously, I was wrong. Any ideas/advice?
p.s. I'm using d3 v3.5.
The challenge is that it is (as far as I know) difficult to determine the transform when appending elements initially as the widths are unknown. But you could go back and calculate the width of each legend entry after they are all appended and then reposition the legend entries accordingly.
The snippet below positions everything overtop of each other to start, then calculates the svg width of each legend g using getBBox. Then, using d3.sum, calculates the width of each element that was appended before it (and thus should be to the left of it) and sets the translate value to the sum of those widths accordingly.
It can probably be cleaned up a bit probably, it's a little quick. If there is lag before the elements are positioned correctly, appending them transparently and then fading them in after they are positioned might be an elegant (visually, less so programatically) solution (or appending them initially outside of the view box).
d3v4:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var rect = groups.append('rect')
.attr('fill',function(d,i) { return d3.schemeCategory10[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups.nodes()[j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
d3v3:
var data = ['short text','much longer text','the longest text passage','short text'];
var svg = d3.select('body')
.append('svg')
.attr('width',800)
.attr('height',200);
var groups = svg.selectAll('g')
.data(data)
.enter()
.append('g');
var color = ["orange","red","purple","green"];
var rect = groups.append('rect')
.attr('fill',function(d,i) { return color[i];})
.attr('height',30)
.attr('width',30);
var text = groups.append('text')
.attr('y', 20)
.attr('x', 35)
.text(function(d) { return d; });
// Now space the groups out after they have been appended:
var padding = 10;
groups.attr('transform', function(d,i) {
return "translate("+(d3.sum(data, function(e,j) {
if (j < i) { return groups[0][j].getBBox().width; } else return 0; }) + padding * i) + ",0)";
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have a line chart (or, more properly a connected scatterplot) where I plot pies/donuts around the points. So, there is a data set that specifies the date and mood for plotting the points along with two other parameters, pos and neg, providing the values to go into the pie chart. The overall data that describes the points is called plotPoints.
That all works great, but what I still would like to do is to set the radius of the pie to be a function of the sum of pos + neg.
When I plot out the points, I can access all of the data with a function(d). In each pie, however, function(d) returns the data about the slices, one at a time. d contains the data for the current slice, not the overall data. For each pie, the first time arc is called it has the frequency for the first pie slice and the second time it is called it has the frequency for the second pie slice.
How do I refer to the current plotPoints properties when drawing the arc so that I can change the radius of the pie/donut to represent the sum of plotPoints[i].pos + plotPoints[i].neg?
The relevant code looks like this:
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(8);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var p = moodChart.selectAll(".pieContainer")
.data(plotPoints).enter()
.append("g")
.attr("class","pieContainer")
.attr("transform", function(d,i) {return "translate(" + (x(d.date)) + "," + (y(d.mood)) + ")"});
p.append("title")
.text(function(d) { return shortDateFormat(d.date) +", " + d.mood.toFixed(2) });
var g = p.selectAll(".arc")
.data(function (d) {return pie([d.neg, d.pos])})
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
It's tough to answer this authoritatively without a bit more code/data to look at but in this situation I usually stash the needed variables in my data-bindings so they are available later:
var g = p.selectAll(".arc")
.data(function (d) {
var total = d.neg + d.pos,
pie_data = pie([d.neg, d.pos]),
point_arc = d3.svg.arc()
.outerRadius((total * radius) - 10) //<-- set radius based on total
.innerRadius((total * radius) - 8);
pie_data.forEach(function(d1){
d1.data.arc = point_arc; //<-- stash the arc for this point in our data bindings
});
return pie_data;
});
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", function(d){
return d.data.arc
})
.style("fill", function(d,i) { return i==0 ? "brown" : "green"; });
I have a table with filtered data that's working properly and now I'm trying to make a corresponding barchart. The barchart consists of a group for each bar, with two text elements and a rect inside of it. The exit selection successfully removes the g element but the internal rect and text somehow ends up in another g.
function updateSvg(data) {
parameters.svg = d3.select(".svg")
.attr("height", data.length * parameters.barHeight)
var life_expectancy = d3.extent(data.map(getter('life_expectancy')));
var min = life_expectancy[0];
var max = life_expectancy[1];
var x = d3.scale.linear()
.domain([0, max])
.range([0, parameters.svgWidth])
// Data join.
var groups = parameters.svg.selectAll("g")
.data(data)
// Enter.
var groupsEnter = groups.enter().append("g").attr("transform", function(d, i) { return "translate(0," + i * parameters.barHeight + ")"; })
// Update.
var bars = groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
var labels = groups.append("text")
.attr("x", 20)
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
var values = groups.append("text")
.attr("x", function(d) { return x(d.life_expectancy) - 50; })
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.life_expectancy})
// Exit.
groups.exit().remove()
}
Here's what I have working so far: http://chrisdaly.github.io/D3/World%20Countries%20Rank/table.html. If you untick all the continents except Oceania for example and inspect the bars, it shows a tonne of different rects etc hidden underneath the correct one. Any guidance is appreciated!
The problem is here
groups.exit().remove()
On slider motion the values with in the country array will change but none of the g group DOM will get removed because the array still has the same number of array elements. So on that g group you go on appending rect and text.
groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
Now when you tick off Americas the g tag for USA will go which is what exit function does. Reason: your array is filtered has no record for USA.
But the g for Asia countries and others you append the text and rect again thus it keeps growing.
Best way out is when you update do this to remove all rect and text:
groups.selectAll("text").remove();
groups.selectAll("rect").remove();
In this code, http://enjalot.com/inlet/4124664/
of which the main part is:
function render(data) {
var nodz = svg.selectAll(".node")
.data(data);
nodz.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "id"+d[0]+d[1]; })
.attr("x",0)
.attr("y", 0)
.attr("transform", function(d) {
return "translate(" + x(d[0]) + "," + y(d[1]) + ")";
})
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("stroke", "black")
.text(function(d) {return d[2]; });
// update
nodz.selectAll("text")
.text(function(d) {
return d[2]; });
// another go
d3.selectAll("text")
.text(function(d) {
return d[2]; });
}
// data in form [x pos, y pos, value to display]
var nodes = [[0,0,0],[1,1,0],[1, -1,0],[2,0,0], [2,2,0]];
render(nodes);
nodes2 = [[0,0,1],[1,1,1],[1, -1,1],[2,0,1], [2,2,1], [2, -2,1]];
render(nodes2);
I call the code to draw some nodes twice.
I expect it to draw five nodes with a value of zero in the first pass,
Then I add another item to the list and update all the values to 1 so expect to see all the values change to 1 and a new node appear. Instead, I'm only seeing the last one being set to 1. I've tried adding a unique id to bind the node to the data but this isn't working. Also tried reselecting to see if the data is now bound. In all the tutorials I've been through, just calling the selectAll().data() part updates the data, what am I missing?
The second optional argument to .data() is a function that tells d3 how to match elements. That's where you need to compare your IDs, see the documentation. That said, it should work without IDs in your case as it matches by index by default.
The problem with updating the text is that after calling .selectAll() you need to call .data() again to let d3 know what you want to match to that selection (i.e. that the new data should be bound to the old data).