How to position the legend in a d3 chart - javascript

How do I position the legend above and out of the chart?
I am working in this d3 example Grouped Bar Chart
Here is my PLUNKER but the legend can overlap the graph. Ideally I would like the legend above and out of the chart.
This is my code that I have to change. I don't understand why the 0 refers to the current position.
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
I can move the legend as follows: PLUNKER
.attr("transform", function(d, i) { return "translate(" + "-500," + i * 20 + ")"; }); which moves imore to the center.
I can then have the legend read from left to right as follows:
.attr("transform", function(d, i) { return "translate(" + (-700+i*100) + "," + 0 + ")"; }); I would be great if I could move this above and outside the chart as it still overlaps some of the graph.
EDIT1 PLUNKER
tks to an answer belwo. This is my attempt, which is above the chart as I would expect, but I would like the different series in the legend to appear closer together (there is too much white space). So how do I have the coloured rect and then the text beside it, but without the whitespace?
## the below is close but I am just guessing
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (-width) + "," + (-margin.top) + ")")
.attr('class','legendHolder')
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
legend.append("rect")
.attr("x", function(d,i){return (width +(150*i))})
.attr("width", 36)
.attr("height", 18)
//.style("text-anchor", "end")
.style("fill", color);
legend.append("text")
//.attr("x", width - 24)
.attr("x", function(d,i){return (width +(140*i))})
.attr("y", 9)
.attr("dy", ".35em")
//.style("text-anchor", "end")
.text(function(d) { return d; });
EDIT2 PLUNKER
This is the best I can do, but I fell I am jsut guessing, maybe I will revisit but in the meant time if anyone can beautifully explain it to me that would be greatly appreciated
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (-width) + "," + (-margin.top) + ")")
.attr('class','legendHolder')
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr('transform', function(d, i) { return "translate(" + -40*i + "," + 0 + ")"; })
.attr("width", 36)
legend.append("rect")
.attr("x", function(d,i){return (width +(150*i))})
.attr("width", 18)
.attr("height", 18)
//.style("text-anchor", "end") //"startOffset"="100%
//.style("startOffset","100%") //"startOffset"="100%
.style("fill", color);
legend.append("text")
//.attr("x", width - 24)
.attr("x", function(d,i){return (width +(150*i)+20)})
.attr("y", 9)
.attr("dy", ".35em")
//.style("text-anchor", "end")
.text(function(d) { return d; });

If you want the legend to be located outside of the graph, you just need to increase the size of the margin where you want it to be placed and translate it into position.
Right now you are positioning the individual parts of your legend based on the size of the <svg>. You can simplify this by creating a <g> that contains all of your legend elements and translating that to its desired position in the graph.
You'll need to play around with the values to get exactly what you want, but below are the values that would allow you to place the legend in the right margin.
var margin = {top: 20, right: 100, bottom: 30, left: 40};
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (margin.left + width) + ",0)")
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 0)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });

The legend in the example appears on the right hand side, despite a transform of zero because the elements in the group have an x attribute of nearly the width of the frame (minus a small offset), pushing them to the right:
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
So an x transform of -500, about half your width, pulls it to the middle, as noted. Using a smaller x attribute for the legend elements might help make it clearer for setting up your legend (this is seen in the other answer), though as your comment notes, it isn't too hard to make it work as it is (just more confusing than needed).

Related

How to format numbers in legend after it has been safely used to define the legend

