D3 - Pie Chart & Force Directed Labels - javascript

I'm looking to create a pie chart with floating labels using D3. I'm new to D3 and I'm not even sure this is possible? Can you use the labels of one graph in another somehow? If you can, can you point me to an example?
Shorter Explanation:
I want labels from:
http://bl.ocks.org/1691430
...to be on a pie chart.
Here's the code I was running below:
Or in a JSBIN: http://jsbin.com/awilak/1/edit
If I understand his code correctly, this is the section that adds the labels. I don't understand what the labelForce.update does. From there, I don't care about transition, so that line isn't needed. Then the rest is just drawing the circles and adds a link / line? If someone could integrate that would be amazing but if you can help me understand what's going on and what I'm missing I'd be more than grateful.
// Now for the labels
// This is the only function call needed, the rest is just drawing the labels
anchors.call(labelForce.update)
labels = svg.selectAll(".labels")
.data(data, function(d,i) {return i;})
labels.exit()
.attr("class","exit")
.transition()
.delay(0)
.duration(500)
.style("opacity",0)
.remove();
// Draw the labelbox, caption and the link
newLabels = labels.enter().append("g").attr("class","labels")
newLabelBox = newLabels.append("g").attr("class","labelbox")
newLabelBox.append("circle").attr("r",11)
newLabelBox.append("text").attr("class","labeltext").attr("y",6)
newLabels.append("line").attr("class","link")
labelBox = svg.selectAll(".labels").selectAll(".labelbox")
links = svg.selectAll(".link")
labelBox.selectAll("text").text(function(d) { return d.num})
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Testing Pie Chart</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.layout.js?2.1.3"></script>
<style type="text/css">
.slice text {
font-size: 16pt;
font-family: Arial;
}
</style>
</head>
<body>
<button id="button"> Test </button>
<br>
<form id="controls">
<div>
<h2>Y axis</h2>
<ul id="y-axis">
<li><label><input checked="checked" type="radio" name="y-axis" value="Component">Component</label></li>
<li><label><input type="radio" name="y-axis" value="Browser">Browser</label></li>
<li><label><input type="radio" name="y-axis" value="Version">Version</label></li>
</ul>
</div>
</form>
<script type="text/javascript">
// return a list of types which are currently selected
function plottableTypes () {
var types = [].map.call (document.querySelectorAll ("#coaster-types input:checked"), function (checkbox) { return checkbox.value;} );
return types;
}
var w = 600, //width
h = 600, //height
r = 100,
r2 = 200, //radius
axis = getAxis (), //axes
color = d3.scale.category20c(); //builtin range of colors
data = [
{"Browser":"Internet Explorer ","Version":"8.0","Toatl":2000,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"9.0 ","Toatl":1852,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"6.0 ","Toatl":1754,"Component":"6077447412293130422"},
{"Browser":"Firefox ","Version":"16.0 ","Toatl":1020,"Component":"6077447412293130422"},
{"Browser":"Chrome ","Version":"23.0 ","Toatl":972,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"7.0 ","Toatl":700,"Component":"6077447412293130422"},
{"Browser":"Mobile Safari ","Version":"6.0 ","Toatl":632,"Component":"6077447412293130422"},
{"Browser":"BOT ","Version":"BOT ","Toatl":356,"Component":"6077447412293130422"},
{"Browser":"Firefox ","Version":"8.0 ","Toatl":196,"Component":"6077447412293130422"},
{"Browser":"Mobile Safari ","Version":"5.1 ","Toatl":184,"Component":"6077447412293130422"}
];
var vis = d3.select("body")
.append("svg:svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", h)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + r2 + "," + r2 + ")") //move the center of the pie chart from 0, 0 to radius, radius
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(r);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) { return d.Toatl; }); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } ) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
arcs.append("svg:text") //add a label to each slice
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.innerRadius = r2;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")"; //this gives us a pair of coordinates like [50, 50]
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
d3.select('#button').on('click', reColor);
var arcOver = d3.svg.arc()
.outerRadius(r + 30)
.innerRadius(0);
var arc = d3.svg.arc()
.outerRadius(r)
.innerRadius(0);
var arcs = vis.selectAll("g.slice")
.attr("class", "slice")
.on("mouseover", function(d) {
getAxis();
d3.select(this)
.select("path")
.transition()
.duration(500)
.attr("d", arcOver);
d3.select(this).select("text")
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
})
.on("mouseout", function(d) {
getAxis();
d3.select(this)
.select("path")
.transition()
.duration(500)
.attr("d", arc);
d3.select(this)
.select("text")
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
});
function reColor(){
var slices = d3.select('body').selectAll('path');
slices.transition()
.duration(2000)
.attr("fill", function(d, i) { return color(i+2); } );
slices.transition()
.delay(2000)
.duration(2000)
.attr("fill", function(d, i) { return color(i+10); } )
}
function makeData(){
}
// return an object containing the currently selected axis choices
function getAxis () {
var y = document.querySelector("#y-axis input:checked").value;
return {
yAxis: y,
};
}
function update() {
axis = getAxis()
arcs.selectAll("text") //add a label to each slice
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
}
document.getElementById("controls").addEventListener ("click", update, false);
document.getElementById("controls").addEventListener ("keyup", update, false);
</script>
</body>
</html>

