D3: How delete shape on click event? - d3.js

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();
}

Related

D3.js get data index of shape on mouseover event

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
})

d3.js trying to call data method twice on same selector produces strange results

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>

D3 how to make dynamic sequential transitions work using transform/translate and offset coordinates?

I am trying to create an interactive org chart such that when I click on a box that box is repositioned in the centre of the SVG container and all other elements transition as well but remain in the same relative position. So if you click the top box in the list, they all move down together. Then if you click one of the lower boxes they all move up together but always so the selected box is in the centre. If you click on a box which is already in the middle it should not move but at the moment they are flying all over the place.
I have got this working for the first click but on each subsequent click the boxes start flying all over the place. I am using the mouse listener to get the current position and calculate an offset to centre the selected box that I feed into transform/translate. I think this is where the strange behaviour is coming from because the offset is calculating correctly (viewed through console.log) but the applied transition is not equal to this calculation.
I have read many posts about transform/translate but they all seem to apply to a single transition, not multiple sequential transitions. I have tried using .attr(transform, null) before each new transition but this didn't work. I have also tried to dynamically extract the current x,y of the selected component and then update these attributes with the offset value but this didn't work either. Am really stuck with this and any help is greatly appreciated!
Thanks,
SD
<script type="text/javascript">
var cwidth = 1000;
var cheight = 500;
var bwidth = 100;
var bheight = 50;
// container definition
var svgContainer = d3.select("body").append("svg")
.attr("width",cwidth)
.attr("height",cheight)
.on("mousemove", mousemove);
// Background gray rectangle
svgContainer.append("svg:rect")
.attr("x",0)
.attr("y",0)
.attr("width",cwidth)
.attr("height",cheight)
.style("fill", "lightgrey");
// data
var secondData = [
{ "idx": 1, "name": "Commercial" },
{ "idx": 2, "name": "Finance" },
{ "idx": 3, "name": "Operations" },
{ "idx": 4, "name": "Business Services" }
];
var secondElements = secondData.length;
// group definition
var secondNodes = svgContainer.append("g")
.attr("class", "nodes")
.selectAll("rect")
.data(secondData)
.enter()
.append("g")
.attr("transform", function(d, i) {
d.x = 300;
d.y = ((cheight/secondElements)*d.idx)-bheight;
return "translate(" + d.x + "," + d.y + ")";
});
// Add elements to the previously added g element.
secondNodes.append("rect")
.attr("class", "node")
.attr("height", bheight)
.attr("width", bwidth)
.style("stroke", "gray")
.style("fill", "white")
.attr("y", function() {return -(bheight/2);})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", center);
// Add a text element to the previously added g element.
secondNodes.append("text")
.attr("text-anchor", "left")
.attr("x", 15)
.attr("y",5)
.text(function(d) {return d.name;});
// gets current coordinates for transition
var current = [0,0];
var xshift = 0;
var yshift = 0;
// get offset to centre from current mouse location
function mousemove() {
//console.log(d3.mouse(this));
current = d3.mouse(this);
xshift = 500 - current[0];
yshift = 250 - current[1];
}
//applies transitions
function center(d) {
secondNodes.selectAll("rect")
.transition()
.delay(0)
.duration(500)
.attr("transform", "translate(" + xshift + "," + yshift + ")")
.each("end", function() {
secondNodes.selectAll("text")
.transition()
.delay(0)
.duration(0)
.attr("transform", null);
});
}
</script>
If you want everything to keep its relative position, it seems to me that something far easier to do would be to include everything in a g element that you can set the transform attribute one. That is, instead of moving many elements, you would have to move just the top-level container. The code you have for handling clicks would remain pretty much the same except that you only need to set the transform attribute on that one element.

Why isn't my updated data re-rendering?

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.

Zooming bar-graph with d3.js

I am trying to create a bar graph with a time scale where its possible to zoom into any time period. I was able to create the zooming functionality for my x-axis(time scale) however my data (in this case rects) doesn't zoom along with my axis.
Here is a simplified version of my graph: http://jsfiddle.net/gorkem/Mf457/5/embedded/result/
As you can see I can zoom in to my x-axis but the bars do not zoom along.
and here is the jfiddle link: http://jsfiddle.net/gorkem/Mf457/6/
var y = d3.scale.linear().domain([0, max_val]).range([graph_height, 0]);
var x = d3.time.scale().domain([minDate, maxDate]).range([0, graph_width]);
var chart = d3.select(location).append("svg")
.attr("class", "chart")
.attr("width", graph_width+20)
.attr("height", graph_height+20)
.call(d3.behavior.zoom().x(x).scaleExtent([1, 8]).on("zoom", zoom));
var lines = chart.selectAll("line");
var lines_y = lines
.data(x.ticks(5))
.enter().append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", function (d) {return graph_height - 20 - d;})
.attr("y2", graph_height)
.style("stroke", "#ccc");
var lines_x = lines
.data(y.ticks(10))
.enter().append("line")
.attr("x1", 0)
.attr("x2", graph_width)
.attr("y1", y)
.attr("y2", y)
.style("stroke", "#ccc");
xAxis = d3.svg.axis().scale(x);
yAxis = d3.svg.axis().scale(y).orient("left");
chart.append("svg:g")
.attr("class", "xaxis")
.attr("transform","translate(0,300)")
.call(xAxis);
chart.append("svg:g")
.attr("class", "yaxis")
.attr("transform", "translate(25,0)")
.call(yAxis);
var rect = chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function (d,i) {return x(new Date(d["date"]))+20; })
.attr("y", function (d,i) { return graph_height - (d["num"] *v_scale);})
.attr("width", x(new Date(data[1]["date"])))
.attr("height", function (d,i) {return d["num"]*v_scale;});
rect.call(d3.behavior.zoom().x(x).scaleExtent([1, 8]).on("zoom", zoom));
function zoom() {
chart.select(".xaxis").call(xAxis);
chart.select(".yaxis").call(yAxis);
}
}
I feel like I should be adding more functionality to my zoom function for the function to be effective on bars(rects). I would really appreciate any help.
Using 'selectAll', I've applied the scaling and translation to each bar respectively, restricting both scale and translation to the x-axis only.
function zoom() {
chart.select(".xaxis").call(xAxis);
chart.select(".yaxis").call(yAxis);
chart.selectAll(".chart rect").attr("transform", "translate(" + d3.event.translate[0] + ",0)scale(" + d3.event.scale + ", 1)");
}
This works equally well with a single path element, which is what I was trying to figure out when I stumbled upon your question.
I'm wondering the same thing, so at least know you're in good company. Here's my current approach. First of all, I can't generate a plot from your code and the links don't send me to anything useful -- no chart renders. So I can't help with your specific problem. However, there is an example that has zoomable rectangles here: http://bl.ocks.org/1962173. I'm currently going through it and deleting unnecessary elements and seeing if I can make it work. Hopefully this helps!

Resources