I need to format the numbers tied to the legend.
This is the code that defines the legend:
MapChart.prototype.addLegend = function() {
let vis = this;
let formatComma = d3.format(",");
let legendData = vis.color.ticks(6).slice(0);
let formattedLegendData = legendData.map(each => formatComma(each));
vis.legend = vis.svg
.selectAll(".legend")
.data(legendData)
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(" + 20 + "," + (20 + i * 20) + ")";
});
vis.legend
.append("rect")
.attr("width", 20)
.attr("height", 20)
.style("fill", vis.color);
vis.legend
.append("text")
.attr("x", 26)
.attr("y", 10)
.attr("dy", ".35em")
.text(String);
vis.svg
.append("text")
.attr("class", "label")
.attr("x", vis.dimensions.width + 20)
.attr("y", 10)
.attr("dy", ".35em");
};
formattedLegendData gives me the formatting I want but legendData gives me the correct colors. With formattedLegendData, all the legend blocks show up as black.
How can I have both, i.e. formatted legend numbers and the correct legend colors.
console.log(legendData); // [0, 50000, 100000, 150000, 200000, 250000, 300000, 350000]
console.log(formattedLegendData); //["0", "50,000", "100,000", "150,000", "200,000", "250,000", "300,000", "350,000"]
If I understand correctly,
You can pass legendData to .data() because your the values in legendData are within your scale's domain: they are numbers. Because of this, the scale (viz.color) returns valid colors as fills. This is why you get the correct colors here.
When you pass formattedLegendData to .data(), when you use .style("fill", vis.color); you pass string values to the scale. Since "10,000" is not likely in your domain, the scale doesn't know how to interpolate a value for the input and you get a black square.
Instead, pass the tick values directly to .data(), and then apply the formatting when adding the text with something like:
.text(formatter)
As below:
let color = d3.scaleSequential(d3.interpolateRdBu)
.domain([0,5000])
let formatComma = d3.format(",");
let legendData = color.ticks(6).slice(0);
let formattedLegendData = legendData.map(each => formatComma(each));
let svg = d3.select("body").append("svg")
let legend = svg
.selectAll(".legend")
.data(legendData)
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(" + 20 + "," + (20 + i * 20) + ")";
});
legend
.append("rect")
.attr("width", 20)
.attr("height", 20)
.style("fill", color);
legend
.append("text")
.attr("class", "label")
.attr("x", 30)
.attr("y", 10)
.attr("dy", ".35em")
.text(formatComma);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

How to add a d3js graph in slideshow instead of body?

