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.
Related
I would like to use the Collide force in D3 to prevent overlaps between nodes in a force layout, but my y-axis is time-based. I would like to only use the force on the nodes' x positions.
I have tried to combine the collide force with a forceY but if I increase the collide radius I can see that nodes get pushed off frame so the Y position is not preserved.
var simulation = d3.forceSimulation(data.nodes)
.force('links', d3.forceLink(data.links))
.force('x', d3.forceX(width/2))
.force('collision', d3.forceCollide().radius(5))
.force('y', d3.forceY( function(d) {
var date = moment(d.properties.date, "YYYY-MM-DD");
var timepos = y_timescale(date)
return timepos; }));
My hunch is that I could modify the source for forceCollide() and remove y but I am just using D3 with <script src="https://d3js.org/d3.v5.min.js"></script> and I'm not sure how to start making a custom version of the force.
Edit: I have added more context in response to the answer below:
- full code sample here
- screenshot here
Not quite enough code in the question to guarantee this is what you need, but making some assumptions:
Often when using a force layout you would allow the forces to calculate the positions and then reposition the node to a given [x,y] co-ordinate on tick e.g.
function ticked() {
nodeSelection.attr('cx', d => d.x).attr('cy', d => d.y);
}
Since you don't want the forces to affect the y co-ordinate just remove it from here i.e.
nodeSelection.attr('cx', d => d.x);
And set the y position on, say, enter:
nodeSelection = nodeSelection
.enter()
.append('circle')
.attr('class', 'node')
.attr('r', 2)
.attr('cy', d => {
// Set y position based on scale here
})
.merge(nodeSelection);
I need to set up a canvas with two types of nodes, on appositive sides of the canvas, forming a column(of nodes),without node overlap, while being drag gable. That is when I drag one node down/up the others should move up or down depending on the space available.
I have tried using multiple forces, but I haven't been able to keep an strain column, nor to apply opposite forces on the canvas
simulationStart = function (nodes) {
simulation = d3.forceSimulation()
.nodes(nodes).force('collision',d3.forceCollide().radius(function(d) {
return 20;
})).force("xAxis",d3.forceX(22 ))
.on('tick', tick);
}
simulationStart(data);
Thank you in advance for you time
I was able to achieve my objective by using forceMnaybody() and getting the x coor from the data itself.
var simulation = d3.forceSimulation(data)
.force('charge', d3.forceManyBody().strength())
.force('x', d3.forceX().x(function(d) {
return d.x;
}))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}))
.on('tick', tick);
I'm hoping someone can spot where I'm going wrong here as I've looked at it for over 24 hours and can't see the issue.
I have a fairly complex dataviz working nicely in D3 but the final step is to 'adjust' any overlapping points - primarily so that their individual tooltips are accessible (on hover) (e.g. I don't want to consider alternatives like 'growing' the points to show their combined status).
So, imagine a pinboard where every pin is a point with hit_x and hit_y coordinates. Everything is working perfectly (filtering, updating, etc) in what I understand to be a fairly standard D3 'update' pattern. Sometimes two pins might have the same coordinates.
I thought I'd use D3 forces (for the first time) to recognise the 'colliding' pins and then adjust their positions accordingly. However, whilst I can get a simple version working on Blockbuilder. I can't get the same thing working when applied to my dataviz, even when I simplify it considerably.
I think perhaps I don't 100% understand the simulation process when using from an update pattern. My (simplified) code is pasted below, and here's in effect what I think it should do:
Appropriately loaded/formatted data is passed to update()
Prepare points ready to attach svg objects (same as 'standard' update pattern).
Prepare a simulation (and initially run it via ticked()).
Visualise the data.
Rerun the simulation so that the collisions are detected...
...during which, ticked() should notice the collisions and adjust the points by adjusting d.x and d.y accordingly until there are no overlaps.
I'm sure there's something obvious I'm missing - possibly related to whether I should pass the 'points' to the simulation or the original data. If anyone can spot it then I'd be very grateful. ¯\(ツ)/¯
function ticked() {
console.log("Ticking...");
points
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg group for handling/styling points later
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Data: Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d) {
return d;
});
// Forces
collisionSimulation = d3.forceSimulation(points)
.force('charge', d3.forceManyBody().strength(10))
.force('x', d3.forceX(function(d) {
return xScale(d.point.hit_x);
}).strength(0.5))
.force('y', d3.forceY(function(d) {
return yScale(d.point.hit_y);
}).strength(0.5))
.alphaTarget(1)
.on('tick', ticked);
console.log(collisionSimulation);
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d) {
return xScale(d.point.hit_x);
})
.attr('cy', function(d) {
return yScale(d.point.hit_y);
})
.attr('r', 5)
.merge(points);
collisionSimulation.nodes(points)
.force("collide", d3.forceCollide().strength(0.5).radius(function() {
return 5;
}));
collisionSimulation.alpha(0.5).restart();
}
OK, now resolved - though I would appreciate further input on the ticked() implementation.
I imagine someone will go through the same process as me at some point, so following is what I did to get things working. The crux of it is that I had been getting confused joining the forces with the visual objects. By passing the same original data to both, the forces were then able to act on the visual objects, rather than both existing as one.
I re-read the core information about data joins. I've read this a number of times over the past few months working with D3 but for some reason, reading it again made my forces / update pattern finally make (un)sense in my head.
Similarly, despite having used a few excellent Blocks examples for reference, I came across this Medium article which made things click for me. It doesn't mean the Blocks examples aren't great - just that for some reason this article helped me.
Thereafter, I updated my code and was able to get things working. I feel like I'm still missing a little bit of 'data join' magic in my ticked() function though as it seems odd to need to 'search' for the relevant items to act on rather than use a previously built reference. I'm sure I can optimise that, but if anyone else can input then great.
Hope that helps someone else out.
function ticked() {
console.log("Ticking...");
d3.select('g.points').selectAll('circle.point')
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
// Update visualisation
function update(data) {
// Animation transitions
var t1 = d3.transition().duration(3000);
// Add svg groups for organising data elements
graph.select("g.points").remove();
var pointsG = graph.append("g")
.attr('class', 'points');
// Points
// Join new data with old elements
points = pointsG.selectAll("circle.point")
.data(data, function(d){ return d; });
// Remove old elements not present in new data
points.exit()
.transition(t1)
.attr('class', 'exit')
.remove();
// Append new elements
points.enter()
.append('circle')
.attr('class', 'point')
.attr('cx', function(d){ return xScale(d.point.hit_x); })
.attr('cy', function(d){ return yScale(d.point.hit_y); })
.attr('r', 5)
.merge(points);
// Forces
collisionSimulation = d3.forceSimulation()
.nodes(data)
.force('charge', d3.forceManyBody().strength(2))
.force('x', d3.forceX(function(d) { return xScale(d.point.hit_x); }).strength(0.5))
.force('y', d3.forceY(function(d) { return yScale(d.point.hit_y); }).strength(0.5))
.force("collide", d3.forceCollide().strength(0.5).radius(function() { return 5; }))
.on('tick', ticked);
collisionSimulation.alpha(0.5).restart();
}
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>
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?