As others mentioned in the comments to your introduction-post it's possible to achieve a solution like you described it and it's possible using your code plus parts of the "moving-labels"-example. If I understand you correctly, you want to achieve non-overlapping labels using the force-layout, which is a pretty nice idea that I didn't stumble upon yet.
The code-part you pasted from the example just draws the labels and the lines as you already explained correctly. The next step is to rearrange the labels in a force-like layout around your pie chart.
The part that rearranges the labels (and links) in the example is the following:
function redrawLabels() {
labelBox
.attr("transform",function(d) { return "translate("+d.labelPos.x+" "+d.labelPos.y+")"})
links
.attr("x1",function(d) { return d.anchorPos.x})
.attr("y1",function(d) { return d.anchorPos.y})
.attr("x2",function(d) { return d.labelPos.x})
.attr("y2",function(d) { return d.labelPos.y})
}
// Initialize the label-forces
labelForce = d3.force_labels()
.linkDistance(0.0)
.gravity(0)
.nodes([]).links([])
.charge(-60)
.on("tick",redrawLabels)
The function is the one that changes the positions of the labels and lines. The force is calculated by D3 and started with the d3.force_labels().... As you can see, the function is assigned as an event-handler for the tick-event. In other words: After every step of calculating the force, D3 calls the 'drawer' for every label and updates the positions.
Unfortunately I'm not very familiar with the force_labels() method of D3, but I would assume it works pretty much like the regular force().
An anchor, in your case, is placed somewhere in each pie-piece for each label. The more centered within each pie-piece (NOT the pie itself), the better. Unfortunately you have to calculate this anchor-position somehow (sin and cos stuff) and set the line-ends to this fixed position within redrawLabels().
After you've done this you will see the first result. You may have to play around with gravity, linkDistance etc values of the force to achieve good results. (That's what the silders in the example do.)
See d3 docs for more info: https://github.com/mbostock/d3/wiki/Force-Layout
Then you will maybe stumble upon the problem that the labels are ordered around the pie without overlapping but in some strange order. You could solve this by initially placing the labels in correct order on a larger circle around your pie instead of positioning them randomly around the panel, which is the cause for the problem. This way you will experience way less jitter and misplacements.
The idea also is described in another blocks example: http://bl.ocks.org/mbostock/7881887
In this example, the nodes are initially placed on a virtual circle. The positioning is calculated by the following functions:
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
They represent a circle with a radius of 200, place in the center of the drawing-panel. The circle is divided into m equally large pieces. i/m just calculates the 'piece-positions' where i ranges from 0 to m-1.
Hope I could help!

Yes, you can definitely combine force-labels with a pie chart! There is nothing particularly special about the pie chart labels you started with, they're just text elements that can be positioned like anything else using transform or x/y. It looks like you were initially positioning these labels according to the centroids of the arcs they were labelling, but you can just as easily use another criteria (like the output of a force layout).
D3's force layout calculates positions for things based on a set of constraints about what is fixed, what is movable, and which are connected to which. The labelForce.update method from Mike's bl.ocks example is being used to inform the force layout about how many objects need to be positioned, and where the fixed "anchor" points are. It then saves the computed positions for the labels into the diagram's data model, and they are used later on in the redrawLabels function.