I am very new to d3js, css, html and trying to practice different examples of d3js. I am trying to add a d3js graph in a slideshow instead of adding it to a body of webpage. I am kind of stuck on how to do this. How do i place a graph in slideshow ? For your reference below is my attempted code-
<div class="mySlides fade">
<div class="numbertext">2 / 3</div>
Something goes here in slide 2
</div>
</div>
<div class="mySlides fade">
<div class="numbertext">3 / 4</div>
<h1 style="font-size:400%;"><u>Data</u></h1>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
/*
* value accessor - returns the value to encode for a given data object.
* scale - maps value to a visual display encoding, such as a pixel position.
* map function - maps from data value to display value
* axis - sets up axis
*/
// setup x
var xValue = function(d) { return d.Contributions;}, // data -> value
xScale = d3.scale.linear().range([0, width]), // value -> display
xMap = function(d) { return xScale(xValue(d));}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
// setup y
var yValue = function(d) { return d.Deaths;}, // data -> value
yScale = d3.scale.linear().range([height, 0]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return d.State;},
color = d3.scale.category10();
// add the graph canvas to the body of the webpage
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// add the tooltip area to the webpage
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// load data
d3.csv("data.csv", function(error, data) {
// change string (from CSV) into number format
data.forEach(function(d) {
d.Contributions = +d.Contributions;
d.Deaths = +d.Deaths;
// console.log(d);
});
// // don't want dots overlapping axis, so add in buffer to data domain
xScale.domain([d3.min(data, xValue)-1, d3.max(data, xValue)+1]);
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]);
// // x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Conntributions");
// // y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Deaths");
// draw dots
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(cValue(d));})
.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d["State"] + "<br/> (" + xValue(d)
+ ", " + yValue(d) + ")")
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// draw legend
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate("+ (i * 20 + 137) + ", 6)";
});
// draw legend colored rectangles
var boxSize = 17;
legend.append("rect")
.attr("x", 0)
.attr("width", boxSize)
.attr("height", boxSize)
.style("fill", color);
// draw legend text
legend.append("text")
.attr("x", -8.2)
.attr("y", 19)
.attr("dy", ".35em")
.style("text-anchor", "end")
.attr("transform","rotate(-43)")
.text(function(d) { return d;})
});
</script>
</div>
This code gets me a graph in all pages of slideshow which i don't want but instead i would like to add a graph in page 3 of my slideshow.
This is an interesting question: normally, the answer here would be just "select the div you want by ID or any other CSS selector that suits you" (or, since that this has been answered many many times, just a comment and a vote to close). The basis for that answer is that this...
var svg = d3.select("body").append("svg")
... will append the SVG at the end of the body. So far, nothing new or difficult.
But why did I said that this is an interesting question?
Because of the funny way (if you don't mind me saying so) you tried to select the div: you thought that D3 would create the chart inside that div just by putting the respective script inside it.
Of course putting the script inside the container div is not the way of doing this. But just for the sake of curiosity, there is a way of doing what you thought you were doing (again, selecting the element that contains the script): by using document.currentScript, which:
Returns the element whose script is currently being processed.
So, all we need in your situation is:
var container = document.currentScript.parentNode;
var svg = d3.select(container)
.append("svg")
//etc...
Which appends the SVG in the <div> (or any other containing element) that contains the <script>.
Here is a basic demo:
<script src="https://d3js.org/d3.v5.min.js"></script>
<div>
<h3>I am div 1</h3>
</div>
<div>
<h3>I am div 2, I have a chart</h3>
<script>
var container = document.currentScript.parentNode;
var svg = d3.select(container)
.append("svg");
var data = d3.range(10).map(d => Math.random() * 150);
var scale = d3.scaleBand()
.domain(data)
.range([0, 300])
.padding(0.4);
var bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.style("fill", "steelblue")
.attr("x", d => scale(d))
.attr("width", scale.bandwidth())
.attr("height", d => 150 - d)
.attr("y", Number)
</script>
</div>
<div>
<h3>I am div 3</h3>
</div>
<div>
<h3>I am div 4</h3>
</div>
Note that document.CurrentScript doesn't work on IE (if you care for IE, just give the div an ID and select it).

Adding labels to both ends of <rect> in a bar chart

I'm using D3 to present some data as a horizontal bar chart. Values will typically range between -10 and +10 on 8 different scales. I have the bars rendering as I want, but I can't work out how to add lables for each of the extreems of the axes.
so far I have:
but I want to achieve something like:
In other words a label for each extreme of each scale.
I have found lots of examples that add data labels to the bars them selves (e.g. the value), but I want to some how force the array of strings to be rendered at the extremes of the container.
At the moment, I am rendering the data from an array, and I have the labels stored in 2 other arrays e.g.
var data = [10, 5, -5, -10, 2, -2, 8, -8];
var leftLabels = ["label 1","label 2", ...];
var rightLabels = ["label 1", "label 2", ...];
Any ideas or links to examples most welcome.
I am not an expert in d3.js, but I think this can be easily done. There are different ways to go about it. I have created a pen for your use case.
I will paste the important part of the code below. In your chart, you will have to certainly make some adjustments to suit your needs. Feel free to play around with the values until you feel they are stable.
// Your array containing labels for left and right values
var leftSideData = ["left1", "left2", "left3", "left4", "left5", "left6", "left7", "left8"];
var rightSideData = ["right1", "right2", "right3", "right4", "right5", "right6", "right7", "right8"];
var left = svg.selectAll(".leftData")
.data(leftSideData)
.enter().append("g")
.attr("class", "leftVal")
.attr("transform", function(d, i) {
return "translate(0," + i * 57 + ")";
});
left.append("text")
.attr("x", 0)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
var right = svg.selectAll(".rightData")
.data(rightSideData)
.enter().append("g")
.attr("class", "rightVal")
.attr("transform", function(d, i) {
return "translate(0," + i * 57 + ")";
});
right.append("text")
.attr("x", width + 30)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
I won't say this is perfect, but I hope you get an idea about how to approach it. All the best!!
It's funny, just by asking the q on SE I find it helps me reformulate the problem.. and then some time later a new try yields a result. Anyone else find that?
I managed to make it work by changing the way the SVG was created. So I now have the following structure:
<SVG>
><g> (one for each bar)
>><text>
>><rect>
>><text>
><other stuff like axies>
It turns out that <text> elements cannot be added to <rect> elements (well they can, be added but they won't render).
the code is:
var data = [10,2,4,-10,...etc...];
var leftLabels = ["left 1","left 1", ...etc...];
var rightLabels = ["right 1","right 2", ...etc...];
//chart dimentions
var margin = { top: 20, right: 30, bottom: 40, left: 30 },
width = 600 - margin.left - margin.right,
barHeight = 30,
height = barHeight * data.length;
//chart bar scaling
var x = d3.scale.linear()
.range([100, width-100]);
var y = d3.scale.ordinal()
.rangeRoundBands([0, height], 0.1);
var chart = d3.select(".chartsvg")
.attr("width", width + margin.left + margin.right)
.attr("height", barHeight * data.length + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain([d3.min(data), d3.max(data)]);
//append a g for each data item
var bar = chart.selectAll(".bar")
.data(data)
.enter()
.append("g");
//in each bar add a rect for the bar chart bar
bar.append("rect")
.attr("class", function (d) { return "bar--" + (d < 0 ? "negative" : "positive"); })
.attr("x", function (d) { return x(Math.min(0, d)); })
.attr("y", function (d, i) { return i* barHeight; })
.attr("width", function (d) { return Math.abs(x(d) - x(0)); })
.attr("height", barHeight-1);
//append the labels to each g using the label data
bar.append("text")
.data(rightLabels)
.attr("x", width)
.attr("y", function (d, i) { return (i * barHeight)+barHeight/2; })
.attr("dy", ".5em")
.attr("fill","steelblue")
.attr("text-anchor","end")
.text(function (d) { return d; });
bar.append("text")
.data(leftLabels)
.attr("x", 0)
.attr("y", function (d, i) { return (i * barHeight) + barHeight / 2; })
.attr("dy", ".5em")
.attr("fill","darkorange")
.attr("text-anchor", "start")
.text(function (d) { return d; });
//then append axis etc...
Formatting: something else to note. It turns out that to color the text in the label you need to use "stroke" and "fill" attributes. These are broadly equiv to the HTML "color" attribute on text.

D3 Set labels on Legend

I need help setting the labels on a D3 stacked bar chart. I'm not sure how to map the color in the legend with a reference to the name property in the data object.
I have a JSFiddle here:
http://jsfiddle.net/Lhs3e7xk/1/
The code in particular I need help with is the legend function:
function updateLegend(dt) {
var legend = svg.selectAll(".legend")
.data(color.domain()) // I tried dt as well.
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
console.log(d)
return color(d.name)
});
}
The output should be the value for the name property in the data set and the color associated with that group.
Fixed MBS [Color01]
Floating MBS [Color02]
CMO [Color03]
Thank you!
Instead of this
.text(function(d, i) {
console.log(d)
return color(d.name)
})
do the text function like this:
.text(function(d, i) {
if(i==0)
return "Fixed MBS"
if(i==1)
return "Floating MBS"
if(i==2)
return "CMO"
});
Working example here
EDIT
For setting legends using data
//your dynamic legend data set
var legends = ["Fixed MBS", "Floating MBS", "CMO"];
function updateLegend(dt) {
var legend = svg.selectAll(".legend")
.data(legends)//pass the lgend data
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i){return color(i)});//color based on index
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
return d;//return the array data
});
}
Working code here

