I'm playing around with the "update" pattern in D3.js. I am just creating a simple bar graph that will update the data when you press the "Change" button.
My problem is that when you press the "Change" button, the first three rendered bars do not get re-rendered. I debugged and saw that the data was properly applied (__data__ was correct) but the re-application failed.
Here is my code and a link to it in CodePen:
var myData = [ 100, 200, 300 ];
d3.select('body').append('button').text("Change").on("click", function() {
myData = [200, 400, 600, 700, 800, 900, 1000];
update(myData);
});
var svg = d3.select('body').append('svg')
.attr("class", "chart")
.attr("y", 30);
var update = function(data) {
var bars = svg.selectAll('g')
.data(data);
var groups = bars.enter()
.append("g")
.attr("transform", function(d,i) {return "translate(0," + i*25 + ")"});
groups
.append("rect")
.attr("height", 25)
.attr("fill", "pink")
.attr("stroke", "white");
groups
.append("text")
.attr("x", 10)
.attr("y", 18)
.attr("fill", "red");
bars.selectAll("rect")
.attr("width", String);
bars.selectAll("text")
.text(String);
};
update(myData);
It works if you change the .selectAll() in your update selection handling to .select():
bars.select("rect")
.attr("width", String);
bars.select("text")
.text(String);
By using selectAll(), you're accessing the data that is bound to the elements that you're selecting (i.e. the rectangles and text elements), which was bound when you appended the elements. This data hasn't been updated though as you've only updated it for the containing g elements. Using .select() instead also binds the new data to the child elements.
The general pattern that you're using is a nested selection and can be a bit confusing to start with and lead to unexpected results.
Related
I am really new to the realm of D3 and based on the book of 'Interactive Data visualization for the web', I managed to create a Bar chart which is mostly based on the code from the following link.
The problem is I don't manage to add a legend to my bar chart based on an object dynamically.
I have tried consulting youtube videos and other stackoverflow questions related to 'adding a legend to a bar-chart', however in my opinion I couldn't find the question concerning how one is able to retrieve keys from an array of objects and use the data to add as an legend to the bar-chart.
For now all my bars also have the same color, see the second code below.
See the code below for the formatting of my object which is embedded in an array.
The name 'key' and 'value' are fixed, while the amount of the objects and their corresponding name and value differ after an click event of the user ( which determines which variables will be included in the object).
The following example is able create a legend, however in this case the formatting of the object is somehow different than in my case and my current knowledge of D3 is limitd, so I have no idea in which ways I have to adapt the code.
2: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
3: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
4: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
5: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
6: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
7: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Based on the code from the book and accounting for other variables, I have currently the following code for visualizing a bar chart, in which the values (see object above) are shown in the bar charts and the color of the bar are all blueish. However there is not yet an legend included in my current code. Therefore I am wondering how one is able to dynamically create a legend based on the 'keys' ( in my case)in the object and represent the corresponding color bound to the bars. I would like to achieve the lowest image which I have drawn a sketch of.
var svg = d3.select("#barchart")
.select("svg")
.remove("svg");
//Width and height
var w = 600;
var h = 250;
var padding=20;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([w - padding,padding ])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) {
return d.value;
})])
.range([padding,h - padding]);
console.log("yscale",yScale);
//Define key function, to be used when binding data
var key = function (d) {
console.log("key", d);
return d.key;
};
// d3.select("svg").remove();
//Create SVG element
var svg = d3.select("#barchart")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("svg", svg);
//Create bars
svg.selectAll("rect")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d.value);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d.value);
})
// .attr("data-legend", function (d) { return d.key })
.attr("fill", function (d) {
return "rgb(0, 0, " + (d.value * 10) + ")";
});
//Create labels
svg.selectAll("text")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("text")
.text(function (d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
If I understood correctly this is what you should need.
Plunker with working code.
First of all I would encourage to use an margin object which will allow better flexibility when dealing with charts
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
};
We want to display the data with an odinal scale from the data and example you provided.
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Taking into account that probably the first element is a sum of sorts of the dataset I think it shouldn't be included in the chart since it is an aggregation of the elements we want to display.
(In case you need to display it as an element you should be able to do it quickly after reviewing the answer)
The element structure in your dataset is the following:
{
key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie",
value: 85
}
The domain of our xScale should be all the key values in our dataset, since the key is a huge string, I created a custom property in each element called label
{
key:
"bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening",
label: "Business Services",
value: 165
}
Lets create our scale with the correct domain and range:
var xScale = d3
.scaleBand()
.domain(dataset.map(d => d.label)) // All our label properties
.rangeRound([0, w - margin.left - margin.right]) // This scale will map our values from [0, width - margin.left - margin.right]
.paddingInner(0.05);
The yScale was almost correct, we just need to change it a little to use our margin object and use the correct range
The range must start from 0, if we used padding as the starting point our values will have an offset, since our values would be mapped from [padding, h - padding]. If we wanted to display a zero the value would be mapped to the padding value, if this is way you want to show the information keep it that way. In this case we will modify the scale.
var yScale = d3
.scaleLinear()
.domain([
0,
d3.max(dataset, function(d) {
return d.value;
})
])
.range([0, h - margin.top - margin.bottom]);
Next we will create a function to get the desired value from our elements
var xKey = function(d) {
return d.label;
};
Add our svg with some visual cues to help visualizing the way the elements are layed out:
var svg = d3
.select("#barchart")
.append("svg")
.style("background", "rgb(243, 243, 243)")
.style("border", "1px dashed #b4b4b4")
.attr("width", w)
.attr("height", h);
We want to use a margin, so lets use a group tag to achieve this, we could individually set the margin in each group/element we desired but I find this way simpler and clearer
var g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
We will need the width and height of the chart with the margins taken into account, lets define them really quick:
const customWidth = w - margin.left - margin.right;
const customHeight = h - margin.top - margin.bottom;
Let us add a rect to show where will our rects will be displayed:
g.append("rect")
.attr("fill", "#e3e3e3")
.attr("width", customWidth)
.attr("height", customHeight);
Lets deal with the rect creation, in your code you had a custom fill function which modified the b value within the RGB color values. In this case since we are dealing with categorical data we will use an array of colors for the rects.
g.append("g")
.attr("class", "rect__container")
.selectAll("rect")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(xKey(d)); // use our key function
})
.attr("y", function(d) {
return customHeight - yScale(d.value); // use our custom size values
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d.value);
})
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use an array of colors and use the index to decide which color to use
});
We have two options to show the labels of the chart:
We can create an x-axis or the desired legends. We will do both since it won't affect the outcome of the chart and either one of them can be removed.
var margin = {
top: 20,
right: 300, // modifiy our margin to have space to display the legends
bottom: 50,
left: 20
};
var legendElement = g
.append("g")
.attr("class", "legend__container")
.attr("transform", `translate(${customWidth}, ${margin.top})`) // set our group position to the end of the chart
.selectAll("g.legend__element")
.data(xScale.domain()) // use the scale domain as data
.enter()
.append("g")
.attr("transform", function(d, i) {
return `translate(${10}, ${i * 30})`; // provide an offset for each element found in the domain
});
legendElement
.append("text")
.attr("x", 30)
.attr("font-size", "14px")
.text(d => d);
legendElement
.append("rect")
.attr("x", 0)
.attr("y", -15)
.attr("width", 20)
.attr("height", 20)
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use the same category color that we previously used in rects
});
Now lets use the axis approach:
// create axis
var x_axis = d3.axisBottom().scale(xScale);
//Append group and insert axis
g.append("g")
.attr("transform", `translate(${0}, ${customHeight})`)
.call(x_axis);
g.append("g")
.attr("transform", `translate(${customWidth / 2}, ${customHeight + 40})`)
.append("text")
.text("Activities")
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.style("text-transform", "uppercase")
.attr("text-anchor", "middle");
And finally create the labels for the value in our data:
//Create labels
g.append("g")
.attr("class", "text__container")
.selectAll("text")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("text")
.text(function(d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(xKey(d)) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return customHeight - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})
I'm new to D3 and am trying to build a table like structure out of rectangles. I would like the header to be a different color than the rest of the rectangles. I've written the following code:
table = svgContainer.selectAll('rect')
.data([managedObj])
.enter()
.append('rect')
.attr("width", 120)
.attr("height", 20)
.attr("fill", "blue")
.text(function(d) {
return d.name;
});
// create table body
table.selectAll('rect')
.data(managedObj.data)
.enter()
.append('rect')
.attr("y", function() {
shift += 20;
return shift;
})
.attr("width", 120)
.attr("height", 20)
.attr("fill", "red")
.text(function(d) {
return d.name;
});
This is producing the following results:
This is almost what I intended except it is nesting the second group of rectangles inside the first rectangle. This causes only the first blue rectangle to be visible. I'm assuming this has something to do with calling the data method twice. How can I fix this issue?
I think I understand the intended result, so I'll give it a go:
This line :
table.selectAll('rect')
is selecting the rectangle just created here:
table = svgContainer.selectAll('rect')....append('rect')....
You don't want to append rectangles to that rectangle (or any rectangle for that matter) because this won't work, but you do want to append them to the SVG itself.
So instead of table.selectAll you should be using svgContainer.selectAll, but there are two other issues:
if you use svgContainer.selectAll('rect') you will be selecting the rect you have already appended, when you actually want an empty selection. See the answer here.
you cannot place text in a rect (See answer here), instead you could append g elements and then append text and rect elements to those. And, for ease of positioning, you could translate the g elements so that positioning the rectangles and text is more straight forward.
So, your code could look like:
var data = ["test1","test2","test3","test4"];
var svgContainer = d3.select('body').append('svg').attr('width',900).attr('height',400);
var header = svgContainer.selectAll('g')
.data([data])
.enter()
.append('g')
.attr('transform','translate(0,0)');
header.append('rect')
.attr("width", 120)
.attr("height", 20)
.attr("fill", "blue");
header.append('text')
.attr('y',15)
.attr('x',5)
.text(function(d) {
return "header";
});
// create table body
var boxes = svgContainer.selectAll('.box')
.data(data)
.enter()
.append('g')
.attr('class','box')
.attr('transform',function(d,i) { return 'translate(0,'+((i+1)*20)+')'; });
boxes.append('rect').attr("width", 120)
.attr("height", 20)
.attr("fill", "red");
boxes.append('text')
.attr('y',15)
.attr('x',5)
.text(function(d) {
return d;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
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();
I currently have a world map that on the click event on one of the countries in my map will add a rectangle on the same place with a different color. The problem with the code I have is that clicking the countries will result in rectangles being added in the svg(on every click a rectangle is added on top of the other) because I am appending them. What I'd like to do is delete the previous added rectangle when one of the countries is clicked and then adding the next one.
I was thinking about using .remove() but I'm not sure if that is the right way and I'm not sure how to implement it in the code.
Any help or suggestion are greatly appreciated!
Thanks in advance!
the code I have
.on("click",clicked)
function clicked(d,i) {
if(d.properties.name === "Mexico") {
var rectGroup = svg.append("g");
var rectGreen = rectGroup.append("rect")
.attr("width", 100)
.attr("height", 100)
.attr("fill", "green")
.attr("transform", "translate(50, 0)");
}else {
var rectGroup = svg.append("g");
var rectBlue = rectGroup.append("rect")
.attr("width", 100)
.attr("height", 100)
.attr("fill", "blue")
.attr("transform", "translate(50, 0)");
}
}
you can do something like
var creation=Date.now();
var rectBlue = rectGroup.append("rect")
.attr("width", 100)
.attr("height", 100)
.attr("fill", "blue")
.attr("transform", "translate(50, 0)")
.attr('id','rect_'+creation)
.attr('onclick',"removeRect('rect_"+creation+"')")
;
and then have a function
function removeRect(id){
d3.selectAll('g #'+id).remove();
}