You need to create two arcs. One for the pie chart drawing, and one which is large for the labels to sit on.
// first arc used for drawing the pie chart
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
// label attached to first arc
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
// second arc for labels
var arc2 = d3.svg.arc()
.outerRadius(radius + 20)
.innerRadius(radius + 20);
// label attached to second arc
g.append("text")
.attr("transform", function(d) { return "translate(" + arc2.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });

Related

d3 chord: center text on circle

I use the d3 chord diagram example of Andrew and want to center all text labels within the curved slice. I tried many things but was never able to center the texts. Do you know what wizzard trick there is needed?
var width = 720,
height = 720,
outerRadius = Math.min(width, height) / 2 - 10,
innerRadius = outerRadius - 24;
var formatPercent = d3.format(".1%");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var layout = d3.layout.chord()
.padding(.04)
.sortSubgroups(d3.descending)
.sortChords(d3.ascending);
var path = d3.svg.chord()
.radius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("circle")
.attr("r", outerRadius);
d3.csv("ex_csv.csv", function(cities) {
d3.json("ex_json.json", function(matrix) {
// Compute the chord layout.
layout.matrix(matrix);
// Add a group per neighborhood.
var group = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group")
.on("mouseover", mouseover);
// Add the group arc.
var groupPath = group.append("path")
.attr("id", function(d, i) { return "group" + i; })
.attr("d", arc)
.style("fill", function(d, i) { return cities[i].color; });
// Add a text label.
var groupText = group.append("text")
.attr("x", 6)
.attr("dy", 15);
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; });
// Remove the labels that don't fit. :(
groupText.filter(function(d, i) { return groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength(); })
.remove();
// Add the chords.
var chord = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return cities[d.source.index].color; })
.attr("d", path);
}
});
});
</script>
As an aside, I would suggest looking to upgrade to v4, documentation for v2 is nearly non-existent and is very hard to help with.
You can set both the text-anchor and the startOffset property to achieve what you are looking for.
First, you'll want to set text-anchor to middle as it is easier to specify the middle point than to find the middle point and work back to find where the text should start.
Second you'll need to set a startOffset. Note that if you use 50%, the text will not appear where you want, as the total length of the text path is all sides of the closed loop (chord anchor) you are appending to. Setting it to 25 % would work if you did not have a different outer and inner radius. But, as you have an outer radius that is 24 pixels greater than the inner radius you can try something like this to calculate the number of pixels you need to offset the center of the text:
groupText.append("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i; })
.text(function(d, i) { return cities[i].name; })
.attr("startOffset",function(d,i) { return (groupPath[0][i].getTotalLength() - 48) / 4 })
.style("text-anchor","middle");
I subtract 48 because the sides of the anchor are 24 pixels each (the difference in the radii). I divide by four because the path doubles back on itself. If it was a general line I would just divide by two.
This approach is a little simplistic as the outer circumference is not the same as the inner circumference of each chord anchor, so I am off by a little bit, but it should be workable.
For labels that are on the cusp of being displayed, this will be awkward: the inner radius is shorter, so the formula for deteriming if a string is short enough to be displayed may be wrong - which may lead to some characters climbing up the side of the anchor (your example also 16 pixels as the difference in radii to calculate if text is too long, rather than 24).
This is the end result:
Here is a demonstration.

D3.js animation for donut labels

I already have a donut chart in d3.js.
The animation for the labels is some thing like this right now :
starting point of labels : the labels are all in the center of the donut
ending point : they end up going behind the arcs.
Now below is what i am trying to achieve....
what i want to achieve :
I want to change the starting point of labels.
I want the labels to appear from behind the arcs of the donuts instead of
the center.
The ending point is fine like it is now.
Is there a way i can achieve this ?
i.e change the emission point of the labels instead from the center they should
appear from behind the arcs.
Here is the code that i am trying to modify :
var text=svg.selectAll('text')
.data(pie(dataset.data))
.enter()
.append("text")
.transition()
.duration(1000)
.attr("transform", function (d) {
console.log(d);
console.log(arc.centroid(d));
var c = arc.centroid(d),
x = c[0],
y = c[1],
h = Math.sqrt(x*x + y*y);
return "translate(" + (x/h * labelr) + ',' +
(y/h * labelr) + ")";
})
.attr("dy", ".4em")
.attr("text-anchor", "middle")
.text(function(d){
return d.data +"%";
})
.style({
fill:'#000',
'font-size':'11px'
});
Below is the link to the fiddle :
https://jsfiddle.net/ahc4wdjk/
In D3 transition selections, the starting value is the current atribute value. So, we start creating the texts behind the arcs:
.attr("x", function(d){
return arc.centroid(d)[0]
})
.attr("y", function(d){
return arc.centroid(d)[1]
})
I had a problem here, because your arcs don't show up at the same time. The solution was giving the texts an initial .attr("opacity", 0) and waiting a little bit (using delay(1000)).
Then, I used your code for the final position, but changing labelr to labelr = radius - 160;.
Here is the fiddle: https://jsfiddle.net/gerardofurtado/zrahm2h4/1/

D3 -- Arcs for chord diagram not being displayed

I trying to understand how the D3 chord diagram works. My first step is to display the arcs for the diagram with following script. But for some reason, the arcs are not showing up.
See web page HERE
Can some one tell me what I am missing?
<body>
<script>
// Chart dimensions.
var width = 960,
height = 750,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
//Create SVG element with chart dementions
var svg = d3. select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append ("g")
.attr("transform", "translate (" + width / 2 + "," + height / 2 + ")");
//------------Reformat Data ------------------------------------------
var matrix = []; // <- here is the data
d3.tsv('picData.tsv', function(err, data)
{
//console.log(data);
pictures = d3.keys(data[0]).slice(1);
//console.log(pictures);
data.forEach(function(row)
{
var mrow = [];
pictures.forEach(function(c)
{
mrow.push(Number(row[c]));
});
matrix.push(mrow);
//console.log(mrow);
});
//console.log('1st row: ' + matrix[0]);
//console.log(matrix);
});
//---------------- Define diagram layout ----------------------------
var chord = d3.layout.chord() //<-- produce a chord diagram from a matrix of input data
.matrix(matrix) //<-- data in matrix form
.padding(0.05)
.sortSubgroups(d3.descending);
var fill = d3.scale.category20(); //<-- https://github.com/mbostock/d3/wiki/API-Reference#d3scale-scales
//console.log(fill);
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
//console.log(g);
// create arcs
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//console.log(arc);
g.append("path")
.attr("d", arc)
.style("fill", function(d) { console.log(d.index); return fill(d.index);})
.style("stroke", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return"group-" + d.index });;
g.append("svg:text")
.attr("x", 6)
.attr("class", "picture")
.attr("dy", 15)
.append("svg:textPath")
.attr("xlink:href", function(d) { return "#group-" + d.index; })
.text(function(d) { return pictures[d.index]; });
//console.log(g);
</script>
</body>
Your problem stems from the fact that d3.tsv is asynchronous:
Issues an HTTP GET request for the comma-separated values (CSV) file at the specified url... The request is processed asynchronously.
As a result, all of your code under "Define diagram layout" is being executed before any data is loaded. Otherwise, your code works fine (See image below). So just move all your code into your d3.tsv(...) call and you'll be all set.
Your script is running without errors, but no elements are being created from your data join. That's usually a sign that you are passing in a zero-length data array.
In fact, you're not passing in an array at all; you're passing a function object. When d3 looks up the array length of that object, it returns undefined, which gets coerced to the number zero, and so no groups and no chords are created.
Relevant part of your code:
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
To actually get the array of chord group data objects, you need to call chord.groups(). Without the () at the end, chord.groups is just the name of the function as an object.
Edited to add:
Ooops, I hadn't even noticed that your drawing code wasn't included inside your d3.tsv callback function. Fix that, as described in mdml's answer, then fix the above.

