New/updated element inherits event handlers from old one, D3 - javascript

when I create circles in D3 i can assign them e.g. onclick events
svg.selectAll("circle").data(dataArray).enter().append("circle").on("click"),function(d){// do stuff});
When I want to create new circles and therefore update the data set of my circles I do this:
svg.selectAll("circle").data(newDataSet,function(d){return d;}).enter().append("circle")
(I left the attributes out on purpose)
Is there a way to somehow inherit the on() events from my old circles or do I have to define these events again?
From my understanding it shouldn't be possible, because d3 is not object orientated.

Firstly, below is not entirely correct. enter will just give you nodes for which there is no existing DOM element. You would use merge (in v4) to update the already existing nodes.
When I want to create new circles and therefore update the data set of
my circles I do this:
svg.selectAll("circle").data(newDataSet,function(d){return d;}).enter().append("circle")
Coming to your actual question, below would assign click listener for each of the circle dom node.
svg.selectAll("circle").data(dataArray).enter().append("circle").on("click", function(d){/* do stuff */});
So, new nodes added would have to assigned new event listeners.
I think you might be missing understanding of data joins, this is an excellent read. Using data joins would look something like this:
function makeCircles(data) {
var circles = d3.select('svg')
.selectAll('circle')
.data(data, function(d) {
return d;
});
circles.exit().remove();
circles.enter()
.append('circle')
.merge(circles)
.attr('r', function(d) {
return d + 5;
})
.attr('cx', function(d, i) {
return 50 + 50 * i;
})
.attr('cy', function(d, i) {
return 30;
})
.on('click', function() {
// do stuff
})
}
var data = [1, 2, 3];
makeCircles(data);
data = [10, 15, 16];
makeCircles(data);
If your concern is about assigning multiple event listeners, why not assign event listener to parent element of circles and let the events bubble?

Related

Do I add eventListener to every SVG element if I follow normal D3 way?

The normal way of handling onclick in d3 is
selection.append(element)
.on("click", someFunction)
If I do it this way on 1000 svg elements, does it mean I just attached 1000 different listeners. If this is the case, is there event delegation for d3 specifically?
#AlexW answer is (partially) correct: there is no event delegation in D3, only event binding.
However, I said partially because it'd be better saying that "there is no native method for event delegation in D3", since in fact it's quite ease to implement it: the ugly alternative to do event delegation with D3 consists in using d3.event.target.
For instance, in this very simple demo, we bind this data...
var data = ["foo", "bar", "baz"];
... to the circles inside an <g> element. Then, we bind an event listener to the group, and get the datum of each circle on click:
g.on("click", function() {
console.log(d3.select(d3.event.target).datum())
})
Here is it:
var svg = d3.select("svg");
var g = svg.append("g");
var data = ["foo", "bar", "baz"];
var circles = g.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 40)
.attr("cx", function(_, i) {
return 50 + 100 * i
})
.attr("r", 20)
.attr("fill", "teal");
g.on("click", function() {
console.log(d3.select(d3.event.target).datum())
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
The nice thing about this approach is that, just as a jQuery event delegation, it works with elements created after the listener was defined. In the following demo, the red circle:
var svg = d3.select("svg");
var g = svg.append("g");
var data = ["foo", "bar", "baz"];
var circles = g.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("cy", 40)
.attr("cx", function(_, i) {
return 50 + 75 * i
})
.attr("r", 20)
.attr("fill", "teal");
g.on("click", function() {
console.log(d3.select(d3.event.target).datum())
});
g.append("circle")
.attr("cy", 40)
.attr("cx", 275)
.attr("r", 20)
.attr("fill", "firebrick")
.datum("foobar")
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
So, despite D3 not having a native, explicit method for event delegation, the solution is quite simple and straightforward.
Yes this would add 1000 event listeners:
Adds or removes a listener to each selected element for the specified
event typenames.
https://github.com/d3/d3-selection/blob/master/README.md#selection_on
If you have 1000+ elements, you may not want to use SVG, as the DOM gets easily bogged down with that many elements. It may be more efficient to use a canvas, etc.
D3 doesn't do event delegation it only does event binding. So you may want to implement the delegation using jQuery or vanilla JS, if you're still considering using SVG.
Here is a d3 svg event delegation implementation: d3-delegation.
You can download the node module with: npm i --save d3-delegation.
Click me to the demo.

D3 adding force to single node

I have made a grouped bubble chart using D3's force layout to create a force towards the middle, like so:
var forceStrength = 0.03;
var center = { x: widthBubbles / 2, y: heightBubbles / 2 };
var simulation = d3.forceSimulation()
.velocityDecay(0.2)
.force('x', d3.forceX().strength(forceStrength).x(center.x))
.force('y', d3.forceY().strength(forceStrength).y(center.y))
.force('charge', d3.forceManyBody().strength(charge))
.on('tick', ticked);
function charge(d) {
return -forceStrength * Math.pow(d.r, 2.0);
}
function ticked() {
bubbles
.attr('cx', function (d) { return d.x; })
.attr('cy', function (d) { return d.y; });
}
My chart looks something similar to this: Gates Foundation Educational Spending
I want to be able to apply a force on a single node when it is clicked, so that the clicked nodes will move to another area. In the documentation of D3 there is something called "positioning forces", which according to the documentation is intended to be used for multiple nodes:
While these forces can be used to position individual nodes, they are intended primarily for global forces that apply to all (or most) nodes.
So my question is if there is a way to apply forces to singles nodes in D3.
I solved the problem by creating a function, which is called every time a bubbles is clicked:
//Function for moving bubbles when selected
function moveSelectedBubbles() {
simulation.force('x', d3.forceX().strength(forceStrength).x(function(d){
//Check if bubble is selected
return (selectedCountries.indexOf(d.data.country) >= 0) ? selectedCenter.x : center.x;
}));
simulation.alpha(1).restart();
}
But this function sets the force of every node each time a bubble is selected. Maybe there is a more efficient way to handle the case.

With D3.js is it better to re-draw or "move" objects?

I've been experimenting with animation.
It's very simple to animate an object across the canvas by clearing the entire canvas and redrawing it in a new location every frame (or "tick"):
// Inside requestAnimationFrame(...) callback
// Clear canvas
canvas.selectAll('*').remove();
// ... calculate position of x and y
// x, y = ...
// Add object in new position
canvas.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 10)
.attr('fill', '#ffffff');
Is this a bad practice or am I doing it right?
For instance, if you were making a screen full of objects moving around, is it better practice to animate them by updating their attributes (e.g., x, y coordinates) in each frame?
Or, perhaps there is some other method I'm entirely unaware of, no?
Note: my animation might include 100-200 objects in view at a time.
It is better to move them, because that is the only way you can animate without errors.
In d3.js the idea is that the objects are data-bound. Clearing and redrawing the 'canvas' is not the correct approach. Firstly its not a canvas, its a web page, and any clearing and redrawing is handled by the browser itself. You job is to bind data to SVG, basically.
You need to make use of the d3 events, enter, exit, update which handles how the SVG behaves when the databound underlying data is modified and let d3 handle the animations.
the most simple example is here: https://bost.ocks.org/mike/circles/
select your elements, and store the selction in a variable
var svg= d3.select("svg");
var circles = svg.selectAll('circle');
now we need to databind something to the circle.
var databoundCircles = circles.data([12,13,14,15,66]);
This data can be anything. Usually I would expect a list of object, but these are simple numbers.
handle how things 'are made' when data appears
databoundCircles.enter().append('circle');;
handle what happens to them when data is removed
databoundCircles.exit().remove()
handle what happens when the data is updated
databoundCircles.attr('r', function(d, i) { return d * 2; })
this will change the radius when the data changes.
And recap from that tutorial:
enter - incoming elements, entering the stage.
update - persistent elements, staying on stage.
exit - outgoing elements, exiting the stage.
so in conclusion: don't do it like you are. Make sure you are using those events specifically to handle the lifecycle of elements.
PRO TIP: if you're using a list of objects make sure you bind the data by id, or some unique identifier, or the animations might behave unusually over time. Remember you are binding data to SVG you are not just wiping and redrawing a canvas!
d3.selectAll('circle').data([{id:1},{id:2}], function(d) { return d.id; });
Make note the optional second argument, that tells us how to bind the data! very important!
var svg = d3.select("svg");
//the data looks like this.
var data = [{
id: 1,
r: 3,
x: 35,
y: 30
}, {
id: 2,
r: 5,
x: 30,
y: 35
}];
//data generator makes the list above
function newList() {
//just make a simple array full of the number 1
var items = new Array(randoNum(1, 10)).fill(1)
//make the pieces of data. ID is important!
return items.map(function(val, i) {
var r = randoNum(1, 16)
return {
id: i,
r: r,
x: randoNum(1, 200) + r,
y: randoNum(1, 100) + r
}
});
}
//im just making rando numbers with this.
function randoNum(from, to) {
return Math.floor(Math.random() * (to - from) + from);
}
function update(data) {
//1. get circles (there are none in the first pass!)
var circles = svg.selectAll('circle');
//2. bind data
var databoundCircles = circles.data(data, function(d) {
return d.id;
});
//3. enter
var enter = databoundCircles.enter()
.append('circle')
.attr('r', 0)
//4. exit
databoundCircles.exit()
.transition()
.attr('r', 0)
.remove();
//5. update
//(everything after transition is tweened)
databoundCircles
.attr('fill', function(d, i){
var h = parseInt(i.toString(16));
return '#' + [h,h,h].join('');
})
.transition()
.duration(1000)
.attr('r', function(d, i) {
return d.r * 4
})
.attr('cx', function(d, i) {
return d.x * 2;
})
.attr('cy', function(d, i){
return d.y * 2
})
;
}
//first time I run, I use my example data above
update(data);
//now i update every few seconds
//watch how d3 'keeps track' of each circle
setInterval(function() {
update(newList());
}, 2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="300">
</svg>
Is this a bad practice or am I doing it right?
Yes, it is a bad practice. In a normal circumstance I like to call it lazy coding: clearing the SVG (or whatever) and painting the dataviz again.
But, in your case, it's even worse: you will end up writing a huge amount of code (not exactly laziness, though), ignoring d3.transition(), which can easily do what you want. And that takes us to your second question:
Or, perhaps there is some other method I'm entirely unaware of, no?
Yes, as I just said, it's called transition(): https://github.com/d3/d3-transition
Then, at the end, you said:
Note: my animation might include 100-200 objects in view at a time.
First, modern browsers can handle that very well. Second, you still have to remove and repaint manually all that elements. If you benchmark the two approaches, maybe this is even worse.
Thus, just use d3.transition().
You can change the data (or the attributes) of the elements anytime you want, and "moving" (or transitioning) them to the new value calling a transition. For instance, to move this circle around, I don't have to remove it and painting it again:
var circle = d3.select("circle")
setInterval(() => {
circle.transition()
.duration(900)
.attr("cx", Math.random() * 300)
.attr("cy", Math.random() * 150)
.ease(d3.easeElastic);
}, 1000)
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg>
<circle r="10" cx="100" cy="50" fill="teal"></circle>
</svg>

d3.js iterate order changes

As project to get to know d3.js, I’m displaying tweets on a map in real-time. Everything has worked this far, and I’m very, very pleased with the library.
On the page, I’m trying to list all languages. On hover, I want all tweets of that language to pop up. All of this is done, except some items in the list pops up the tweets of another language. A wrong one, I might add.
This is how I project the dots on the map:
points.selectAll('circle')
.data(tweets)
.enter()
.append('circle')
// Attach node to tweet, so I can use refer to the nodes later
.each(function(d) {
d.node = this;
})
.attr('r', 1);
This is how I create the list:
var data = d3.nest()
// Group by language code
.key(function(d) { return d.lang.code; })
.entries(tweets)
// Sort list by amount of tweets in that language
.sort(function(a, b) {
return b.values.length - a.values.length;
});
var items = languages_dom
// Add items
.selectAll('li')
.data(data)
.enter()
.append('li');
// Used for debugging
.attr('data-lang', function(d) {
return d.key; // Group key = language code
})
// Set text
.text(function(d) {
var dt = d.values[0];
return dt.lang.name;
})
// Mouseover handler
.on('mouseover', function(d) {
// Compare attribute with
// These values are actually different
var attr = d3.select(this).attr('data-lang');
console.log(attr, d.key);
// Pop up each node
d.values.forEach(function(d) {
d = d3.select(d.node);
d.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
});
Note that the script above is run several times. d.key refers to another value later in the chain, while I’m not modifying data in that chain.
Edit 22:08
Things seems to work fine when I’m not sorting the data. At least it’s a lead.
As noted in the comments, you're overwriting d in your forEach function. Instead, you should try something like
d.values.forEach(function(p) {
d3.select(p.node)
.transition()
.duration(200)
.attr('opacity', 0.5)
.attr('r', 8);
});
Notice the forEach variable is named p instead of d.
As the data changed, the old data seems to be kept somehow.
Either way, I simply deleted the list before applying the new data:
languages_dom
.selectAll('li')
.remove();
Can’t say this is graceful, nor performant, but it gets the job done :)

Referencing different sets of elements using d3 js

This has to be simple, but it's been a while since I was using d3.js and I can't figure out a good solution.
I have a single set of data and I'm using it to create two sets of elements
circles = svg.selectAll('.highcircles')
.data(data)
.enter()
.append('circle');
and
list.selectAll('.states-list')
.data(data)
.enter()
.append('p');
I'd like to be able to have on mouseover of the <p> tags, to have the related circle animate. I can't though think of the way to link the two. Is it through a data-state attribute? Is there a better solution?
selection.filter can be used to filter down a selection based on data. You can use the datum from the <p> event target to filter down a <circle> selection like this:
var circleMatch = svg.selectAll(".highcircles")
.filter(function(d) {
return d.key === targetDatum.key; // 'key' is some datum-unique property
});
You can add "id" attributes to your circles, and then reference those ids in your mouseover function. Something like this:
circles.attr("id", function(d) { return "id" + d; })
list.on('mouseover', function(d) {
d3.select("#id" + d)
.style("fill", "yellow")
})
http://jsfiddle.net/woodedlawn/7ZqZx/

Categories

Resources