D3.js how to add grid boxes for value ranges

I'm trying to find a way to add grid boxes to my D3.js scatterplot. The boxes should have a certain color depending on in which value range they reside. For example the box that is between x value 0-20 and y value 0-20 should be green.
What I have now:
What I want - Illustration:
I know that I could add a background image for the svg, but that doesn't seem to be a viable solution.
My code so far:
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 1200 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// setup x
var xValue = function(d) { return d.Impact;}, // data -> value
xScale = d3.scale.linear().range([0, width]), // value -> display
xMap = function(d) { return xScale(xValue(d));}, // data -> display
xAxis = d3.svg.axis().scale(xScale).orient("bottom");
// setup y
var yValue = function(d) { return d.Likelihood;}, // data -> value
yScale = d3.scale.linear().range([height, 0]), // value -> display
yMap = function(d) { return yScale(yValue(d));}, // data -> display
yAxis = d3.svg.axis().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return d.Conf;},
color = d3.scale.category20();
// add the graph canvas to the body of the webpage
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right + 400)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// add the tooltip area to the webpage
var tooltip = d3.select("#chart").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// change string (from CSV) into number format
data.forEach(function(d) {
d,Impact = +d.Impact;
d.Likelihood = +d.Likelihood;
});
// don't want dots overlapping axis, so add in buffer to data domain
xScale.domain([d3.min(data, xValue)-1, d3.max(data, xValue)+1]);
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]);
// scales w/o extra padding
xScale.domain([d3.min(data, xValue), d3.max(data, xValue)]);
yScale.domain([d3.min(data, yValue), d3.max(data, yValue)]);
// x-axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.attr("class", "grid")
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Likelihood");
// y-axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.attr("class", "grid")
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Impact");
// draw dots
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5.5)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(d.CategoryMain);})
.on("mouseover", function(d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.CategoryMain + "<br/> " + d.CategorySub1 + "<br/>(" + xValue(d)
+ ", " + yValue(d) + ")")
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 10) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// draw legend
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(160," + (i+7) * 20 + ")"; });
// draw legend colored rectangles
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
// draw legend text
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d;})
})
Any suggestions how this could be done?

Categories

Resources