grouping data in d3 pie chart

Im creating a small d3 (http://d3js.org/) pie chart where some pieces of the pie are separated
and translated away from the center of the pie on hover.
So far i managed separating the different parts of the pie and translating them away from the center of the circle, but they are translated separatedly. My goal is to group the pieces and translate them away together.
Im very new to d3js.
Basically what I want to achieve is grouping some of the elements and translating them away from the center of origin grouped.
Right now the pieces are translated separately and Im guessing that I can fix the problem by adding a parent and translating that parent away from the center.
So basically my question is :
How can i group pieces of data (fx the ones labeled food) and translate that group away from the center of origin?
The project can be found below and the content of the csv file in the bottom. Copy paste the code into a html-document and copy paste the csv data into a file named 'data.csv' located in the same folder.
Thanks
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.arc path {
stroke: #fff;
}
</style>
<body>
<!--https://github.com/mhemesath/r2d3/-->
<!--[if lte IE 8]><script src="r2d3.min.js"></script><![endif]-->
<!--[if gte IE 9]><!-->
<script src="http://d3js.org/d3.v3.min.js"></script>
<!--<![endif]-->
<script>
// dimension
var width = 960,
height = 500,
radius = Math.min(width, height) / 2;
// colors
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
//append svg to body
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
d3.csv("data.csv", function(error, data) {
var g = svg.selectAll(".arcs")
.data(pie(data))
.enter().append("g")
.attr("class", function (d, i) {
return data[i].class; // i is 0-based.
});
// Select all classes named 'pizza' and translate away from center of circle
svg.selectAll('.food')
.attr("transform", function(d) { //set the origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.innerRadius = 0;
d.outerRadius = 0;
x = arc.centroid(d)[0];
y = arc.centroid(d)[1];
return "translate(" + x/4 +','+ y/4 + ")"; //this gives us a pair of coordinates like [50, 50]
});
// color fill
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.age); });
// append text
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
});
</script>
//content of csv file :
age,population,class
<5,2704659,food
5-13,4499890,food
14-17,2159981,food
18-24,3853788,icecream
25-44,14106543,icecream
45-64,8819342,pizza
≥65,612463,pizza
I can think of two ways to do this.
1) Create an outer pie chart with a single pie segment for each class, then create addition pie charts inside each outer pie segment. The inner pie charts need to specify a start/end angle for their piechart (which matches the outer pie segment for that class). Here's a example on Plunker: http://plnkr.co/edit/cblP4d?p=preview
2) Instead of translating each datapoint individually, create an array of translations for each class. Each class's translation uses the centroid for that class. When you translate each arc, use the translation array (referenced by class).

D3 choropleth map with legend

I have a choropleth map of the united states showing total population. I would like to add a legend to the map showing the quantile range values.I’ve seen other similar questions about this topic but can’t seem to get it to work for my specific case. I know I need to include the color range or color domain but just not sure if this is the correct way. As of right now just one feature shows up in the legend, could it be that all the legend features are stacked on top of each other. How can I know for sure and how can I fix this.
//Define default colorbrewer scheme
var colorSchemeSelect = "Greens";
var colorScheme = colorbrewer[colorSchemeSelect];
//define default number of quantiles
var quantiles = 5;
//Define quantile scale to sort data values into buckets of color
var color = d3.scale.quantile()
.range(colorScheme[quantiles]);
d3.csv(data, function (data) {
color.domain([
d3.min(data, function (d) {
return d.value;
}),
d3.max(data, function (d
return d.value
})
]);
//legend
var legend = svg.selectAll('rect')
.data(color.domain().reverse())
.enter()
.append('rect')
.attr("x", width - 780)
.attr("y", function(d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
The legend code that you're using would work perfectly well if you had an ordinal scale, where the domain is made up of discrete values that correlate to the range of colours on a one-to-one basis. But you're using a quantile scale, and so need a different approach.
For a d3 quantile scale, the domain is the list of all possible input values, and the range is a list of discrete output values. The domain list is sorted in ascending order and then divided into equal-sized groups, which are assigned to each output value from the range. The number of groups is determined by the number of output values.
With that in mind, in order to get one legend entry for each colour, you're going to need to use your colour scale's range, not the domain, as the data for your legend. Then you can use the quantileScale.invertExtent() method to find the minimum and maximum input values that are getting drawn with that colour.
Sample code, making each legend entry a <g> containing both the coloured rectangle and a text label showing the corresponding values.
var legend = svg.selectAll('g.legendEntry')
.data(color.range().reverse())
.enter()
.append('g').attr('class', 'legendEntry');
legend
.append('rect')
.attr("x", width - 780)
.attr("y", function(d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("stroke", "black")
.style("stroke-width", 1)
.style("fill", function(d){return d;});
//the data objects are the fill colors
legend
.append('text')
.attr("x", width - 765) //leave 5 pixel space after the <rect>
.attr("y", function(d, i) {
return i * 20;
})
.attr("dy", "0.8em") //place text one line *below* the x,y point
.text(function(d,i) {
var extent = color.invertExtent(d);
//extent will be a two-element array, format it however you want:
var format = d3.format("0.2f");
return format(+extent[0]) + " - " + format(+extent[1]);
});

Categories

Resources